Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dfa7cc62e | ||
|
|
23e93bfd01 | ||
|
|
16f9f4671e | ||
|
|
0c300e5e72 | ||
|
|
b9a1e78b53 | ||
|
|
337f7cdab4 | ||
|
|
1b0390f0b7 | ||
|
|
bc52933b58 | ||
|
|
2fdad2344c | ||
|
|
0c7eaa2df2 | ||
|
|
a5f8e56b3c | ||
|
|
8ffde62c1a | ||
|
|
39247b77a2 | ||
|
|
984b2aeee2 | ||
|
|
eed104be5b | ||
|
|
00bd535b3c | ||
|
|
18e914242f | ||
|
|
e68837a34a | ||
|
|
b30162d98b | ||
|
|
dba372d25a | ||
|
|
2eb48e75d3 | ||
|
|
867903cd5f | ||
|
|
8aeb1df0ad | ||
|
|
6bea827293 | ||
|
|
a119854da7 | ||
|
|
bfbfd7b843 | ||
|
|
0ca7008735 | ||
|
|
4bc71c52ff | ||
|
|
8f27f10dec | ||
|
|
a93ec2cab9 | ||
|
|
386e40a0bf | ||
|
|
bda150d4b6 | ||
|
|
87836e53d1 | ||
|
|
7e0483f1a5 | ||
|
|
309ee9cb0f | ||
|
|
79b885502e | ||
|
|
745bdee86d | ||
|
|
08eb9cc55f | ||
|
|
778f85d492 | ||
|
|
fb92be7d3e | ||
|
|
6df588f40e | ||
|
|
b46ed58dff | ||
|
|
0d9f57a9c9 | ||
|
|
b7dbe54c83 | ||
|
|
43d1dfff71 | ||
|
|
9949a46ee3 | ||
|
|
dfbcf05b2f | ||
|
|
f56fab9876 | ||
|
|
c9ea7da092 | ||
|
|
22d46e1e90 | ||
|
|
6af94afc56 | ||
|
|
f7f92903de | ||
|
|
b35e17526b | ||
|
|
bf32c08d37 | ||
|
|
5d337409d6 | ||
|
|
67f5ac303e | ||
|
|
578546a171 | ||
|
|
aa0b629a3e | ||
|
|
69beaa0a83 | ||
|
|
4fcc49d49a | ||
|
|
d15985e399 | ||
|
|
d429128e65 | ||
|
|
2fce8326b6 | ||
|
|
433c68a33d | ||
|
|
58acb65f12 | ||
|
|
7978955819 | ||
|
|
c6118e0cdb | ||
|
|
7d063b905f | ||
|
|
e0ff198c3f | ||
|
|
8b86e1c229 | ||
|
|
24be28a662 | ||
|
|
c6788b4917 | ||
|
|
4a52bf02e6 | ||
|
|
08b627cb8c | ||
|
|
73f542adad | ||
|
|
0d11b0c55a | ||
|
|
5b9d2bd1e3 | ||
|
|
cf1e141837 | ||
|
|
3894a22174 | ||
|
|
cad155e655 | ||
|
|
4477b1ca1f | ||
|
|
cd443466c7 | ||
|
|
18012ddab1 | ||
|
|
beb015194f | ||
|
|
c3904d9fdd | ||
|
|
aee5779983 | ||
|
|
765a1c4354 | ||
|
|
86840a8822 | ||
|
|
487fb2d5c1 | ||
|
|
54d5e96a6d | ||
|
|
481ad3c844 | ||
|
|
0ce85cf76b | ||
|
|
5849e4586c | ||
|
|
34c0a7163f | ||
|
|
89e3ceab00 | ||
|
|
5a5e64abef | ||
|
|
2368e0d263 | ||
|
|
547149da44 | ||
|
|
a1144bbf76 | ||
|
|
6b8d637b75 | ||
|
|
fd47eb7f4b | ||
|
|
7db86dcf8c | ||
|
|
e75323c16e | ||
|
|
97d01d7201 | ||
|
|
90b42a4bb7 | ||
|
|
fbf2fe919c | ||
|
|
d3ddf7c51e | ||
|
|
5759241573 | ||
|
|
3747abbc3b | ||
|
|
226e6cf1ce | ||
|
|
1778abb683 | ||
|
|
90a20dd1b0 | ||
|
|
21c9129e14 | ||
|
|
19d2d0bddd | ||
|
|
83c43332d4 | ||
|
|
3a1b8d1db1 | ||
|
|
46426e27b4 | ||
|
|
9402822fa3 | ||
|
|
651fe361fc | ||
|
|
aee1713aec | ||
|
|
ecfa14829b | ||
|
|
92e3ec785c | ||
|
|
8102f19f79 | ||
|
|
840a00ebcd | ||
|
|
181f96d250 | ||
|
|
ce2389f135 | ||
|
|
f15dc77f33 | ||
|
|
caa81c24a6 | ||
|
|
e3cb62788c | ||
|
|
fc46cbb893 | ||
|
|
d94a71e9e1 | ||
|
|
cf81253535 | ||
|
|
25c4b42538 | ||
|
|
f7a6e0cb5e | ||
|
|
85275e5dd6 | ||
|
|
4e5b467ce5 | ||
|
|
874aa5ab75 | ||
|
|
0940d72d5d | ||
|
|
0d1739d70f | ||
|
|
60fe0843ac | ||
|
|
794f226209 | ||
|
|
2fbf3476b8 | ||
|
|
897ac60a82 | ||
|
|
18f228ca1d | ||
|
|
14de30b7aa | ||
|
|
3faa39e787 | ||
|
|
63b5ba0bb9 |
5
.github/workflows/ci.yaml
vendored
@@ -24,4 +24,7 @@ jobs:
|
||||
run: npm run lint
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
run: npm run build
|
||||
|
||||
- name: Run tests
|
||||
run: npm run test:ci
|
||||
33
.github/workflows/cla.yaml
vendored
Normal file
@@ -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:
|
||||
11
.github/workflows/publish.yaml
vendored
@@ -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 }}
|
||||
|
||||
184
CHANGELOG.md
@@ -1,5 +1,189 @@
|
||||
# Changelog
|
||||
|
||||
## [1.14.0](https://github.com/chartdb/chartdb/compare/v1.13.2...v1.14.0) (2025-08-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add floating "Show All" button when tables are out of view ([#787](https://github.com/chartdb/chartdb/issues/787)) ([bda150d](https://github.com/chartdb/chartdb/commit/bda150d4b6d6fb90beb423efba69349d21a037a5))
|
||||
* add table selection for large database imports ([#776](https://github.com/chartdb/chartdb/issues/776)) ([0d9f57a](https://github.com/chartdb/chartdb/commit/0d9f57a9c969a67e350d6bf25e07c3a9ef5bba39))
|
||||
* **canvas:** Add filter tables on canvas ([#774](https://github.com/chartdb/chartdb/issues/774)) ([dfbcf05](https://github.com/chartdb/chartdb/commit/dfbcf05b2f595f5b7b77dd61abf77e6e07acaf8f))
|
||||
* **custom-types:** add highlight fields option for custom types ([#726](https://github.com/chartdb/chartdb/issues/726)) ([7e0483f](https://github.com/chartdb/chartdb/commit/7e0483f1a5512a6a737baf61caf7513e043f2e96))
|
||||
* **datatypes:** Add decimal / numeric attribute support + organize field row ([#715](https://github.com/chartdb/chartdb/issues/715)) ([778f85d](https://github.com/chartdb/chartdb/commit/778f85d49214232a39710e47bb5d4ec41b75d427))
|
||||
* **dbml:** Edit Diagram Directly from DBML ([#819](https://github.com/chartdb/chartdb/issues/819)) ([1b0390f](https://github.com/chartdb/chartdb/commit/1b0390f0b7652fe415540b7942cf53ec87143f08))
|
||||
* **default value:** add default value option to table field settings ([#770](https://github.com/chartdb/chartdb/issues/770)) ([c9ea7da](https://github.com/chartdb/chartdb/commit/c9ea7da0923ff991cb936235674d9a52b8186137))
|
||||
* enhance primary key and unique field handling logic ([#817](https://github.com/chartdb/chartdb/issues/817)) ([39247b7](https://github.com/chartdb/chartdb/commit/39247b77a299caa4f29ea434af3028155c6d37ed))
|
||||
* implement area grouping with parent-child relationships ([#762](https://github.com/chartdb/chartdb/issues/762)) ([b35e175](https://github.com/chartdb/chartdb/commit/b35e17526b3c9b918928ae5f3f89711ea7b2529c))
|
||||
* **schema:** support create new schema ([#801](https://github.com/chartdb/chartdb/issues/801)) ([867903c](https://github.com/chartdb/chartdb/commit/867903cd5f24d96ce1fe718dc9b562e2f2b75276))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add open and create diagram to side menu ([#757](https://github.com/chartdb/chartdb/issues/757)) ([67f5ac3](https://github.com/chartdb/chartdb/commit/67f5ac303ebf5ada97d5c80fb08a2815ca205a91))
|
||||
* add PostgreSQL tests and fix parsing SQL ([#760](https://github.com/chartdb/chartdb/issues/760)) ([5d33740](https://github.com/chartdb/chartdb/commit/5d337409d64d1078b538350016982a98e684c06c))
|
||||
* area resizers size ([#830](https://github.com/chartdb/chartdb/issues/830)) ([23e93bf](https://github.com/chartdb/chartdb/commit/23e93bfd01d741dd3d11aa5c479cef97e1a86fa6))
|
||||
* **area:** redo/undo after dragging an area with tables ([#767](https://github.com/chartdb/chartdb/issues/767)) ([6af94af](https://github.com/chartdb/chartdb/commit/6af94afc56cf8987b8fc9e3f0a9bfa966de35408))
|
||||
* **canvas filter:** improve scroller on canvas filter ([#799](https://github.com/chartdb/chartdb/issues/799)) ([6bea827](https://github.com/chartdb/chartdb/commit/6bea82729362a8c7b73dc089ddd9e52bae176aa2))
|
||||
* **canvas:** fix filter eye button ([#780](https://github.com/chartdb/chartdb/issues/780)) ([b7dbe54](https://github.com/chartdb/chartdb/commit/b7dbe54c83c75cfe3c556f7a162055dcfe2de23d))
|
||||
* clone of custom types ([#804](https://github.com/chartdb/chartdb/issues/804)) ([b30162d](https://github.com/chartdb/chartdb/commit/b30162d98bc659a61aae023cdeaead4ce25c7ae9))
|
||||
* **cockroachdb:** support schema creation for cockroachdb ([#803](https://github.com/chartdb/chartdb/issues/803)) ([dba372d](https://github.com/chartdb/chartdb/commit/dba372d25a8c642baf8600d05aa154882729d446))
|
||||
* **dbml actions:** set dbml tooltips side ([#798](https://github.com/chartdb/chartdb/issues/798)) ([a119854](https://github.com/chartdb/chartdb/commit/a119854da7c935eb595984ea9398e04136ce60c4))
|
||||
* **dbml editor:** move tooltips button to be on the right ([#797](https://github.com/chartdb/chartdb/issues/797)) ([bfbfd7b](https://github.com/chartdb/chartdb/commit/bfbfd7b843f96c894b1966ad95393b866c927466))
|
||||
* **dbml export:** fix handle tables with same name under different schemas ([#807](https://github.com/chartdb/chartdb/issues/807)) ([18e9142](https://github.com/chartdb/chartdb/commit/18e914242faccd6376fe5a7cd5a4478667f065ee))
|
||||
* **dbml export:** handle tables with same name under different schemas ([#806](https://github.com/chartdb/chartdb/issues/806)) ([e68837a](https://github.com/chartdb/chartdb/commit/e68837a34aa635fb6fc02c7f1289495e5c448242))
|
||||
* **dbml field comments:** support export field comments in dbml ([#796](https://github.com/chartdb/chartdb/issues/796)) ([0ca7008](https://github.com/chartdb/chartdb/commit/0ca700873577bbfbf1dd3f8088c258fc89b10c53))
|
||||
* **dbml import:** fix dbml import types + schemas ([#808](https://github.com/chartdb/chartdb/issues/808)) ([00bd535](https://github.com/chartdb/chartdb/commit/00bd535b3c62d26d25a6276d52beb10e26afad76))
|
||||
* **dbml-export:** merge field attributes into single brackets and fix schema syntax ([#790](https://github.com/chartdb/chartdb/issues/790)) ([309ee9c](https://github.com/chartdb/chartdb/commit/309ee9cb0ff1f5a68ed183e3919e1a11a8410909))
|
||||
* **dbml-import:** handle unsupported DBML features and add comprehensive tests ([#766](https://github.com/chartdb/chartdb/issues/766)) ([22d46e1](https://github.com/chartdb/chartdb/commit/22d46e1e90729730cc25dd6961bfe8c3d2ae0c98))
|
||||
* **dbml:** dbml indentation ([#829](https://github.com/chartdb/chartdb/issues/829)) ([16f9f46](https://github.com/chartdb/chartdb/commit/16f9f4671e011eb66ba9594bed47570eda3eed66))
|
||||
* **dbml:** dbml note syntax ([#826](https://github.com/chartdb/chartdb/issues/826)) ([337f7cd](https://github.com/chartdb/chartdb/commit/337f7cdab4759d15cb4d25a8c0e9394e99ba33d4))
|
||||
* **dbml:** fix dbml output format ([#815](https://github.com/chartdb/chartdb/issues/815)) ([eed104b](https://github.com/chartdb/chartdb/commit/eed104be5ba2b7d9940ffac38e7877722ad764fc))
|
||||
* **dbml:** fix schemas with same table names ([#828](https://github.com/chartdb/chartdb/issues/828)) ([0c300e5](https://github.com/chartdb/chartdb/commit/0c300e5e72cc5ff22cac42f8dbaed167061157c6))
|
||||
* **dbml:** import dbml notes (table + fields) ([#827](https://github.com/chartdb/chartdb/issues/827)) ([b9a1e78](https://github.com/chartdb/chartdb/commit/b9a1e78b53c932c0b1a12ee38b62494a5c2f9348))
|
||||
* **dbml:** support multiple relationships on same field in inline DBML ([#822](https://github.com/chartdb/chartdb/issues/822)) ([a5f8e56](https://github.com/chartdb/chartdb/commit/a5f8e56b3ca97b851b6953481644d3a3ff7ce882))
|
||||
* **dbml:** support spaces in names ([#794](https://github.com/chartdb/chartdb/issues/794)) ([8f27f10](https://github.com/chartdb/chartdb/commit/8f27f10dec96af400dc2c12a30b22b3a346803a9))
|
||||
* fix hotkeys on form elements ([#778](https://github.com/chartdb/chartdb/issues/778)) ([43d1dff](https://github.com/chartdb/chartdb/commit/43d1dfff71f2b960358a79b0112b78d11df91fb7))
|
||||
* fix screen freeze after schema select ([#800](https://github.com/chartdb/chartdb/issues/800)) ([8aeb1df](https://github.com/chartdb/chartdb/commit/8aeb1df0ad353c49e91243453f24bfa5921a89ab))
|
||||
* **i18n:** add Croatian (hr) language support ([#802](https://github.com/chartdb/chartdb/issues/802)) ([2eb48e7](https://github.com/chartdb/chartdb/commit/2eb48e75d303d622f51327d22502a6f78e7fb32d))
|
||||
* improve SQL export formatting and add schema-aware FK grouping ([#783](https://github.com/chartdb/chartdb/issues/783)) ([6df588f](https://github.com/chartdb/chartdb/commit/6df588f40e6e7066da6125413b94466429d48767))
|
||||
* lost in canvas button animation ([#793](https://github.com/chartdb/chartdb/issues/793)) ([a93ec2c](https://github.com/chartdb/chartdb/commit/a93ec2cab906d0e4431d8d1668adcf2dbfc3c80f))
|
||||
* **readonly:** fix zoom out on readonly ([#818](https://github.com/chartdb/chartdb/issues/818)) ([8ffde62](https://github.com/chartdb/chartdb/commit/8ffde62c1a00893c4bf6b4dd39068df530375416))
|
||||
* remove error lag after autofix ([#764](https://github.com/chartdb/chartdb/issues/764)) ([bf32c08](https://github.com/chartdb/chartdb/commit/bf32c08d37c02ee6d7946a41633bb97b2271fcb7))
|
||||
* remove unnecessary import ([#791](https://github.com/chartdb/chartdb/issues/791)) ([87836e5](https://github.com/chartdb/chartdb/commit/87836e53d145b825f9c4f80abca72f418df50e6c))
|
||||
* **scroll:** disable scroll x behavior ([#795](https://github.com/chartdb/chartdb/issues/795)) ([4bc71c5](https://github.com/chartdb/chartdb/commit/4bc71c52ff5c462800d8530b72a5aadb7d7f85ed))
|
||||
* set focus on filter search ([#775](https://github.com/chartdb/chartdb/issues/775)) ([9949a46](https://github.com/chartdb/chartdb/commit/9949a46ee3ba7f46a2ea7f2c0d7101cc9336df4f))
|
||||
* solve issue with multiple render of tables ([#823](https://github.com/chartdb/chartdb/issues/823)) ([0c7eaa2](https://github.com/chartdb/chartdb/commit/0c7eaa2df20cfb6994b7e6251c760a2d4581c879))
|
||||
* **sql-export:** escape newlines and quotes in multi-line comments ([#765](https://github.com/chartdb/chartdb/issues/765)) ([f7f9290](https://github.com/chartdb/chartdb/commit/f7f92903def84a94ac0c66f625f96a6681383945))
|
||||
* **sql-server:** improvment for sql-server import via sql script ([#789](https://github.com/chartdb/chartdb/issues/789)) ([79b8855](https://github.com/chartdb/chartdb/commit/79b885502e3385e996a52093a3ccd5f6e469993a))
|
||||
* **table-node:** fix comment icon on field ([#786](https://github.com/chartdb/chartdb/issues/786)) ([745bdee](https://github.com/chartdb/chartdb/commit/745bdee86d07f1e9c3a2d24237c48c25b9a8eeea))
|
||||
* **table-node:** improve field spacing ([#785](https://github.com/chartdb/chartdb/issues/785)) ([08eb9cc](https://github.com/chartdb/chartdb/commit/08eb9cc55f0077f53afea6f9ce720341e1a583c2))
|
||||
* **table-select:** add loading indication for import ([#782](https://github.com/chartdb/chartdb/issues/782)) ([b46ed58](https://github.com/chartdb/chartdb/commit/b46ed58dff1ec74579fb1544dba46b0f77730c52))
|
||||
* **ui:** reduce spacing between primary key icon and short field types ([#816](https://github.com/chartdb/chartdb/issues/816)) ([984b2ae](https://github.com/chartdb/chartdb/commit/984b2aeee22c43cb9bda77df2c22087973079af4))
|
||||
* update MariaDB database import smart query ([#792](https://github.com/chartdb/chartdb/issues/792)) ([386e40a](https://github.com/chartdb/chartdb/commit/386e40a0bf93d9aef1486bb1e729d8f485e675eb))
|
||||
* update multiple schemas toast to require user action ([#771](https://github.com/chartdb/chartdb/issues/771)) ([f56fab9](https://github.com/chartdb/chartdb/commit/f56fab9876fb9fc46c6c708231324a90d8a7851d))
|
||||
* update relationship when table width changes via expand/shrink ([#825](https://github.com/chartdb/chartdb/issues/825)) ([bc52933](https://github.com/chartdb/chartdb/commit/bc52933b58bfe6bc73779d9401128254cbf497d5))
|
||||
|
||||
## [1.13.2](https://github.com/chartdb/chartdb/compare/v1.13.1...v1.13.2) (2025-07-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add DISABLE_ANALYTICS flag to opt-out of Fathom analytics ([#750](https://github.com/chartdb/chartdb/issues/750)) ([aa0b629](https://github.com/chartdb/chartdb/commit/aa0b629a3eaf8e8b60473ea3f28f769270c7714c))
|
||||
|
||||
## [1.13.1](https://github.com/chartdb/chartdb/compare/v1.13.0...v1.13.1) (2025-07-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **custom_types:** fix display custom types in select box ([#737](https://github.com/chartdb/chartdb/issues/737)) ([24be28a](https://github.com/chartdb/chartdb/commit/24be28a662c48fc5bc62e76446b9669d83d7d3e0))
|
||||
* **dbml-editor:** for some cases that the dbml had issues ([#739](https://github.com/chartdb/chartdb/issues/739)) ([e0ff198](https://github.com/chartdb/chartdb/commit/e0ff198c3fd416498dac5680bb323ec88c54b65c))
|
||||
* **dbml:** Filter duplicate tables at diagram level before export dbml ([#746](https://github.com/chartdb/chartdb/issues/746)) ([d429128](https://github.com/chartdb/chartdb/commit/d429128e65aa28c500eac2487356e4869506e948))
|
||||
* **export-sql:** conditionally show generic option and reorder by diagram type ([#708](https://github.com/chartdb/chartdb/issues/708)) ([c6118e0](https://github.com/chartdb/chartdb/commit/c6118e0cdb0e5caaf73447d33db2fde1a98efe60))
|
||||
* general performance improvements on canvas ([#751](https://github.com/chartdb/chartdb/issues/751)) ([4fcc49d](https://github.com/chartdb/chartdb/commit/4fcc49d49a76a4b886ffd6cf0b40cf2fc49952ec))
|
||||
* **import-database:** for custom types query to import supabase & timescale ([#745](https://github.com/chartdb/chartdb/issues/745)) ([2fce832](https://github.com/chartdb/chartdb/commit/2fce8326b67b751d38dd34f409fea574449d0298))
|
||||
* **import-db:** fix mariadb import ([#740](https://github.com/chartdb/chartdb/issues/740)) ([7d063b9](https://github.com/chartdb/chartdb/commit/7d063b905f19f51501468bd0bd794a25cf65e1be))
|
||||
* **performance:** improve storage provider performance ([#734](https://github.com/chartdb/chartdb/issues/734)) ([c6788b4](https://github.com/chartdb/chartdb/commit/c6788b49173d9cce23571daeb460285cb7cffb11))
|
||||
* resolve unresponsive cursor and input glitches when editing field comments ([#749](https://github.com/chartdb/chartdb/issues/749)) ([d15985e](https://github.com/chartdb/chartdb/commit/d15985e3999a0cd54213b2fb08c55d48a1b8b3b2))
|
||||
* **table name:** updates table name value when its updated from canvas/sidebar ([#716](https://github.com/chartdb/chartdb/issues/716)) ([8b86e1c](https://github.com/chartdb/chartdb/commit/8b86e1c22992aaadcce7ad5fc1d267c5a57a99f0))
|
||||
|
||||
## [1.13.0](https://github.com/chartdb/chartdb/compare/v1.12.0...v1.13.0) (2025-05-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **custom-types:** add enums and composite types for Postgres ([#714](https://github.com/chartdb/chartdb/issues/714)) ([c3904d9](https://github.com/chartdb/chartdb/commit/c3904d9fdd63ef5b76a44e73582d592f2c418687))
|
||||
* **export-sql:** add custom types to export sql script ([#720](https://github.com/chartdb/chartdb/issues/720)) ([cad155e](https://github.com/chartdb/chartdb/commit/cad155e6550f171b8faecbfdff27032798ecea43))
|
||||
* **oracle:** support oracle in ChartDB ([#709](https://github.com/chartdb/chartdb/issues/709)) ([765a1c4](https://github.com/chartdb/chartdb/commit/765a1c43547a29bd3428c942c7afb56f63aaf046))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **canvas:** prevent canvas blink and lag on field edit ([#723](https://github.com/chartdb/chartdb/issues/723)) ([cd44346](https://github.com/chartdb/chartdb/commit/cd443466c7952f1cdc3739645c12130b9231e3a1))
|
||||
* **canvas:** prevent canvas blink and lag on primary field edit ([#725](https://github.com/chartdb/chartdb/issues/725)) ([4477b1c](https://github.com/chartdb/chartdb/commit/4477b1ca1fe6b282b604739a23e31181acd4d7bc))
|
||||
* **custom_types:** fix custom types on storage provider ([#721](https://github.com/chartdb/chartdb/issues/721)) ([beb0151](https://github.com/chartdb/chartdb/commit/beb015194f917c0ba644458410162d2b7599918c))
|
||||
* **custom_types:** fix custom types on storage provider ([#722](https://github.com/chartdb/chartdb/issues/722)) ([18012dd](https://github.com/chartdb/chartdb/commit/18012ddab1718bcce3432aea626adf6fc9be25d9))
|
||||
* **custom-types:** fetch directly via the smart-query the custom types ([#729](https://github.com/chartdb/chartdb/issues/729)) ([cf1e141](https://github.com/chartdb/chartdb/commit/cf1e141837eda77d717ad87489ce9946b688e226))
|
||||
* **dbml-editor:** export comments with schema if existsed ([#728](https://github.com/chartdb/chartdb/issues/728)) ([73f542a](https://github.com/chartdb/chartdb/commit/73f542adad2d66a1e84fc656a0c34d9b1f39f33c))
|
||||
* **dbml-editor:** fix export dbml - to show enums ([#724](https://github.com/chartdb/chartdb/issues/724)) ([3894a22](https://github.com/chartdb/chartdb/commit/3894a221745d32c13160bedcb1bcf53d89897698))
|
||||
* **import-database:** remove the default fetch from import database ([#718](https://github.com/chartdb/chartdb/issues/718)) ([0d11b0c](https://github.com/chartdb/chartdb/commit/0d11b0c55a94a12a764785cfdcf2ba10437241d6))
|
||||
* **menu:** add oracle to import menu ([#713](https://github.com/chartdb/chartdb/issues/713)) ([aee5779](https://github.com/chartdb/chartdb/commit/aee577998342eb4a2b05b3e03181992a435712d8))
|
||||
* **relationship:** fix creating of relationships ([#732](https://github.com/chartdb/chartdb/issues/732)) ([08b627c](https://github.com/chartdb/chartdb/commit/08b627cb8ca8fdf08d8ed2ff7e89104887deffb7))
|
||||
|
||||
## [1.12.0](https://github.com/chartdb/chartdb/compare/v1.11.0...v1.12.0) (2025-05-20)
|
||||
|
||||
|
||||
### 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)
|
||||
|
||||
|
||||
|
||||
45
CLA.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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 it’s 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)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@ FROM node:22-alpine AS builder
|
||||
ARG VITE_OPENAI_API_KEY
|
||||
ARG VITE_OPENAI_API_ENDPOINT
|
||||
ARG VITE_LLM_MODEL_NAME
|
||||
ARG VITE_HIDE_BUCKLE_DOT_DEV
|
||||
ARG VITE_HIDE_CHARTDB_CLOUD
|
||||
ARG VITE_DISABLE_ANALYTICS
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
@@ -16,7 +17,8 @@ COPY . .
|
||||
RUN echo "VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}" > .env && \
|
||||
echo "VITE_OPENAI_API_ENDPOINT=${VITE_OPENAI_API_ENDPOINT}" >> .env && \
|
||||
echo "VITE_LLM_MODEL_NAME=${VITE_LLM_MODEL_NAME}" >> .env && \
|
||||
echo "VITE_HIDE_BUCKLE_DOT_DEV=${VITE_HIDE_BUCKLE_DOT_DEV}" >> .env
|
||||
echo "VITE_HIDE_CHARTDB_CLOUD=${VITE_HIDE_CHARTDB_CLOUD}" >> .env && \
|
||||
echo "VITE_DISABLE_ANALYTICS=${VITE_DISABLE_ANALYTICS}" >> .env
|
||||
|
||||
RUN npm run build
|
||||
|
||||
|
||||
42
README.md
@@ -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 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.
|
||||
- **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
|
||||
@@ -123,6 +125,8 @@ docker run \
|
||||
-p 8080:80 chartdb
|
||||
```
|
||||
|
||||
> **Privacy Note:** ChartDB includes privacy-focused analytics via Fathom Analytics. You can disable this by adding `-e DISABLE_ANALYTICS=true` to the run command or `--build-arg VITE_DISABLE_ANALYTICS=true` when building.
|
||||
|
||||
> **Note:** You must configure either Option 1 (OpenAI API key) OR Option 2 (Custom endpoint and model name) for AI capabilities to work. Do not mix the two options.
|
||||
|
||||
Open your browser and navigate to `http://localhost:8080`.
|
||||
@@ -145,9 +149,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
|
||||
|
||||
|
||||
@@ -10,11 +10,12 @@ server {
|
||||
|
||||
location /config.js {
|
||||
default_type application/javascript;
|
||||
return 200 "window.env = {
|
||||
return 200 "window.env = {
|
||||
OPENAI_API_KEY: \"$OPENAI_API_KEY\",
|
||||
OPENAI_API_ENDPOINT: \"$OPENAI_API_ENDPOINT\",
|
||||
LLM_MODEL_NAME: \"$LLM_MODEL_NAME\",
|
||||
HIDE_BUCKLE_DOT_DEV: \"$HIDE_BUCKLE_DOT_DEV\"
|
||||
HIDE_CHARTDB_CLOUD: \"$HIDE_CHARTDB_CLOUD\",
|
||||
DISABLE_ANALYTICS: \"$DISABLE_ANALYTICS\"
|
||||
};";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Replace placeholders in nginx.conf
|
||||
envsubst '${OPENAI_API_KEY} ${OPENAI_API_ENDPOINT} ${LLM_MODEL_NAME} ${HIDE_BUCKLE_DOT_DEV}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
|
||||
envsubst '${OPENAI_API_KEY} ${OPENAI_API_ENDPOINT} ${LLM_MODEL_NAME} ${HIDE_CHARTDB_CLOUD} ${DISABLE_ANALYTICS}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
|
||||
|
||||
# Start Nginx
|
||||
nginx -g "daemon off;"
|
||||
|
||||
20
index.html
@@ -13,11 +13,21 @@
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<script src="/config.js"></script>
|
||||
<script
|
||||
src="https://cdn.usefathom.com/script.js"
|
||||
data-site="PRHIVBNN"
|
||||
defer
|
||||
></script>
|
||||
<script>
|
||||
// Load analytics only if not disabled
|
||||
(function() {
|
||||
const disableAnalytics = (window.env && window.env.DISABLE_ANALYTICS === 'true') ||
|
||||
(typeof process !== 'undefined' && process.env && process.env.VITE_DISABLE_ANALYTICS === 'true');
|
||||
|
||||
if (!disableAnalytics) {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.usefathom.com/script.js';
|
||||
script.setAttribute('data-site', 'PRHIVBNN');
|
||||
script.defer = true;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
1546
package-lock.json
generated
31
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chartdb",
|
||||
"private": true,
|
||||
"version": "1.9.0",
|
||||
"version": "1.14.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -9,7 +9,11 @@
|
||||
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
||||
"lint:fix": "npm run lint -- --fix",
|
||||
"preview": "vite preview",
|
||||
"prepare": "husky"
|
||||
"prepare": "husky",
|
||||
"test": "vitest",
|
||||
"test:ci": "vitest run --reporter=verbose --bail=1",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^0.0.51",
|
||||
@@ -22,7 +26,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 +35,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.2.3",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@xyflow/react": "^12.3.1",
|
||||
"@xyflow/react": "^12.8.2",
|
||||
"ahooks": "^3.8.1",
|
||||
"ai": "^3.3.14",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"dexie": "^4.0.8",
|
||||
@@ -50,8 +54,9 @@
|
||||
"html-to-image": "^1.11.11",
|
||||
"i18next": "^23.14.0",
|
||||
"i18next-browser-languagedetector": "^8.0.0",
|
||||
"lucide-react": "^0.441.0",
|
||||
"lucide-react": "^0.525.0",
|
||||
"monaco-editor": "^0.52.0",
|
||||
"motion": "^12.23.6",
|
||||
"nanoid": "^5.0.7",
|
||||
"node-sql-parser": "^5.3.2",
|
||||
"react": "^18.3.1",
|
||||
@@ -73,12 +78,16 @@
|
||||
"@eslint/compat": "^1.2.4",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||
"@typescript-eslint/parser": "^8.18.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
@@ -90,6 +99,7 @@
|
||||
"eslint-plugin-react-refresh": "^0.4.7",
|
||||
"eslint-plugin-tailwindcss": "^3.17.4",
|
||||
"globals": "^15.13.0",
|
||||
"happy-dom": "^18.0.1",
|
||||
"husky": "^9.1.5",
|
||||
"postcss": "^8.4.40",
|
||||
"prettier": "^3.3.3",
|
||||
@@ -97,6 +107,7 @@
|
||||
"tailwindcss": "^3.4.7",
|
||||
"typescript": "^5.2.2",
|
||||
"unplugin-inject-preload": "^3.0.0",
|
||||
"vite": "^5.3.4"
|
||||
"vite": "^5.3.4",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
src/assets/cloudflare_d1.png
Normal file
|
After Width: | Height: | Size: 937 B |
|
Before Width: | Height: | Size: 416 KiB After Width: | Height: | Size: 482 KiB |
|
Before Width: | Height: | Size: 391 KiB After Width: | Height: | Size: 434 KiB |
|
Before Width: | Height: | Size: 441 KiB After Width: | Height: | Size: 543 KiB |
|
Before Width: | Height: | Size: 405 KiB After Width: | Height: | Size: 488 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 404 KiB |
|
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 359 KiB |
BIN
src/assets/oracle_logo.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src/assets/oracle_logo_2.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src/assets/oracle_logo_dark.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
@@ -1,2 +1,3 @@
|
||||
import './config.ts';
|
||||
export { Editor } from '@monaco-editor/react';
|
||||
export { DiffEditor } from '@monaco-editor/react';
|
||||
|
||||
@@ -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,48 @@ 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;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface CodeSnippetProps {
|
||||
className?: string;
|
||||
code: string;
|
||||
codeToCopy?: string;
|
||||
language?: 'sql' | 'shell';
|
||||
loading?: boolean;
|
||||
autoScroll?: boolean;
|
||||
isComplete?: boolean;
|
||||
editorProps?: React.ComponentProps<EditorType>;
|
||||
actions?: CodeSnippetAction[];
|
||||
actionsTooltipSide?: 'top' | 'right' | 'bottom' | 'left';
|
||||
allowCopy?: boolean;
|
||||
}
|
||||
|
||||
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
({
|
||||
className,
|
||||
code,
|
||||
codeToCopy,
|
||||
loading,
|
||||
language = 'sql',
|
||||
autoScroll = false,
|
||||
isComplete = true,
|
||||
editorProps,
|
||||
actions,
|
||||
actionsTooltipSide,
|
||||
allowCopy = true,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const monaco = useMonaco();
|
||||
@@ -85,7 +107,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 +119,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
),
|
||||
});
|
||||
}
|
||||
}, [code, t, toast]);
|
||||
}, [code, codeToCopy, t, toast]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -111,36 +133,67 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
) : (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
{isComplete ? (
|
||||
<Tooltip
|
||||
onOpenChange={setTooltipOpen}
|
||||
open={isCopied || tooltipOpen}
|
||||
>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
className="absolute right-1 top-1 z-10"
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
className=" h-fit p-1.5"
|
||||
variant="outline"
|
||||
onClick={copyToClipboard}
|
||||
<div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
|
||||
{allowCopy ? (
|
||||
<Tooltip
|
||||
onOpenChange={setTooltipOpen}
|
||||
open={isCopied || tooltipOpen}
|
||||
>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
className="h-fit p-1.5"
|
||||
variant="outline"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{isCopied ? (
|
||||
<CopyCheck size={16} />
|
||||
) : (
|
||||
<Copy size={16} />
|
||||
)}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side={actionsTooltipSide}
|
||||
>
|
||||
{isCopied ? (
|
||||
<CopyCheck size={16} />
|
||||
) : (
|
||||
<Copy size={16} />
|
||||
{t(
|
||||
isCopied
|
||||
? 'copied'
|
||||
: 'copy_to_clipboard'
|
||||
)}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t(
|
||||
isCopied
|
||||
? 'copied'
|
||||
: 'copy_to_clipboard'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
|
||||
{actions &&
|
||||
actions.length > 0 &&
|
||||
actions.map((action, index) => (
|
||||
<Tooltip key={index}>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
className={cn(
|
||||
'h-fit p-1.5',
|
||||
action.className
|
||||
)}
|
||||
variant="outline"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
<action.icon
|
||||
size={16}
|
||||
/>
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side={actionsTooltipSide}
|
||||
>
|
||||
{action.label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Editor
|
||||
|
||||
51
src/components/code-snippet/dbml/utils.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { DBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
|
||||
import * as monaco from 'monaco-editor';
|
||||
|
||||
export const highlightErrorLine = ({
|
||||
error,
|
||||
model,
|
||||
editorDecorationsCollection,
|
||||
}: {
|
||||
error: DBMLError;
|
||||
model?: monaco.editor.ITextModel | null;
|
||||
editorDecorationsCollection:
|
||||
| monaco.editor.IEditorDecorationsCollection
|
||||
| undefined;
|
||||
}) => {
|
||||
if (!model) return;
|
||||
if (!editorDecorationsCollection) return;
|
||||
|
||||
const decorations = [
|
||||
{
|
||||
range: new monaco.Range(
|
||||
error.line,
|
||||
1,
|
||||
error.line,
|
||||
model.getLineMaxColumn(error.line)
|
||||
),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
className: 'dbml-error-line',
|
||||
glyphMarginClassName: 'dbml-error-glyph',
|
||||
hoverMessage: { value: error.message },
|
||||
overviewRuler: {
|
||||
color: '#ff0000',
|
||||
position: monaco.editor.OverviewRulerLane.Right,
|
||||
darkColor: '#ff0000',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
editorDecorationsCollection?.set(decorations);
|
||||
};
|
||||
|
||||
export const clearErrorHighlight = (
|
||||
editorDecorationsCollection:
|
||||
| monaco.editor.IEditorDecorationsCollection
|
||||
| undefined
|
||||
) => {
|
||||
if (editorDecorationsCollection) {
|
||||
editorDecorationsCollection.clear();
|
||||
}
|
||||
};
|
||||
@@ -37,18 +37,28 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
|
||||
const datatypePattern = dataTypesNames.join('|');
|
||||
|
||||
monaco.languages.setMonarchTokensProvider('dbml', {
|
||||
keywords: ['Table', 'Ref', 'Indexes'],
|
||||
keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum'],
|
||||
datatypes: dataTypesNames,
|
||||
tokenizer: {
|
||||
root: [
|
||||
[/\b(Table|Ref|Indexes)\b/, 'keyword'],
|
||||
[
|
||||
/\b([Tt][Aa][Bb][Ll][Ee]|[Ee][Nn][Uu][Mm]|[Rr][Ee][Ff]|[Ii][Nn][Dd][Ee][Xx][Ee][Ss]|[Nn][Oo][Tt][Ee])\b/,
|
||||
'keyword',
|
||||
],
|
||||
[/\[.*?\]/, 'annotation'],
|
||||
[/'''/, 'string', '@tripleQuoteString'],
|
||||
[/".*?"/, 'string'],
|
||||
[/'.*?'/, 'string'],
|
||||
[/`.*?`/, 'string'],
|
||||
[/[{}]/, 'delimiter'],
|
||||
[/[<>]/, 'operator'],
|
||||
[new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
|
||||
],
|
||||
tripleQuoteString: [
|
||||
[/[^']+/, 'string'],
|
||||
[/'''/, 'string', '@pop'],
|
||||
[/'/, 'string'],
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -52,7 +52,7 @@ export const EmptyState = forwardRef<
|
||||
</Label>
|
||||
<Label
|
||||
className={cn(
|
||||
'text-sm font-normal text-muted-foreground',
|
||||
'text-sm text-center font-normal text-muted-foreground',
|
||||
descriptionClassName
|
||||
)}
|
||||
>
|
||||
|
||||
121
src/components/pagination/pagination.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { ButtonProps } from '../button/button';
|
||||
import { buttonVariants } from '../button/button-variants';
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
DotsHorizontalIcon,
|
||||
} from '@radix-ui/react-icons';
|
||||
|
||||
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
className={cn('mx-auto flex w-full justify-center', className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
Pagination.displayName = 'Pagination';
|
||||
|
||||
const PaginationContent = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<'ul'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
className={cn('flex flex-row items-center gap-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
PaginationContent.displayName = 'PaginationContent';
|
||||
|
||||
const PaginationItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<'li'>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li ref={ref} className={cn('', className)} {...props} />
|
||||
));
|
||||
PaginationItem.displayName = 'PaginationItem';
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean;
|
||||
} & Pick<ButtonProps, 'size'> &
|
||||
React.ComponentProps<'a'>;
|
||||
|
||||
const PaginationLink = ({
|
||||
className,
|
||||
isActive,
|
||||
size = 'icon',
|
||||
...props
|
||||
}: PaginationLinkProps) => (
|
||||
<a
|
||||
aria-current={isActive ? 'page' : undefined}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? 'outline' : 'ghost',
|
||||
size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
PaginationLink.displayName = 'PaginationLink';
|
||||
|
||||
const PaginationPrevious = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn('gap-1 pl-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon className="size-4" />
|
||||
<span>Previous</span>
|
||||
</PaginationLink>
|
||||
);
|
||||
PaginationPrevious.displayName = 'PaginationPrevious';
|
||||
|
||||
const PaginationNext = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) => (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn('gap-1 pr-2.5', className)}
|
||||
{...props}
|
||||
>
|
||||
<span>Next</span>
|
||||
<ChevronRightIcon className="size-4" />
|
||||
</PaginationLink>
|
||||
);
|
||||
PaginationNext.displayName = 'PaginationNext';
|
||||
|
||||
const PaginationEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<'span'>) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn('flex h-9 w-9 items-center justify-center', className)}
|
||||
{...props}
|
||||
>
|
||||
<DotsHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
);
|
||||
PaginationEllipsis.displayName = 'PaginationEllipsis';
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
};
|
||||
@@ -26,6 +26,7 @@ export interface SelectBoxOption {
|
||||
description?: string;
|
||||
regex?: string;
|
||||
extractRegex?: RegExp;
|
||||
group?: string;
|
||||
}
|
||||
|
||||
export interface SelectBoxProps {
|
||||
@@ -51,6 +52,7 @@ export interface SelectBoxProps {
|
||||
disabled?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
popoverClassName?: string;
|
||||
}
|
||||
|
||||
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
@@ -75,6 +77,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
disabled,
|
||||
open,
|
||||
onOpenChange: setOpen,
|
||||
popoverClassName,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
@@ -90,6 +93,8 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
(isOpen: boolean) => {
|
||||
setOpen?.(isOpen);
|
||||
setIsOpen(isOpen);
|
||||
|
||||
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
|
||||
},
|
||||
[setOpen]
|
||||
);
|
||||
@@ -175,6 +180,101 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
[isOpen, onOpenChange]
|
||||
);
|
||||
|
||||
const groups = React.useMemo(
|
||||
() =>
|
||||
options.reduce(
|
||||
(acc, option) => {
|
||||
if (option.group) {
|
||||
if (!acc[option.group]) {
|
||||
acc[option.group] = [];
|
||||
}
|
||||
acc[option.group].push(option);
|
||||
} else {
|
||||
if (!acc['default']) {
|
||||
acc['default'] = [];
|
||||
}
|
||||
acc['default'].push(option);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, SelectBoxOption[]>
|
||||
),
|
||||
[options]
|
||||
);
|
||||
|
||||
const hasGroups = React.useMemo(
|
||||
() =>
|
||||
Object.keys(groups).filter((group) => group !== 'default')
|
||||
.length > 0,
|
||||
[groups]
|
||||
);
|
||||
|
||||
const renderOption = React.useCallback(
|
||||
(option: SelectBoxOption) => {
|
||||
const isSelected =
|
||||
Array.isArray(value) && value.includes(option.value);
|
||||
|
||||
const isRegexMatch =
|
||||
option.regex && new RegExp(option.regex)?.test(searchTerm);
|
||||
|
||||
const matches = option.extractRegex
|
||||
? searchTerm.match(option.extractRegex)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
className="flex items-center"
|
||||
key={option.value}
|
||||
keywords={option.regex ? [option.regex] : undefined}
|
||||
onSelect={() =>
|
||||
handleSelect(
|
||||
option.value,
|
||||
matches?.map((match) => match?.toString())
|
||||
)
|
||||
}
|
||||
>
|
||||
{multiple && (
|
||||
<div
|
||||
className={cn(
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
isSelected
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
)}
|
||||
>
|
||||
<CheckIcon />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-1 items-center truncate">
|
||||
<span>
|
||||
{isRegexMatch ? searchTerm : option.label}
|
||||
{!isRegexMatch && optionSuffix
|
||||
? optionSuffix(option)
|
||||
: ''}
|
||||
</span>
|
||||
{option.description && (
|
||||
<span className="ml-1 w-0 flex-1 truncate text-xs text-muted-foreground">
|
||||
{option.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{((!multiple && option.value === value) ||
|
||||
isRegexMatch) && (
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
option.value === value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
},
|
||||
[value, multiple, searchTerm, handleSelect, optionSuffix]
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
|
||||
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
|
||||
@@ -245,7 +345,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-fit min-w-[var(--radix-popover-trigger-width)] p-0"
|
||||
className={cn(
|
||||
'w-fit min-w-[var(--radix-popover-trigger-width)] p-0',
|
||||
popoverClassName
|
||||
)}
|
||||
align="center"
|
||||
>
|
||||
<Command
|
||||
@@ -317,96 +420,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
|
||||
<ScrollArea>
|
||||
<div className="max-h-64 w-full">
|
||||
<CommandGroup>
|
||||
<CommandList className="max-h-fit w-full">
|
||||
{options.map((option) => {
|
||||
const isSelected =
|
||||
Array.isArray(value) &&
|
||||
value.includes(option.value);
|
||||
|
||||
const isRegexMatch =
|
||||
option.regex &&
|
||||
new RegExp(option.regex)?.test(
|
||||
searchTerm
|
||||
);
|
||||
|
||||
const matches = option.extractRegex
|
||||
? searchTerm.match(
|
||||
option.extractRegex
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
className="flex items-center"
|
||||
key={option.value}
|
||||
keywords={
|
||||
option.regex
|
||||
? [option.regex]
|
||||
: undefined
|
||||
}
|
||||
// value={option.value}
|
||||
onSelect={() =>
|
||||
handleSelect(
|
||||
option.value,
|
||||
matches?.map(
|
||||
(match) =>
|
||||
match.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
{multiple && (
|
||||
<div
|
||||
className={cn(
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
isSelected
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'opacity-50 [&_svg]:invisible'
|
||||
)}
|
||||
>
|
||||
<CheckIcon />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center truncate">
|
||||
<span>
|
||||
{isRegexMatch
|
||||
? searchTerm
|
||||
: option.label}
|
||||
{!isRegexMatch &&
|
||||
optionSuffix
|
||||
? optionSuffix(
|
||||
option
|
||||
)
|
||||
: ''}
|
||||
</span>
|
||||
{option.description && (
|
||||
<span className="ml-1 text-xs text-muted-foreground">
|
||||
{
|
||||
option.description
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{((!multiple &&
|
||||
option.value ===
|
||||
value) ||
|
||||
isRegexMatch) && (
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
option.value ===
|
||||
value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandList>
|
||||
</CommandGroup>
|
||||
<CommandList className="max-h-fit w-full">
|
||||
{hasGroups
|
||||
? Object.entries(groups).map(
|
||||
([groupName, groupOptions]) => (
|
||||
<CommandGroup
|
||||
key={groupName}
|
||||
heading={groupName}
|
||||
>
|
||||
{groupOptions.map(
|
||||
renderOption
|
||||
)}
|
||||
</CommandGroup>
|
||||
)
|
||||
)
|
||||
: options.map(renderOption)}
|
||||
</CommandList>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</Command>
|
||||
|
||||
135
src/components/sheet/sheet.tsx
Normal 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,
|
||||
};
|
||||
790
src/components/sidebar/sidebar.tsx
Normal 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,
|
||||
};
|
||||
11
src/components/sidebar/use-sidebar.tsx
Normal 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;
|
||||
};
|
||||
16
src/components/skeleton/skeleton.tsx
Normal 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 };
|
||||
@@ -20,6 +20,7 @@ export function Toaster() {
|
||||
description,
|
||||
action,
|
||||
layout = 'row',
|
||||
hideCloseButton = false,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
@@ -38,7 +39,7 @@ export function Toaster() {
|
||||
) : null}
|
||||
</div>
|
||||
{layout === 'row' ? action : null}
|
||||
<ToastClose />
|
||||
{!hideCloseButton ? <ToastClose /> : null}
|
||||
</Toast>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -12,6 +12,7 @@ type ToasterToast = ToastProps & {
|
||||
description?: React.ReactNode;
|
||||
action?: ToastActionElement;
|
||||
layout?: 'row' | 'column';
|
||||
hideCloseButton?: boolean;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
17
src/components/tree-view/tree-item-skeleton.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { Skeleton } from '../skeleton/skeleton';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface TreeItemSkeletonProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
export const TreeItemSkeleton: React.FC<TreeItemSkeletonProps> = ({
|
||||
className,
|
||||
style,
|
||||
}) => {
|
||||
return (
|
||||
<div className={cn('px-2 py-1', className)} style={style}>
|
||||
<Skeleton className="h-3.5 w-full rounded-sm" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
461
src/components/tree-view/tree-view.tsx
Normal file
@@ -0,0 +1,461 @@
|
||||
import {
|
||||
ChevronRight,
|
||||
File,
|
||||
Folder,
|
||||
Loader2,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Button } from '@/components/button/button';
|
||||
import type {
|
||||
TreeNode,
|
||||
FetchChildrenFunction,
|
||||
SelectableTreeProps,
|
||||
} from './tree';
|
||||
import type { ExpandedState } from './use-tree';
|
||||
import { useTree } from './use-tree';
|
||||
import type { Dispatch, ReactNode, SetStateAction } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { TreeItemSkeleton } from './tree-item-skeleton';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/tooltip/tooltip';
|
||||
|
||||
interface TreeViewProps<
|
||||
Type extends string,
|
||||
Context extends Record<Type, unknown>,
|
||||
> {
|
||||
data: TreeNode<Type, Context>[];
|
||||
fetchChildren?: FetchChildrenFunction<Type, Context>;
|
||||
onNodeClick?: (node: TreeNode<Type, Context>) => void;
|
||||
className?: string;
|
||||
defaultIcon?: LucideIcon;
|
||||
defaultFolderIcon?: LucideIcon;
|
||||
defaultIconProps?: React.ComponentProps<LucideIcon>;
|
||||
defaultFolderIconProps?: React.ComponentProps<LucideIcon>;
|
||||
selectable?: SelectableTreeProps<Type, Context>;
|
||||
expanded?: ExpandedState;
|
||||
setExpanded?: Dispatch<SetStateAction<ExpandedState>>;
|
||||
renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode;
|
||||
renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode;
|
||||
loadingNodeIds?: string[];
|
||||
}
|
||||
|
||||
export function TreeView<
|
||||
Type extends string,
|
||||
Context extends Record<Type, unknown>,
|
||||
>({
|
||||
data,
|
||||
fetchChildren,
|
||||
onNodeClick,
|
||||
className,
|
||||
defaultIcon = File,
|
||||
defaultFolderIcon = Folder,
|
||||
defaultIconProps,
|
||||
defaultFolderIconProps,
|
||||
selectable,
|
||||
expanded: expandedProp,
|
||||
setExpanded: setExpandedProp,
|
||||
renderHoverComponent,
|
||||
renderActionsComponent,
|
||||
loadingNodeIds,
|
||||
}: TreeViewProps<Type, Context>) {
|
||||
const { expanded, loading, loadedChildren, hasMoreChildren, toggleNode } =
|
||||
useTree({
|
||||
fetchChildren,
|
||||
expanded: expandedProp,
|
||||
setExpanded: setExpandedProp,
|
||||
});
|
||||
const [selectedIdInternal, setSelectedIdInternal] = React.useState<
|
||||
string | undefined
|
||||
>(selectable?.defaultSelectedId);
|
||||
|
||||
const selectedId = useMemo(() => {
|
||||
return selectable?.selectedId ?? selectedIdInternal;
|
||||
}, [selectable?.selectedId, selectedIdInternal]);
|
||||
|
||||
const setSelectedId = useCallback(
|
||||
(value: SetStateAction<string | undefined>) => {
|
||||
if (selectable?.setSelectedId) {
|
||||
selectable.setSelectedId(value);
|
||||
} else {
|
||||
setSelectedIdInternal(value);
|
||||
}
|
||||
},
|
||||
[selectable, setSelectedIdInternal]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectable?.enabled && selectable.defaultSelectedId) {
|
||||
if (selectable.defaultSelectedId === selectedId) return;
|
||||
setSelectedId(selectable.defaultSelectedId);
|
||||
const { node, path } = findNodeById(
|
||||
data,
|
||||
selectable.defaultSelectedId
|
||||
);
|
||||
|
||||
if (node) {
|
||||
selectable.onSelectedChange?.(node);
|
||||
|
||||
// Expand all parent nodes
|
||||
for (const parent of path) {
|
||||
if (expanded[parent.id]) continue;
|
||||
toggleNode(
|
||||
parent.id,
|
||||
parent.type,
|
||||
parent.context,
|
||||
parent.children
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [selectable, toggleNode, selectedId, data, expanded, setSelectedId]);
|
||||
|
||||
const handleNodeSelect = (node: TreeNode<Type, Context>) => {
|
||||
if (selectable?.enabled) {
|
||||
setSelectedId(node.id);
|
||||
selectable.onSelectedChange?.(node);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('w-full', className)}>
|
||||
{data.map((node, index) => (
|
||||
<TreeNode
|
||||
key={node.id}
|
||||
node={node}
|
||||
level={0}
|
||||
expanded={expanded}
|
||||
loading={loading}
|
||||
loadedChildren={loadedChildren}
|
||||
hasMoreChildren={hasMoreChildren}
|
||||
onToggle={toggleNode}
|
||||
onNodeClick={onNodeClick}
|
||||
defaultIcon={defaultIcon}
|
||||
defaultFolderIcon={defaultFolderIcon}
|
||||
defaultIconProps={defaultIconProps}
|
||||
defaultFolderIconProps={defaultFolderIconProps}
|
||||
selectable={selectable?.enabled}
|
||||
selectedId={selectedId}
|
||||
onSelect={handleNodeSelect}
|
||||
className={index > 0 ? 'mt-0.5' : ''}
|
||||
renderHoverComponent={renderHoverComponent}
|
||||
renderActionsComponent={renderActionsComponent}
|
||||
loadingNodeIds={loadingNodeIds}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TreeNodeProps<
|
||||
Type extends string,
|
||||
Context extends Record<Type, unknown>,
|
||||
> {
|
||||
node: TreeNode<Type, Context>;
|
||||
level: number;
|
||||
expanded: Record<string, boolean>;
|
||||
loading: Record<string, boolean>;
|
||||
loadedChildren: Record<string, TreeNode<Type, Context>[]>;
|
||||
hasMoreChildren: Record<string, boolean>;
|
||||
onToggle: (
|
||||
nodeId: string,
|
||||
nodeType: Type,
|
||||
nodeContext: Context[Type],
|
||||
staticChildren?: TreeNode<Type, Context>[]
|
||||
) => void;
|
||||
onNodeClick?: (node: TreeNode<Type, Context>) => void;
|
||||
defaultIcon: LucideIcon;
|
||||
defaultFolderIcon: LucideIcon;
|
||||
defaultIconProps?: React.ComponentProps<LucideIcon>;
|
||||
defaultFolderIconProps?: React.ComponentProps<LucideIcon>;
|
||||
selectable?: boolean;
|
||||
selectedId?: string;
|
||||
onSelect: (node: TreeNode<Type, Context>) => void;
|
||||
className?: string;
|
||||
renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode;
|
||||
renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode;
|
||||
loadingNodeIds?: string[];
|
||||
}
|
||||
|
||||
function TreeNode<Type extends string, Context extends Record<Type, unknown>>({
|
||||
node,
|
||||
level,
|
||||
expanded,
|
||||
loading,
|
||||
loadedChildren,
|
||||
hasMoreChildren,
|
||||
onToggle,
|
||||
onNodeClick,
|
||||
defaultIcon: DefaultIcon,
|
||||
defaultFolderIcon: DefaultFolderIcon,
|
||||
defaultIconProps,
|
||||
defaultFolderIconProps,
|
||||
selectable,
|
||||
selectedId,
|
||||
onSelect,
|
||||
className,
|
||||
renderHoverComponent,
|
||||
renderActionsComponent,
|
||||
loadingNodeIds,
|
||||
}: TreeNodeProps<Type, Context>) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const isExpanded = expanded[node.id];
|
||||
const isLoading = loading[node.id];
|
||||
const children = loadedChildren[node.id] || node.children;
|
||||
const isSelected = selectedId === node.id;
|
||||
|
||||
const IconComponent =
|
||||
node.icon || (node.isFolder ? DefaultFolderIcon : DefaultIcon);
|
||||
const iconProps: React.ComponentProps<LucideIcon> = {
|
||||
strokeWidth: isSelected ? 2.5 : 2,
|
||||
...(node.isFolder ? defaultFolderIconProps : defaultIconProps),
|
||||
...node.iconProps,
|
||||
className: cn(
|
||||
'h-3.5 w-3.5 text-muted-foreground flex-none',
|
||||
isSelected && 'text-primary text-white',
|
||||
node.iconProps?.className
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 px-2 py-1 rounded-lg cursor-pointer group h-6',
|
||||
'transition-colors duration-200',
|
||||
isSelected
|
||||
? 'bg-sky-500 border border-sky-600 border dark:bg-sky-600 dark:border-sky-700'
|
||||
: 'hover:bg-gray-200/50 border border-transparent dark:hover:bg-gray-700/50',
|
||||
node.className
|
||||
)}
|
||||
{...(isSelected ? { 'data-selected': true } : {})}
|
||||
style={{ paddingLeft: `${level * 16 + 8}px` }}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (selectable && !node.unselectable) {
|
||||
onSelect(node);
|
||||
}
|
||||
// if (node.isFolder) {
|
||||
// onToggle(node.id, node.children);
|
||||
// }
|
||||
|
||||
// called only once in case of double click
|
||||
if (e.detail !== 2) {
|
||||
onNodeClick?.(node);
|
||||
}
|
||||
}}
|
||||
onDoubleClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (node.isFolder) {
|
||||
onToggle(
|
||||
node.id,
|
||||
node.type,
|
||||
node.context,
|
||||
node.children
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-none items-center gap-1.5">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn(
|
||||
'h-3.5 w-3.5 p-0 hover:bg-transparent flex-none',
|
||||
isExpanded && 'rotate-90',
|
||||
'transition-transform duration-200'
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (node.isFolder) {
|
||||
onToggle(
|
||||
node.id,
|
||||
node.type,
|
||||
node.context,
|
||||
node.children
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{node.isFolder &&
|
||||
(isLoading ? (
|
||||
<Loader2
|
||||
className={cn('size-3.5 animate-spin', {
|
||||
'text-white': isSelected,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<ChevronRight
|
||||
className={cn('size-3.5', {
|
||||
'text-white': isSelected,
|
||||
})}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
))}
|
||||
</Button>
|
||||
|
||||
{node.tooltip ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{loadingNodeIds?.includes(node.id) ? (
|
||||
<Loader2
|
||||
className={cn('size-3.5 animate-spin', {
|
||||
'text-white': isSelected,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<IconComponent
|
||||
{...(isSelected
|
||||
? { 'data-selected': true }
|
||||
: {})}
|
||||
{...iconProps}
|
||||
/>
|
||||
)}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
align="center"
|
||||
className="max-w-[400px]"
|
||||
>
|
||||
{node.tooltip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : node.empty ? null : loadingNodeIds?.includes(
|
||||
node.id
|
||||
) ? (
|
||||
<Loader2
|
||||
className={cn('size-3.5 animate-spin', {
|
||||
// 'text-white': isSelected,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<IconComponent
|
||||
{...(isSelected ? { 'data-selected': true } : {})}
|
||||
{...iconProps}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<span
|
||||
{...node.labelProps}
|
||||
className={cn(
|
||||
'text-xs truncate min-w-0 flex-1 w-0',
|
||||
isSelected && 'font-medium text-primary text-white',
|
||||
node.labelProps?.className
|
||||
)}
|
||||
{...(isSelected ? { 'data-selected': true } : {})}
|
||||
>
|
||||
{node.empty ? '' : node.name}
|
||||
</span>
|
||||
{renderActionsComponent && renderActionsComponent(node)}
|
||||
{isHovered && renderHoverComponent
|
||||
? renderHoverComponent(node)
|
||||
: null}
|
||||
</div>
|
||||
|
||||
<AnimatePresence initial={false}>
|
||||
{isExpanded && children && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{
|
||||
height: 'auto',
|
||||
opacity: 1,
|
||||
transition: {
|
||||
height: {
|
||||
duration: Math.min(
|
||||
0.3 + children.length * 0.018,
|
||||
0.7
|
||||
),
|
||||
ease: 'easeInOut',
|
||||
},
|
||||
opacity: {
|
||||
duration: Math.min(
|
||||
0.2 + children.length * 0.012,
|
||||
0.4
|
||||
),
|
||||
ease: 'easeInOut',
|
||||
},
|
||||
},
|
||||
}}
|
||||
exit={{
|
||||
height: 0,
|
||||
opacity: 0,
|
||||
transition: {
|
||||
height: {
|
||||
duration: Math.min(
|
||||
0.2 + children.length * 0.01,
|
||||
0.45
|
||||
),
|
||||
ease: 'easeInOut',
|
||||
},
|
||||
opacity: {
|
||||
duration: 0.1,
|
||||
ease: 'easeOut',
|
||||
},
|
||||
},
|
||||
}}
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
{children.map((child) => (
|
||||
<TreeNode
|
||||
key={child.id}
|
||||
node={child}
|
||||
level={level + 1}
|
||||
expanded={expanded}
|
||||
loading={loading}
|
||||
loadedChildren={loadedChildren}
|
||||
hasMoreChildren={hasMoreChildren}
|
||||
onToggle={onToggle}
|
||||
onNodeClick={onNodeClick}
|
||||
defaultIcon={DefaultIcon}
|
||||
defaultFolderIcon={DefaultFolderIcon}
|
||||
defaultIconProps={defaultIconProps}
|
||||
defaultFolderIconProps={defaultFolderIconProps}
|
||||
selectable={selectable}
|
||||
selectedId={selectedId}
|
||||
onSelect={onSelect}
|
||||
className="mt-0.5"
|
||||
renderHoverComponent={renderHoverComponent}
|
||||
renderActionsComponent={renderActionsComponent}
|
||||
loadingNodeIds={loadingNodeIds}
|
||||
/>
|
||||
))}
|
||||
{isLoading ? (
|
||||
<TreeItemSkeleton
|
||||
style={{
|
||||
paddingLeft: `${level + 2 * 16 + 8}px`,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function findNodeById<
|
||||
Type extends string,
|
||||
Context extends Record<Type, unknown>,
|
||||
>(
|
||||
nodes: TreeNode<Type, Context>[],
|
||||
id: string,
|
||||
initialPath: TreeNode<Type, Context>[] = []
|
||||
): { node: TreeNode<Type, Context> | null; path: TreeNode<Type, Context>[] } {
|
||||
const path: TreeNode<Type, Context>[] = [...initialPath];
|
||||
for (const node of nodes) {
|
||||
if (node.id === id) return { node, path };
|
||||
if (node.children) {
|
||||
const found = findNodeById(node.children, id, [...path, node]);
|
||||
if (found.node) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { node: null, path };
|
||||
}
|
||||
41
src/components/tree-view/tree.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import type React from 'react';
|
||||
|
||||
export interface TreeNode<
|
||||
Type extends string,
|
||||
Context extends Record<Type, unknown>,
|
||||
> {
|
||||
id: string;
|
||||
name: string;
|
||||
isFolder?: boolean;
|
||||
children?: TreeNode<Type, Context>[];
|
||||
icon?: LucideIcon;
|
||||
iconProps?: React.ComponentProps<LucideIcon>;
|
||||
labelProps?: React.ComponentProps<'span'>;
|
||||
type: Type;
|
||||
unselectable?: boolean;
|
||||
tooltip?: string;
|
||||
context: Context[Type];
|
||||
empty?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export type FetchChildrenFunction<
|
||||
Type extends string,
|
||||
Context extends Record<Type, unknown>,
|
||||
> = (
|
||||
nodeId: string,
|
||||
nodeType: Type,
|
||||
nodeContext: Context[Type]
|
||||
) => Promise<TreeNode<Type, Context>[]>;
|
||||
|
||||
export interface SelectableTreeProps<
|
||||
Type extends string,
|
||||
Context extends Record<Type, unknown>,
|
||||
> {
|
||||
enabled: boolean;
|
||||
defaultSelectedId?: string;
|
||||
onSelectedChange?: (node: TreeNode<Type, Context>) => void;
|
||||
selectedId?: string;
|
||||
setSelectedId?: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
}
|
||||
153
src/components/tree-view/use-tree.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import type { TreeNode, FetchChildrenFunction } from './tree';
|
||||
|
||||
export interface ExpandedState {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
|
||||
interface LoadingState {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
|
||||
interface LoadedChildren<
|
||||
Type extends string,
|
||||
Context extends Record<Type, unknown>,
|
||||
> {
|
||||
[key: string]: TreeNode<Type, Context>[];
|
||||
}
|
||||
|
||||
interface HasMoreChildrenState {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
|
||||
export function useTree<
|
||||
Type extends string,
|
||||
Context extends Record<Type, unknown>,
|
||||
>({
|
||||
fetchChildren,
|
||||
expanded: expandedProp,
|
||||
setExpanded: setExpandedProp,
|
||||
}: {
|
||||
fetchChildren?: FetchChildrenFunction<Type, Context>;
|
||||
expanded?: ExpandedState;
|
||||
setExpanded?: Dispatch<SetStateAction<ExpandedState>>;
|
||||
}) {
|
||||
const [expandedInternal, setExpandedInternal] = useState<ExpandedState>({});
|
||||
|
||||
const expanded = useMemo(
|
||||
() => expandedProp ?? expandedInternal,
|
||||
[expandedProp, expandedInternal]
|
||||
);
|
||||
const setExpanded = useCallback(
|
||||
(value: SetStateAction<ExpandedState>) => {
|
||||
if (setExpandedProp) {
|
||||
setExpandedProp(value);
|
||||
} else {
|
||||
setExpandedInternal(value);
|
||||
}
|
||||
},
|
||||
[setExpandedProp, setExpandedInternal]
|
||||
);
|
||||
|
||||
const [loading, setLoading] = useState<LoadingState>({});
|
||||
const [loadedChildren, setLoadedChildren] = useState<
|
||||
LoadedChildren<Type, Context>
|
||||
>({});
|
||||
const [hasMoreChildren, setHasMoreChildren] =
|
||||
useState<HasMoreChildrenState>({});
|
||||
|
||||
const mergeChildren = useCallback(
|
||||
(
|
||||
staticChildren: TreeNode<Type, Context>[] = [],
|
||||
fetchedChildren: TreeNode<Type, Context>[] = []
|
||||
) => {
|
||||
const fetchedChildrenIds = new Set(
|
||||
fetchedChildren.map((child) => child.id)
|
||||
);
|
||||
const uniqueStaticChildren = staticChildren.filter(
|
||||
(child) => !fetchedChildrenIds.has(child.id)
|
||||
);
|
||||
return [...uniqueStaticChildren, ...fetchedChildren];
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const toggleNode = useCallback(
|
||||
async (
|
||||
nodeId: string,
|
||||
nodeType: Type,
|
||||
nodeContext: Context[Type],
|
||||
staticChildren?: TreeNode<Type, Context>[]
|
||||
) => {
|
||||
if (expanded[nodeId]) {
|
||||
// If we're collapsing, just update expanded state
|
||||
setExpanded((prev) => ({ ...prev, [nodeId]: false }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get any previously fetched children
|
||||
const previouslyFetchedChildren = loadedChildren[nodeId] || [];
|
||||
|
||||
// If we have static children, merge them with any previously fetched children
|
||||
if (staticChildren?.length) {
|
||||
const mergedChildren = mergeChildren(
|
||||
staticChildren,
|
||||
previouslyFetchedChildren
|
||||
);
|
||||
setLoadedChildren((prev) => ({
|
||||
...prev,
|
||||
[nodeId]: mergedChildren,
|
||||
}));
|
||||
|
||||
// Only show "more loading" if we haven't fetched children before
|
||||
setHasMoreChildren((prev) => ({
|
||||
...prev,
|
||||
[nodeId]: !previouslyFetchedChildren.length,
|
||||
}));
|
||||
}
|
||||
|
||||
// Set expanded state immediately to show static/previously fetched children
|
||||
setExpanded((prev) => ({ ...prev, [nodeId]: true }));
|
||||
|
||||
// If we haven't loaded dynamic children yet
|
||||
if (!previouslyFetchedChildren.length) {
|
||||
setLoading((prev) => ({ ...prev, [nodeId]: true }));
|
||||
try {
|
||||
const fetchedChildren = await fetchChildren?.(
|
||||
nodeId,
|
||||
nodeType,
|
||||
nodeContext
|
||||
);
|
||||
// Merge static and newly fetched children
|
||||
const allChildren = mergeChildren(
|
||||
staticChildren || [],
|
||||
fetchedChildren
|
||||
);
|
||||
|
||||
setLoadedChildren((prev) => ({
|
||||
...prev,
|
||||
[nodeId]: allChildren,
|
||||
}));
|
||||
setHasMoreChildren((prev) => ({
|
||||
...prev,
|
||||
[nodeId]: false,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('Error loading children:', error);
|
||||
} finally {
|
||||
setLoading((prev) => ({ ...prev, [nodeId]: false }));
|
||||
}
|
||||
}
|
||||
},
|
||||
[expanded, loadedChildren, fetchChildren, mergeChildren, setExpanded]
|
||||
);
|
||||
|
||||
return {
|
||||
expanded,
|
||||
loading,
|
||||
loadedChildren,
|
||||
hasMoreChildren,
|
||||
toggleNode,
|
||||
};
|
||||
}
|
||||
@@ -12,6 +12,8 @@ export interface CanvasContext {
|
||||
}) => void;
|
||||
setOverlapGraph: (graph: Graph<string>) => void;
|
||||
overlapGraph: Graph<string>;
|
||||
setShowFilter: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
showFilter: boolean;
|
||||
}
|
||||
|
||||
export const canvasContext = createContext<CanvasContext>({
|
||||
@@ -19,4 +21,6 @@ export const canvasContext = createContext<CanvasContext>({
|
||||
fitView: emptyFn,
|
||||
setOverlapGraph: emptyFn,
|
||||
overlapGraph: createGraph(),
|
||||
setShowFilter: emptyFn,
|
||||
showFilter: false,
|
||||
});
|
||||
|
||||
@@ -21,6 +21,8 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
||||
const [overlapGraph, setOverlapGraph] =
|
||||
useState<Graph<string>>(createGraph());
|
||||
|
||||
const [showFilter, setShowFilter] = useState(false);
|
||||
|
||||
const reorderTables = useCallback(
|
||||
(
|
||||
options: { updateHistory?: boolean } = {
|
||||
@@ -77,6 +79,8 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
||||
fitView,
|
||||
setOverlapGraph,
|
||||
overlapGraph,
|
||||
setShowFilter,
|
||||
showFilter,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -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,10 +72,15 @@ export interface ChartDBContext {
|
||||
schemas: DBSchema[];
|
||||
relationships: DBRelationship[];
|
||||
dependencies: DBDependency[];
|
||||
areas: Area[];
|
||||
customTypes: DBCustomType[];
|
||||
currentDiagram: Diagram;
|
||||
events: EventEmitter<ChartDBEvent>;
|
||||
readonly?: boolean;
|
||||
|
||||
highlightedCustomType?: DBCustomType;
|
||||
highlightCustomTypeId: (id?: string) => void;
|
||||
|
||||
filteredSchemas?: string[];
|
||||
filterSchemas: (schemaIds: string[]) => void;
|
||||
|
||||
@@ -88,6 +95,10 @@ export interface ChartDBContext {
|
||||
updateDiagramUpdatedAt: () => Promise<void>;
|
||||
clearDiagramData: () => Promise<void>;
|
||||
deleteDiagram: () => Promise<void>;
|
||||
updateDiagramData: (
|
||||
diagram: Diagram,
|
||||
options?: { forceUpdateStorage?: boolean }
|
||||
) => Promise<void>;
|
||||
|
||||
// Database type operations
|
||||
updateDatabaseType: (databaseType: DatabaseType) => Promise<void>;
|
||||
@@ -221,6 +232,63 @@ 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>;
|
||||
|
||||
// Filters
|
||||
hiddenTableIds?: string[];
|
||||
addHiddenTableId: (tableId: string) => Promise<void>;
|
||||
removeHiddenTableId: (tableId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const chartDBContext = createContext<ChartDBContext>({
|
||||
@@ -230,7 +298,10 @@ export const chartDBContext = createContext<ChartDBContext>({
|
||||
tables: [],
|
||||
relationships: [],
|
||||
dependencies: [],
|
||||
areas: [],
|
||||
customTypes: [],
|
||||
schemas: [],
|
||||
highlightCustomTypeId: emptyFn,
|
||||
filteredSchemas: [],
|
||||
filterSchemas: emptyFn,
|
||||
currentDiagram: {
|
||||
@@ -250,6 +321,7 @@ export const chartDBContext = createContext<ChartDBContext>({
|
||||
loadDiagramFromData: emptyFn,
|
||||
clearDiagramData: emptyFn,
|
||||
deleteDiagram: emptyFn,
|
||||
updateDiagramData: emptyFn,
|
||||
|
||||
// Database type operations
|
||||
updateDatabaseType: emptyFn,
|
||||
@@ -296,4 +368,27 @@ 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,
|
||||
|
||||
// Filters
|
||||
hiddenTableIds: [],
|
||||
addHiddenTableId: emptyFn,
|
||||
removeHiddenTableId: emptyFn,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import { deepCopy, generateId } from '@/lib/utils';
|
||||
import { randomColor } from '@/lib/colors';
|
||||
@@ -21,9 +21,15 @@ 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';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
|
||||
export interface ChartDBProviderProps {
|
||||
diagram?: Diagram;
|
||||
@@ -34,11 +40,17 @@ export const ChartDBProvider: React.FC<
|
||||
React.PropsWithChildren<ChartDBProviderProps>
|
||||
> = ({ children, diagram, readonly: readonlyProp }) => {
|
||||
const { hasDiff } = useDiff();
|
||||
let db = useStorage();
|
||||
const dbStorage = useStorage();
|
||||
let db = dbStorage;
|
||||
const events = useEventEmitter<ChartDBEvent>();
|
||||
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
||||
const { addUndoAction, resetRedoStack, resetUndoStack } =
|
||||
useRedoUndoStack();
|
||||
const {
|
||||
getHiddenTablesForDiagram,
|
||||
hideTableForDiagram,
|
||||
unhideTableForDiagram,
|
||||
} = useConfig();
|
||||
const [diagramId, setDiagramId] = useState('');
|
||||
const [diagramName, setDiagramName] = useState('');
|
||||
const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
|
||||
@@ -56,8 +68,16 @@ export const ChartDBProvider: React.FC<
|
||||
const [dependencies, setDependencies] = useState<DBDependency[]>(
|
||||
diagram?.dependencies ?? []
|
||||
);
|
||||
const [areas, setAreas] = useState<Area[]>(diagram?.areas ?? []);
|
||||
const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
|
||||
diagram?.customTypes ?? []
|
||||
);
|
||||
const [hiddenTableIds, setHiddenTableIds] = useState<string[]>([]);
|
||||
const { events: diffEvents } = useDiff();
|
||||
|
||||
const [highlightedCustomTypeId, setHighlightedCustomTypeId] =
|
||||
useState<string>();
|
||||
|
||||
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
|
||||
const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
|
||||
setTables((tables) =>
|
||||
@@ -76,6 +96,14 @@ export const ChartDBProvider: React.FC<
|
||||
|
||||
diffEvents.useSubscription(diffCalculatedHandler);
|
||||
|
||||
// Sync hiddenTableIds with config
|
||||
useEffect(() => {
|
||||
if (diagramId) {
|
||||
const hiddenTables = getHiddenTablesForDiagram(diagramId);
|
||||
setHiddenTableIds(hiddenTables);
|
||||
}
|
||||
}, [diagramId, getHiddenTablesForDiagram]);
|
||||
|
||||
const defaultSchemaName = defaultSchemas[databaseType];
|
||||
|
||||
const readonly = useMemo(
|
||||
@@ -152,6 +180,8 @@ export const ChartDBProvider: React.FC<
|
||||
tables,
|
||||
relationships,
|
||||
dependencies,
|
||||
areas,
|
||||
customTypes,
|
||||
}),
|
||||
[
|
||||
diagramId,
|
||||
@@ -161,6 +191,8 @@ export const ChartDBProvider: React.FC<
|
||||
tables,
|
||||
relationships,
|
||||
dependencies,
|
||||
areas,
|
||||
customTypes,
|
||||
diagramCreatedAt,
|
||||
diagramUpdatedAt,
|
||||
]
|
||||
@@ -172,6 +204,8 @@ export const ChartDBProvider: React.FC<
|
||||
setTables([]);
|
||||
setRelationships([]);
|
||||
setDependencies([]);
|
||||
setAreas([]);
|
||||
setCustomTypes([]);
|
||||
setDiagramUpdatedAt(updatedAt);
|
||||
|
||||
resetRedoStack();
|
||||
@@ -182,6 +216,8 @@ export const ChartDBProvider: React.FC<
|
||||
db.deleteDiagramTables(diagramId),
|
||||
db.deleteDiagramRelationships(diagramId),
|
||||
db.deleteDiagramDependencies(diagramId),
|
||||
db.deleteDiagramAreas(diagramId),
|
||||
db.deleteDiagramCustomTypes(diagramId),
|
||||
]);
|
||||
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
||||
|
||||
@@ -194,6 +230,8 @@ export const ChartDBProvider: React.FC<
|
||||
setTables([]);
|
||||
setRelationships([]);
|
||||
setDependencies([]);
|
||||
setAreas([]);
|
||||
setCustomTypes([]);
|
||||
resetRedoStack();
|
||||
resetUndoStack();
|
||||
|
||||
@@ -202,6 +240,8 @@ export const ChartDBProvider: React.FC<
|
||||
db.deleteDiagramRelationships(diagramId),
|
||||
db.deleteDiagram(diagramId),
|
||||
db.deleteDiagramDependencies(diagramId),
|
||||
db.deleteDiagramAreas(diagramId),
|
||||
db.deleteDiagramCustomTypes(diagramId),
|
||||
]);
|
||||
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
||||
|
||||
@@ -283,22 +323,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();
|
||||
}
|
||||
@@ -757,13 +802,23 @@ export const ChartDBProvider: React.FC<
|
||||
options = { updateHistory: true }
|
||||
) => {
|
||||
const fields = getTable(tableId)?.fields ?? [];
|
||||
setTables((tables) =>
|
||||
tables.map((table) =>
|
||||
table.id === tableId
|
||||
? { ...table, fields: [...table.fields, field] }
|
||||
: table
|
||||
)
|
||||
);
|
||||
setTables((tables) => {
|
||||
return tables.map((table) => {
|
||||
if (table.id === tableId) {
|
||||
db.updateTable({
|
||||
id: tableId,
|
||||
attributes: {
|
||||
...table,
|
||||
fields: [...table.fields, field],
|
||||
},
|
||||
});
|
||||
|
||||
return { ...table, fields: [...table.fields, field] };
|
||||
}
|
||||
|
||||
return table;
|
||||
});
|
||||
});
|
||||
|
||||
events.emit({
|
||||
action: 'add_field',
|
||||
@@ -784,13 +839,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) {
|
||||
@@ -1363,20 +1411,161 @@ export const ChartDBProvider: React.FC<
|
||||
]
|
||||
);
|
||||
|
||||
// Area operations
|
||||
const addAreas: ChartDBContext['addAreas'] = useCallback(
|
||||
async (areas: Area[], options = { updateHistory: true }) => {
|
||||
setAreas((currentAreas) => [...currentAreas, ...areas]);
|
||||
|
||||
const updatedAt = new Date();
|
||||
setDiagramUpdatedAt(updatedAt);
|
||||
|
||||
await Promise.all([
|
||||
...areas.map((area) => db.addArea({ diagramId, area })),
|
||||
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||
]);
|
||||
|
||||
if (options.updateHistory) {
|
||||
addUndoAction({
|
||||
action: 'addAreas',
|
||||
redoData: { areas },
|
||||
undoData: { areaIds: areas.map((a) => a.id) },
|
||||
});
|
||||
resetRedoStack();
|
||||
}
|
||||
},
|
||||
[db, diagramId, setAreas, addUndoAction, resetRedoStack]
|
||||
);
|
||||
|
||||
const addArea: ChartDBContext['addArea'] = useCallback(
|
||||
async (area: Area, options = { updateHistory: true }) => {
|
||||
return addAreas([area], options);
|
||||
},
|
||||
[addAreas]
|
||||
);
|
||||
|
||||
const createArea: ChartDBContext['createArea'] = useCallback(
|
||||
async (attributes) => {
|
||||
const area: Area = {
|
||||
id: generateId(),
|
||||
name: `Area ${areas.length + 1}`,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 300,
|
||||
height: 200,
|
||||
color: 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 highlightCustomTypeId = useCallback(
|
||||
(id?: string) => setHighlightedCustomTypeId(id),
|
||||
[setHighlightedCustomTypeId]
|
||||
);
|
||||
|
||||
const highlightedCustomType = useMemo(() => {
|
||||
return highlightedCustomTypeId
|
||||
? customTypes.find((type) => type.id === highlightedCustomTypeId)
|
||||
: undefined;
|
||||
}, [highlightedCustomTypeId, customTypes]);
|
||||
|
||||
const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
|
||||
useCallback(
|
||||
async (diagram) => {
|
||||
(diagram) => {
|
||||
setDiagramId(diagram.id);
|
||||
setDiagramName(diagram.name);
|
||||
setDatabaseType(diagram.databaseType);
|
||||
setDatabaseEdition(diagram.databaseEdition);
|
||||
setTables(diagram?.tables ?? []);
|
||||
setRelationships(diagram?.relationships ?? []);
|
||||
setDependencies(diagram?.dependencies ?? []);
|
||||
setTables(diagram.tables ?? []);
|
||||
setRelationships(diagram.relationships ?? []);
|
||||
setDependencies(diagram.dependencies ?? []);
|
||||
setAreas(diagram.areas ?? []);
|
||||
setCustomTypes(diagram.customTypes ?? []);
|
||||
setDiagramCreatedAt(diagram.createdAt);
|
||||
setDiagramUpdatedAt(diagram.updatedAt);
|
||||
setHighlightedCustomTypeId(undefined);
|
||||
|
||||
events.emit({ action: 'load_diagram', data: { diagram } });
|
||||
|
||||
resetRedoStack();
|
||||
resetUndoStack();
|
||||
},
|
||||
[
|
||||
setDiagramId,
|
||||
@@ -1386,18 +1575,35 @@ export const ChartDBProvider: React.FC<
|
||||
setTables,
|
||||
setRelationships,
|
||||
setDependencies,
|
||||
setAreas,
|
||||
setCustomTypes,
|
||||
setDiagramCreatedAt,
|
||||
setDiagramUpdatedAt,
|
||||
setHighlightedCustomTypeId,
|
||||
events,
|
||||
resetRedoStack,
|
||||
resetUndoStack,
|
||||
]
|
||||
);
|
||||
|
||||
const updateDiagramData: ChartDBContext['updateDiagramData'] = useCallback(
|
||||
async (diagram, options) => {
|
||||
const st = options?.forceUpdateStorage ? dbStorage : db;
|
||||
await st.deleteDiagram(diagram.id);
|
||||
await st.addDiagram({ diagram });
|
||||
loadDiagramFromData(diagram);
|
||||
},
|
||||
[db, dbStorage, loadDiagramFromData]
|
||||
);
|
||||
|
||||
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
|
||||
async (diagramId: string) => {
|
||||
const diagram = await db.getDiagram(diagramId, {
|
||||
includeRelationships: true,
|
||||
includeTables: true,
|
||||
includeDependencies: true,
|
||||
includeAreas: true,
|
||||
includeCustomTypes: true,
|
||||
});
|
||||
|
||||
if (diagram) {
|
||||
@@ -1409,6 +1615,173 @@ 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,
|
||||
]
|
||||
);
|
||||
|
||||
const addHiddenTableId: ChartDBContext['addHiddenTableId'] = useCallback(
|
||||
async (tableId: string) => {
|
||||
if (!hiddenTableIds.includes(tableId)) {
|
||||
setHiddenTableIds((prev) => [...prev, tableId]);
|
||||
await hideTableForDiagram(diagramId, tableId);
|
||||
}
|
||||
},
|
||||
[hiddenTableIds, diagramId, hideTableForDiagram]
|
||||
);
|
||||
|
||||
const removeHiddenTableId: ChartDBContext['removeHiddenTableId'] =
|
||||
useCallback(
|
||||
async (tableId: string) => {
|
||||
if (hiddenTableIds.includes(tableId)) {
|
||||
setHiddenTableIds((prev) =>
|
||||
prev.filter((id) => id !== tableId)
|
||||
);
|
||||
await unhideTableForDiagram(diagramId, tableId);
|
||||
}
|
||||
},
|
||||
[hiddenTableIds, diagramId, unhideTableForDiagram]
|
||||
);
|
||||
|
||||
return (
|
||||
<chartDBContext.Provider
|
||||
value={{
|
||||
@@ -1418,12 +1791,14 @@ export const ChartDBProvider: React.FC<
|
||||
tables,
|
||||
relationships,
|
||||
dependencies,
|
||||
areas,
|
||||
currentDiagram,
|
||||
schemas,
|
||||
filteredSchemas,
|
||||
events,
|
||||
readonly,
|
||||
filterSchemas,
|
||||
updateDiagramData,
|
||||
updateDiagramId,
|
||||
updateDiagramName,
|
||||
loadDiagram,
|
||||
@@ -1465,6 +1840,26 @@ export const ChartDBProvider: React.FC<
|
||||
removeDependency,
|
||||
removeDependencies,
|
||||
updateDependency,
|
||||
createArea,
|
||||
addArea,
|
||||
addAreas,
|
||||
getArea,
|
||||
removeArea,
|
||||
removeAreas,
|
||||
updateArea,
|
||||
customTypes,
|
||||
createCustomType,
|
||||
addCustomType,
|
||||
addCustomTypes,
|
||||
getCustomType,
|
||||
removeCustomType,
|
||||
removeCustomTypes,
|
||||
updateCustomType,
|
||||
hiddenTableIds,
|
||||
addHiddenTableId,
|
||||
removeHiddenTableId,
|
||||
highlightCustomTypeId,
|
||||
highlightedCustomType,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -4,10 +4,27 @@ 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>;
|
||||
getHiddenTablesForDiagram: (diagramId: string) => string[];
|
||||
setHiddenTablesForDiagram: (
|
||||
diagramId: string,
|
||||
hiddenTableIds: string[]
|
||||
) => Promise<void>;
|
||||
hideTableForDiagram: (diagramId: string, tableId: string) => Promise<void>;
|
||||
unhideTableForDiagram: (
|
||||
diagramId: string,
|
||||
tableId: string
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export const ConfigContext = createContext<ConfigContext>({
|
||||
config: undefined,
|
||||
updateConfig: emptyFn,
|
||||
getHiddenTablesForDiagram: () => [],
|
||||
setHiddenTablesForDiagram: emptyFn,
|
||||
hideTableForDiagram: emptyFn,
|
||||
unhideTableForDiagram: emptyFn,
|
||||
});
|
||||
|
||||
@@ -19,19 +19,111 @@ 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;
|
||||
};
|
||||
|
||||
const getHiddenTablesForDiagram = (diagramId: string): string[] => {
|
||||
return config?.hiddenTablesByDiagram?.[diagramId] ?? [];
|
||||
};
|
||||
|
||||
const setHiddenTablesForDiagram = async (
|
||||
diagramId: string,
|
||||
hiddenTableIds: string[]
|
||||
): Promise<void> => {
|
||||
return updateConfig({
|
||||
updateFn: (currentConfig) => ({
|
||||
...currentConfig,
|
||||
hiddenTablesByDiagram: {
|
||||
...currentConfig.hiddenTablesByDiagram,
|
||||
[diagramId]: hiddenTableIds,
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const hideTableForDiagram = async (
|
||||
diagramId: string,
|
||||
tableId: string
|
||||
): Promise<void> => {
|
||||
return updateConfig({
|
||||
updateFn: (currentConfig) => {
|
||||
const currentHiddenTables =
|
||||
currentConfig.hiddenTablesByDiagram?.[diagramId] ?? [];
|
||||
if (currentHiddenTables.includes(tableId)) {
|
||||
return currentConfig; // Already hidden, no change needed
|
||||
}
|
||||
|
||||
return {
|
||||
...currentConfig,
|
||||
hiddenTablesByDiagram: {
|
||||
...currentConfig.hiddenTablesByDiagram,
|
||||
[diagramId]: [...currentHiddenTables, tableId],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const unhideTableForDiagram = async (
|
||||
diagramId: string,
|
||||
tableId: string
|
||||
): Promise<void> => {
|
||||
return updateConfig({
|
||||
updateFn: (currentConfig) => {
|
||||
const currentHiddenTables =
|
||||
currentConfig.hiddenTablesByDiagram?.[diagramId] ?? [];
|
||||
const filteredTables = currentHiddenTables.filter(
|
||||
(id) => id !== tableId
|
||||
);
|
||||
|
||||
if (filteredTables.length === currentHiddenTables.length) {
|
||||
return currentConfig; // Not hidden, no change needed
|
||||
}
|
||||
|
||||
return {
|
||||
...currentConfig,
|
||||
hiddenTablesByDiagram: {
|
||||
...currentConfig.hiddenTablesByDiagram,
|
||||
[diagramId]: filteredTables,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider value={{ config, updateConfig }}>
|
||||
<ConfigContext.Provider
|
||||
value={{
|
||||
config,
|
||||
updateConfig,
|
||||
getHiddenTablesForDiagram,
|
||||
setHiddenTablesForDiagram,
|
||||
hideTableForDiagram,
|
||||
unhideTableForDiagram,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ConfigContext.Provider>
|
||||
);
|
||||
|
||||
@@ -9,10 +9,13 @@ import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/i
|
||||
import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
|
||||
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
||||
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
|
||||
import type { CreateDiagramDialogProps } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
|
||||
|
||||
export interface DialogContext {
|
||||
// Create diagram dialog
|
||||
openCreateDiagramDialog: () => void;
|
||||
openCreateDiagramDialog: (
|
||||
params?: Omit<CreateDiagramDialogProps, 'dialog'>
|
||||
) => void;
|
||||
closeCreateDiagramDialog: () => void;
|
||||
|
||||
// Open diagram dialog
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import type { DialogContext } from './dialog-context';
|
||||
import { dialogContext } from './dialog-context';
|
||||
import type { CreateDiagramDialogProps } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
|
||||
import { CreateDiagramDialog } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
|
||||
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
|
||||
import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
|
||||
@@ -26,6 +27,17 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false);
|
||||
const [newDiagramDialogParams, setNewDiagramDialogParams] =
|
||||
useState<Omit<CreateDiagramDialogProps, 'dialog'>>();
|
||||
const openNewDiagramDialogHandler: DialogContext['openCreateDiagramDialog'] =
|
||||
useCallback(
|
||||
(props) => {
|
||||
setNewDiagramDialogParams(props);
|
||||
setOpenNewDiagramDialog(true);
|
||||
},
|
||||
[setOpenNewDiagramDialog]
|
||||
);
|
||||
|
||||
const [openOpenDiagramDialog, setOpenOpenDiagramDialog] = useState(false);
|
||||
const [openDiagramDialogParams, setOpenDiagramDialogParams] =
|
||||
useState<Omit<OpenDiagramDialogProps, 'dialog'>>();
|
||||
@@ -128,7 +140,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
return (
|
||||
<dialogContext.Provider
|
||||
value={{
|
||||
openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
|
||||
openCreateDiagramDialog: openNewDiagramDialogHandler,
|
||||
closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
|
||||
openOpenDiagramDialog: openOpenDiagramDialogHandler,
|
||||
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
|
||||
@@ -161,7 +173,10 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} />
|
||||
<CreateDiagramDialog
|
||||
dialog={{ open: openNewDiagramDialog }}
|
||||
{...newDiagramDialogParams}
|
||||
/>
|
||||
<OpenDiagramDialog
|
||||
dialog={{ open: openOpenDiagramDialog }}
|
||||
{...openDiagramDialogParams}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { createContext } from 'react';
|
||||
import type { DiffMap } from './types';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { EventEmitter } from 'ahooks/lib/useEventEmitter';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||
import type { DiffMap } from '@/lib/domain/diff/diff';
|
||||
|
||||
export type DiffEventType = 'diff_calculated';
|
||||
|
||||
@@ -14,35 +14,45 @@ export type DiffEventBase<T extends DiffEventType, D> = {
|
||||
data: D;
|
||||
};
|
||||
|
||||
export type DiffCalculatedData = {
|
||||
tablesAdded: DBTable[];
|
||||
fieldsAdded: Map<string, DBField[]>;
|
||||
relationshipsAdded: DBRelationship[];
|
||||
};
|
||||
|
||||
export type DiffCalculatedEvent = DiffEventBase<
|
||||
'diff_calculated',
|
||||
{
|
||||
tablesAdded: DBTable[];
|
||||
fieldsAdded: Map<string, DBField[]>;
|
||||
relationshipsAdded: DBRelationship[];
|
||||
}
|
||||
DiffCalculatedData
|
||||
>;
|
||||
|
||||
export type DiffEvent = DiffCalculatedEvent;
|
||||
|
||||
export interface DiffContext {
|
||||
newDiagram: Diagram | null;
|
||||
originalDiagram: Diagram | null;
|
||||
diffMap: DiffMap;
|
||||
hasDiff: boolean;
|
||||
isSummaryOnly: boolean;
|
||||
|
||||
calculateDiff: ({
|
||||
diagram,
|
||||
newDiagram,
|
||||
options,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
newDiagram: Diagram;
|
||||
options?: {
|
||||
summaryOnly?: boolean;
|
||||
};
|
||||
}) => void;
|
||||
resetDiff: () => void;
|
||||
|
||||
// table diff
|
||||
checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
|
||||
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: ({
|
||||
@@ -56,6 +66,15 @@ export interface DiffContext {
|
||||
checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
|
||||
getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
|
||||
getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
|
||||
getFieldNewPrimaryKey: ({ fieldId }: { fieldId: string }) => boolean | null;
|
||||
getFieldNewNullable: ({ fieldId }: { fieldId: string }) => boolean | null;
|
||||
getFieldNewCharacterMaximumLength: ({
|
||||
fieldId,
|
||||
}: {
|
||||
fieldId: string;
|
||||
}) => string | null;
|
||||
getFieldNewScale: ({ fieldId }: { fieldId: string }) => number | null;
|
||||
getFieldNewPrecision: ({ fieldId }: { fieldId: string }) => number | null;
|
||||
|
||||
// relationship diff
|
||||
checkIfNewRelationship: ({
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import type { DiffContext, DiffEvent } from './diff-context';
|
||||
import type {
|
||||
DiffCalculatedData,
|
||||
DiffContext,
|
||||
DiffEvent,
|
||||
} from './diff-context';
|
||||
import { diffContext } from './diff-context';
|
||||
import type { ChartDBDiff, DiffMap } from './types';
|
||||
import { generateDiff, getDiffMapKey } from './diff-check/diff-check';
|
||||
|
||||
import {
|
||||
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>()
|
||||
);
|
||||
@@ -22,6 +32,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
const [fieldsChanged, setFieldsChanged] = React.useState<
|
||||
Map<string, boolean>
|
||||
>(new Map<string, boolean>());
|
||||
const [isSummaryOnly, setIsSummaryOnly] = React.useState<boolean>(false);
|
||||
|
||||
const events = useEventEmitter<DiffEvent>();
|
||||
|
||||
@@ -39,7 +50,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
if (diff.object === 'field' && diff.type === 'added') {
|
||||
const field = newDiagram?.tables
|
||||
?.find((table) => table.id === diff.tableId)
|
||||
?.fields.find((f) => f.id === diff.fieldId);
|
||||
?.fields.find((f) => f.id === diff.newField.id);
|
||||
|
||||
if (field) {
|
||||
newFieldsMap.set(diff.tableId, [
|
||||
@@ -67,7 +78,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
diffMap.forEach((diff) => {
|
||||
if (diff.object === 'relationship' && diff.type === 'added') {
|
||||
const relationship = newDiagram?.relationships?.find(
|
||||
(rel) => rel.id === diff.relationshipId
|
||||
(rel) => rel.id === diff.newRelationship.id
|
||||
);
|
||||
|
||||
if (relationship) {
|
||||
@@ -81,8 +92,43 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
[]
|
||||
);
|
||||
|
||||
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 }) => {
|
||||
({ diagram, newDiagram: newDiagramArg, options }) => {
|
||||
const {
|
||||
diffMap: newDiffs,
|
||||
changedTables: newChangedTables,
|
||||
@@ -93,35 +139,18 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setTablesChanged(newChangedTables);
|
||||
setFieldsChanged(newChangedFields);
|
||||
setNewDiagram(newDiagramArg);
|
||||
setOriginalDiagram(diagram);
|
||||
setIsSummaryOnly(options?.summaryOnly ?? false);
|
||||
|
||||
events.emit({
|
||||
action: 'diff_calculated',
|
||||
data: {
|
||||
tablesAdded:
|
||||
newDiagramArg?.tables?.filter((table) => {
|
||||
const tableKey = getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: table.id,
|
||||
});
|
||||
|
||||
return (
|
||||
newDiffs.has(tableKey) &&
|
||||
newDiffs.get(tableKey)?.type === 'added'
|
||||
);
|
||||
}) ?? [],
|
||||
|
||||
fieldsAdded: generateNewFieldsMap({
|
||||
diffMap: newDiffs,
|
||||
newDiagram: newDiagramArg,
|
||||
}),
|
||||
relationshipsAdded: findNewRelationships({
|
||||
diffMap: newDiffs,
|
||||
newDiagram: newDiagramArg,
|
||||
}),
|
||||
},
|
||||
data: generateDiffCalculatedData({
|
||||
diffMap: newDiffs,
|
||||
newDiagram: newDiagramArg,
|
||||
}),
|
||||
});
|
||||
},
|
||||
[setDiffMap, events, generateNewFieldsMap, findNewRelationships]
|
||||
[setDiffMap, events, generateDiffCalculatedData]
|
||||
);
|
||||
|
||||
const getTableNewName = useCallback<DiffContext['getTableNewName']>(
|
||||
@@ -145,6 +174,26 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
[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]);
|
||||
@@ -258,6 +307,117 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getFieldNewPrimaryKey = useCallback<
|
||||
DiffContext['getFieldNewPrimaryKey']
|
||||
>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
attribute: 'primaryKey',
|
||||
});
|
||||
|
||||
if (diffMap.has(fieldKey)) {
|
||||
const diff = diffMap.get(fieldKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as boolean;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getFieldNewNullable = useCallback<DiffContext['getFieldNewNullable']>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
attribute: 'nullable',
|
||||
});
|
||||
|
||||
if (diffMap.has(fieldKey)) {
|
||||
const diff = diffMap.get(fieldKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as boolean;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getFieldNewCharacterMaximumLength = useCallback<
|
||||
DiffContext['getFieldNewCharacterMaximumLength']
|
||||
>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
attribute: 'characterMaximumLength',
|
||||
});
|
||||
|
||||
if (diffMap.has(fieldKey)) {
|
||||
const diff = diffMap.get(fieldKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as string;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getFieldNewScale = useCallback<DiffContext['getFieldNewScale']>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
attribute: 'scale',
|
||||
});
|
||||
|
||||
if (diffMap.has(fieldKey)) {
|
||||
const diff = diffMap.get(fieldKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as number;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getFieldNewPrecision = useCallback<
|
||||
DiffContext['getFieldNewPrecision']
|
||||
>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
attribute: 'precision',
|
||||
});
|
||||
|
||||
if (diffMap.has(fieldKey)) {
|
||||
const diff = diffMap.get(fieldKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as number;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfNewRelationship = useCallback<
|
||||
DiffContext['checkIfNewRelationship']
|
||||
>(
|
||||
@@ -292,20 +452,33 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const resetDiff = useCallback<DiffContext['resetDiff']>(() => {
|
||||
setDiffMap(new Map<string, ChartDBDiff>());
|
||||
setTablesChanged(new Map<string, boolean>());
|
||||
setFieldsChanged(new Map<string, boolean>());
|
||||
setNewDiagram(null);
|
||||
setOriginalDiagram(null);
|
||||
setIsSummaryOnly(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<diffContext.Provider
|
||||
value={{
|
||||
newDiagram,
|
||||
originalDiagram,
|
||||
diffMap,
|
||||
hasDiff: diffMap.size > 0,
|
||||
isSummaryOnly,
|
||||
|
||||
calculateDiff,
|
||||
resetDiff,
|
||||
|
||||
// table diff
|
||||
getTableNewName,
|
||||
checkIfNewTable,
|
||||
checkIfTableRemoved,
|
||||
checkIfTableHasChange,
|
||||
getTableNewColor,
|
||||
|
||||
// field diff
|
||||
checkIfFieldHasChange,
|
||||
@@ -313,6 +486,11 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
checkIfNewField,
|
||||
getFieldNewName,
|
||||
getFieldNewType,
|
||||
getFieldNewPrimaryKey,
|
||||
getFieldNewNullable,
|
||||
getFieldNewCharacterMaximumLength,
|
||||
getFieldNewScale,
|
||||
getFieldNewPrecision,
|
||||
|
||||
// relationship diff
|
||||
checkIfNewRelationship,
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||
|
||||
export type TableDiffAttribute = 'name' | 'comments';
|
||||
|
||||
export interface TableDiff {
|
||||
object: 'table';
|
||||
type: 'added' | 'removed' | 'changed';
|
||||
tableId: string;
|
||||
attributes?: TableDiffAttribute;
|
||||
oldValue?: string;
|
||||
newValue?: string;
|
||||
}
|
||||
|
||||
export interface RelationshipDiff {
|
||||
object: 'relationship';
|
||||
type: 'added' | 'removed';
|
||||
relationshipId: string;
|
||||
}
|
||||
|
||||
export type FieldDiffAttribute =
|
||||
| 'name'
|
||||
| 'type'
|
||||
| 'primaryKey'
|
||||
| 'unique'
|
||||
| 'nullable'
|
||||
| 'comments';
|
||||
|
||||
export interface FieldDiff {
|
||||
object: 'field';
|
||||
type: 'added' | 'removed' | 'changed';
|
||||
fieldId: string;
|
||||
tableId: string;
|
||||
attributes?: FieldDiffAttribute;
|
||||
oldValue?: string | boolean | DataType;
|
||||
newValue?: string | boolean | DataType;
|
||||
}
|
||||
|
||||
export interface IndexDiff {
|
||||
object: 'index';
|
||||
type: 'added' | 'removed';
|
||||
indexId: string;
|
||||
tableId: string;
|
||||
}
|
||||
|
||||
export type ChartDBDiff = TableDiff | FieldDiff | IndexDiff | RelationshipDiff;
|
||||
|
||||
export type DiffMap = Map<string, ChartDBDiff>;
|
||||
|
||||
export type DiffObject =
|
||||
| TableDiff['object']
|
||||
| FieldDiff['object']
|
||||
| IndexDiff['object']
|
||||
| RelationshipDiff['object'];
|
||||
@@ -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>({
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
import logoDark from '@/assets/logo-dark.png';
|
||||
import logoLight from '@/assets/logo-light.png';
|
||||
import type { EffectiveTheme } from '../theme-context/theme-context';
|
||||
|
||||
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
@@ -57,8 +58,16 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
[]
|
||||
);
|
||||
|
||||
const getBackgroundColor = useCallback(
|
||||
(theme: EffectiveTheme, transparent: boolean): string => {
|
||||
if (transparent) return 'transparent';
|
||||
return theme === 'light' ? '#ffffff' : '#141414';
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const exportImage: ExportImageContext['exportImage'] = useCallback(
|
||||
async (type, scale = 1) => {
|
||||
async (type, { includePatternBG, transparent, scale }) => {
|
||||
showLoader({
|
||||
animated: false,
|
||||
});
|
||||
@@ -114,34 +123,37 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
defs.innerHTML = markerDefs.innerHTML;
|
||||
}
|
||||
|
||||
const pattern = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'pattern'
|
||||
);
|
||||
pattern.setAttribute('id', 'background-pattern');
|
||||
pattern.setAttribute('width', String(16 * viewport.zoom));
|
||||
pattern.setAttribute('height', String(16 * viewport.zoom));
|
||||
pattern.setAttribute('patternUnits', 'userSpaceOnUse');
|
||||
pattern.setAttribute(
|
||||
'patternTransform',
|
||||
`translate(${viewport.x % (16 * viewport.zoom)} ${viewport.y % (16 * viewport.zoom)})`
|
||||
);
|
||||
if (includePatternBG) {
|
||||
const pattern = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'pattern'
|
||||
);
|
||||
pattern.setAttribute('id', 'background-pattern');
|
||||
pattern.setAttribute('width', String(16 * viewport.zoom));
|
||||
pattern.setAttribute('height', String(16 * viewport.zoom));
|
||||
pattern.setAttribute('patternUnits', 'userSpaceOnUse');
|
||||
pattern.setAttribute(
|
||||
'patternTransform',
|
||||
`translate(${viewport.x % (16 * viewport.zoom)} ${viewport.y % (16 * viewport.zoom)})`
|
||||
);
|
||||
|
||||
const dot = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'circle'
|
||||
);
|
||||
const dot = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'circle'
|
||||
);
|
||||
|
||||
const dotSize = viewport.zoom * 0.5;
|
||||
dot.setAttribute('cx', String(viewport.zoom));
|
||||
dot.setAttribute('cy', String(viewport.zoom));
|
||||
dot.setAttribute('r', String(dotSize));
|
||||
const dotColor =
|
||||
effectiveTheme === 'light' ? '#92939C' : '#777777';
|
||||
dot.setAttribute('fill', dotColor);
|
||||
const dotSize = viewport.zoom * 0.5;
|
||||
dot.setAttribute('cx', String(viewport.zoom));
|
||||
dot.setAttribute('cy', String(viewport.zoom));
|
||||
dot.setAttribute('r', String(dotSize));
|
||||
const dotColor =
|
||||
effectiveTheme === 'light' ? '#92939C' : '#777777';
|
||||
dot.setAttribute('fill', dotColor);
|
||||
|
||||
pattern.appendChild(dot);
|
||||
defs.appendChild(pattern);
|
||||
}
|
||||
|
||||
pattern.appendChild(dot);
|
||||
defs.appendChild(pattern);
|
||||
tempSvg.appendChild(defs);
|
||||
|
||||
const backgroundRect = document.createElementNS(
|
||||
@@ -196,10 +208,10 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
const initialDataUrl = await imageCreateFn(
|
||||
viewportElement,
|
||||
{
|
||||
backgroundColor:
|
||||
effectiveTheme === 'light'
|
||||
? '#ffffff'
|
||||
: '#141414',
|
||||
backgroundColor: getBackgroundColor(
|
||||
effectiveTheme,
|
||||
transparent
|
||||
),
|
||||
width: reactFlowBounds.width,
|
||||
height: reactFlowBounds.height,
|
||||
style: {
|
||||
@@ -285,6 +297,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
}, 0);
|
||||
},
|
||||
[
|
||||
getBackgroundColor,
|
||||
downloadImage,
|
||||
getViewport,
|
||||
hideLoader,
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -8,6 +8,7 @@ export enum KeyboardShortcutAction {
|
||||
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
|
||||
SHOW_ALL = 'show_all',
|
||||
TOGGLE_THEME = 'toggle_theme',
|
||||
TOGGLE_FILTER = 'toggle_filter',
|
||||
}
|
||||
|
||||
export interface KeyboardShortcut {
|
||||
@@ -71,6 +72,13 @@ export const keyboardShortcuts: Record<
|
||||
keyCombinationMac: 'meta+m',
|
||||
keyCombinationWin: 'ctrl+m',
|
||||
},
|
||||
[KeyboardShortcutAction.TOGGLE_FILTER]: {
|
||||
action: KeyboardShortcutAction.TOGGLE_FILTER,
|
||||
keyCombinationLabelMac: '⌘F',
|
||||
keyCombinationLabelWin: 'Ctrl+F',
|
||||
keyCombinationMac: 'meta+f',
|
||||
keyCombinationWin: 'ctrl+f',
|
||||
},
|
||||
};
|
||||
|
||||
export interface KeyboardShortcutForOS {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -19,6 +19,9 @@ export interface LocalConfigContext {
|
||||
showCardinality: boolean;
|
||||
setShowCardinality: (showCardinality: boolean) => void;
|
||||
|
||||
showFieldAttributes: boolean;
|
||||
setShowFieldAttributes: (showFieldAttributes: boolean) => void;
|
||||
|
||||
hideMultiSchemaNotification: boolean;
|
||||
setHideMultiSchemaNotification: (
|
||||
hideMultiSchemaNotification: boolean
|
||||
@@ -50,6 +53,9 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
||||
showCardinality: true,
|
||||
setShowCardinality: emptyFn,
|
||||
|
||||
showFieldAttributes: true,
|
||||
setShowFieldAttributes: emptyFn,
|
||||
|
||||
hideMultiSchemaNotification: false,
|
||||
setHideMultiSchemaNotification: emptyFn,
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const themeKey = 'theme';
|
||||
const scrollActionKey = 'scroll_action';
|
||||
const schemasFilterKey = 'schemas_filter';
|
||||
const showCardinalityKey = 'show_cardinality';
|
||||
const showFieldAttributesKey = 'show_field_attributes';
|
||||
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
|
||||
const githubRepoOpenedKey = 'github_repo_opened';
|
||||
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
||||
@@ -34,6 +35,11 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
|
||||
);
|
||||
|
||||
const [showFieldAttributes, setShowFieldAttributes] =
|
||||
React.useState<boolean>(
|
||||
(localStorage.getItem(showFieldAttributesKey) || 'true') === 'true'
|
||||
);
|
||||
|
||||
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
|
||||
React.useState<boolean>(
|
||||
(localStorage.getItem(hideMultiSchemaNotificationKey) ||
|
||||
@@ -119,6 +125,8 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setSchemasFilter,
|
||||
showCardinality,
|
||||
setShowCardinality,
|
||||
showFieldAttributes,
|
||||
setShowFieldAttributes,
|
||||
hideMultiSchemaNotification,
|
||||
setHideMultiSchemaNotification,
|
||||
setGithubRepoOpened,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -48,6 +48,7 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
||||
handleThemeToggle,
|
||||
{
|
||||
preventDefault: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[handleThemeToggle]
|
||||
);
|
||||
|
||||
@@ -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,78 @@ 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, IDisposable } from 'monaco-editor';
|
||||
import { waitFor } from '@/lib/utils';
|
||||
import {
|
||||
validateSQL,
|
||||
type ValidationResult,
|
||||
} from '@/lib/data/sql-import/sql-validator';
|
||||
import { SQLValidationStatus } from './sql-validation-status';
|
||||
|
||||
const calculateContentSizeMB = (content: string): number => {
|
||||
return content.length / (1024 * 1024); // Convert to MB
|
||||
};
|
||||
|
||||
const calculateIsLargeFile = (content: string): boolean => {
|
||||
const contentSizeMB = calculateContentSizeMB(content);
|
||||
return contentSizeMB > 2; // Consider large if over 2MB
|
||||
};
|
||||
|
||||
const errorScriptOutputMessage =
|
||||
'Invalid JSON. Please correct it or contact us at chartdb.io@gmail.com for help.';
|
||||
'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.';
|
||||
|
||||
// Helper to detect if content is likely SQL DDL or JSON
|
||||
const detectContentType = (content: string): 'query' | 'ddl' | null => {
|
||||
if (!content || content.trim().length === 0) return null;
|
||||
|
||||
// Common SQL DDL keywords
|
||||
const ddlKeywords = [
|
||||
'CREATE TABLE',
|
||||
'ALTER TABLE',
|
||||
'DROP TABLE',
|
||||
'CREATE INDEX',
|
||||
'CREATE VIEW',
|
||||
'CREATE PROCEDURE',
|
||||
'CREATE FUNCTION',
|
||||
'CREATE SCHEMA',
|
||||
'CREATE DATABASE',
|
||||
];
|
||||
|
||||
const upperContent = content.toUpperCase();
|
||||
|
||||
// Check for SQL DDL patterns
|
||||
const hasDDLKeywords = ddlKeywords.some((keyword) =>
|
||||
upperContent.includes(keyword)
|
||||
);
|
||||
if (hasDDLKeywords) return 'ddl';
|
||||
|
||||
// Check if it looks like JSON
|
||||
try {
|
||||
// Just check structure, don't need full parse for detection
|
||||
if (
|
||||
(content.trim().startsWith('{') && content.trim().endsWith('}')) ||
|
||||
(content.trim().startsWith('[') && content.trim().endsWith(']'))
|
||||
) {
|
||||
return 'query';
|
||||
}
|
||||
} catch (error) {
|
||||
// Not valid JSON, might be partial
|
||||
console.error('Error detecting content type:', error);
|
||||
}
|
||||
|
||||
// If we can't confidently detect, return null
|
||||
return null;
|
||||
};
|
||||
|
||||
export interface ImportDatabaseProps {
|
||||
goBack?: () => void;
|
||||
@@ -57,6 +111,8 @@ export interface ImportDatabaseProps {
|
||||
>;
|
||||
keepDialogAfterImport?: boolean;
|
||||
title: string;
|
||||
importMethod: 'query' | 'ddl';
|
||||
setImportMethod: (method: 'query' | 'ddl') => void;
|
||||
}
|
||||
|
||||
export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
@@ -70,34 +126,80 @@ 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 pasteDisposableRef = useRef<IDisposable | 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);
|
||||
const [sqlValidation, setSqlValidation] = useState<ValidationResult | null>(
|
||||
null
|
||||
);
|
||||
const [isAutoFixing, setIsAutoFixing] = useState(false);
|
||||
const [showAutoFixButton, setShowAutoFixButton] = 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') {
|
||||
setSqlValidation(null);
|
||||
setShowAutoFixButton(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scriptResult.trim()) {
|
||||
setSqlValidation(null);
|
||||
setShowAutoFixButton(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// First run our validation based on database type
|
||||
const validation = validateSQL(scriptResult, databaseType);
|
||||
setSqlValidation(validation);
|
||||
|
||||
// If we have auto-fixable errors, show the auto-fix button
|
||||
if (validation.fixedSQL && validation.errors.length > 0) {
|
||||
setShowAutoFixButton(true);
|
||||
// Don't try to parse invalid SQL
|
||||
setErrorMessage('SQL contains syntax errors');
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide auto-fix button if no fixes available
|
||||
setShowAutoFixButton(false);
|
||||
|
||||
// Validate the SQL (either original or already fixed)
|
||||
parseSQLError({
|
||||
sqlContent: scriptResult,
|
||||
sourceDatabaseType: databaseType,
|
||||
}).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 +219,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 +227,150 @@ 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 handleAutoFix = useCallback(() => {
|
||||
if (sqlValidation?.fixedSQL) {
|
||||
setIsAutoFixing(true);
|
||||
setShowAutoFixButton(false);
|
||||
setErrorMessage('');
|
||||
|
||||
// Apply the fix with a delay so user sees the fixing message
|
||||
setTimeout(() => {
|
||||
setScriptResult(sqlValidation.fixedSQL!);
|
||||
|
||||
setTimeout(() => {
|
||||
setIsAutoFixing(false);
|
||||
}, 100);
|
||||
}, 1000);
|
||||
}
|
||||
}, [sqlValidation, setScriptResult]);
|
||||
|
||||
const handleErrorClick = useCallback((line: number) => {
|
||||
if (editorRef.current) {
|
||||
// Set cursor to the error line
|
||||
editorRef.current.setPosition({ lineNumber: line, column: 1 });
|
||||
editorRef.current.revealLineInCenter(line);
|
||||
editorRef.current.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const formatEditor = useCallback(() => {
|
||||
if (editorRef.current) {
|
||||
const model = editorRef.current.getModel();
|
||||
if (model) {
|
||||
const content = model.getValue();
|
||||
|
||||
// Skip formatting for large files (> 2MB)
|
||||
if (calculateIsLargeFile(content)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
editorRef.current
|
||||
?.getAction('editor.action.formatDocument')
|
||||
?.run();
|
||||
}, 50);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleInputChange: OnChange = useCallback(
|
||||
(inputValue) => {
|
||||
setScriptResult(inputValue ?? '');
|
||||
|
||||
// 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]);
|
||||
|
||||
useEffect(() => {
|
||||
// Cleanup paste handler on unmount
|
||||
return () => {
|
||||
if (pasteDisposableRef.current) {
|
||||
pasteDisposableRef.current.dispose();
|
||||
pasteDisposableRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleEditorDidMount = useCallback(
|
||||
(editor: editor.IStandaloneCodeEditor) => {
|
||||
editorRef.current = editor;
|
||||
|
||||
// Cleanup previous disposable if it exists
|
||||
if (pasteDisposableRef.current) {
|
||||
pasteDisposableRef.current.dispose();
|
||||
pasteDisposableRef.current = null;
|
||||
}
|
||||
|
||||
// Add paste handler for all modes
|
||||
const disposable = editor.onDidPaste(() => {
|
||||
const model = editor.getModel();
|
||||
if (!model) return;
|
||||
|
||||
const content = model.getValue();
|
||||
|
||||
// Skip formatting for large files (> 2MB) to prevent browser freezing
|
||||
const isLargeFile = calculateIsLargeFile(content);
|
||||
|
||||
// First, detect content type to determine if we should switch modes
|
||||
const detectedType = detectContentType(content);
|
||||
if (detectedType && detectedType !== importMethod) {
|
||||
// Switch to the detected mode immediately
|
||||
setImportMethod(detectedType);
|
||||
|
||||
// Only format if it's JSON (query mode) AND file is not too large
|
||||
if (detectedType === 'query' && !isLargeFile) {
|
||||
// For JSON mode, format after a short delay
|
||||
setTimeout(() => {
|
||||
editor
|
||||
.getAction('editor.action.formatDocument')
|
||||
?.run();
|
||||
}, 100);
|
||||
}
|
||||
// For DDL mode, do NOT format as it can break the SQL
|
||||
} else {
|
||||
// Content type didn't change, apply formatting based on current mode
|
||||
if (importMethod === 'query' && !isLargeFile) {
|
||||
// Only format JSON content if not too large
|
||||
setTimeout(() => {
|
||||
editor
|
||||
.getAction('editor.action.formatDocument')
|
||||
?.run();
|
||||
}, 100);
|
||||
}
|
||||
// For DDL mode or large files, do NOT format
|
||||
}
|
||||
});
|
||||
|
||||
pasteDisposableRef.current = disposable;
|
||||
},
|
||||
[importMethod, setImportMethod]
|
||||
);
|
||||
|
||||
const renderHeader = useCallback(() => {
|
||||
return (
|
||||
@@ -164,228 +381,137 @@ 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: false, // Never format on paste - we handle it manually
|
||||
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 || (importMethod === 'ddl' && sqlValidation) ? (
|
||||
<SQLValidationStatus
|
||||
validation={sqlValidation}
|
||||
errorMessage={errorMessage}
|
||||
isAutoFixing={isAutoFixing}
|
||||
onErrorClick={handleErrorClick}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
[
|
||||
errorMessage,
|
||||
scriptResult,
|
||||
importMethod,
|
||||
effectiveTheme,
|
||||
debouncedHandleInputChange,
|
||||
handleEditorDidMount,
|
||||
sqlValidation,
|
||||
isAutoFixing,
|
||||
handleErrorClick,
|
||||
]
|
||||
);
|
||||
|
||||
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,13 +545,43 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
</DialogClose>
|
||||
)}
|
||||
|
||||
{keepDialogAfterImport ? (
|
||||
{showCheckJsonButton ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
onClick={handleCheckJson}
|
||||
disabled={isCheckingJson}
|
||||
>
|
||||
{isCheckingJson ? (
|
||||
<Spinner size="small" />
|
||||
) : (
|
||||
t(
|
||||
'new_diagram_dialog.import_database.check_script_result'
|
||||
)
|
||||
)}
|
||||
</Button>
|
||||
) : showAutoFixButton && importMethod === 'ddl' ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleAutoFix}
|
||||
disabled={isAutoFixing}
|
||||
className="bg-sky-600 text-white hover:bg-sky-700"
|
||||
>
|
||||
{isAutoFixing ? (
|
||||
<Spinner size="small" />
|
||||
) : (
|
||||
'Try auto-fix'
|
||||
)}
|
||||
</Button>
|
||||
) : keepDialogAfterImport ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
disabled={
|
||||
scriptResult.trim().length === 0 ||
|
||||
errorMessage.length > 0
|
||||
errorMessage.length > 0 ||
|
||||
isAutoFixing
|
||||
}
|
||||
onClick={handleImport}
|
||||
>
|
||||
@@ -437,9 +593,9 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
type="button"
|
||||
variant="default"
|
||||
disabled={
|
||||
showCheckJsonButton ||
|
||||
scriptResult.trim().length === 0 ||
|
||||
errorMessage.length > 0
|
||||
errorMessage.length > 0 ||
|
||||
isAutoFixing
|
||||
}
|
||||
onClick={handleImport}
|
||||
>
|
||||
@@ -468,8 +624,14 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
errorMessage.length,
|
||||
scriptResult,
|
||||
showCheckJsonButton,
|
||||
isCheckingJson,
|
||||
handleCheckJson,
|
||||
goBack,
|
||||
t,
|
||||
importMethod,
|
||||
isAutoFixing,
|
||||
showAutoFixButton,
|
||||
handleAutoFix,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
179
src/dialogs/common/import-database/sql-validation-status.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { CheckCircle, AlertTriangle, MessageCircleWarning } from 'lucide-react';
|
||||
import { Alert, AlertDescription } from '@/components/alert/alert';
|
||||
import type { ValidationResult } from '@/lib/data/sql-import/sql-validator';
|
||||
import { Separator } from '@/components/separator/separator';
|
||||
import { ScrollArea } from '@/components/scroll-area/scroll-area';
|
||||
import { Spinner } from '@/components/spinner/spinner';
|
||||
|
||||
interface SQLValidationStatusProps {
|
||||
validation?: ValidationResult | null;
|
||||
errorMessage: string;
|
||||
isAutoFixing?: boolean;
|
||||
onErrorClick?: (line: number) => void;
|
||||
}
|
||||
|
||||
export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({
|
||||
validation,
|
||||
errorMessage,
|
||||
isAutoFixing = false,
|
||||
onErrorClick,
|
||||
}) => {
|
||||
const hasErrors = useMemo(
|
||||
() => validation?.errors.length && validation.errors.length > 0,
|
||||
[validation?.errors]
|
||||
);
|
||||
const hasWarnings = useMemo(
|
||||
() => validation?.warnings && validation.warnings.length > 0,
|
||||
[validation?.warnings]
|
||||
);
|
||||
const wasAutoFixed = useMemo(
|
||||
() =>
|
||||
validation?.warnings?.some((w) =>
|
||||
w.message.includes('Auto-fixed')
|
||||
) || false,
|
||||
[validation?.warnings]
|
||||
);
|
||||
|
||||
if (!validation && !errorMessage && !isAutoFixing) return null;
|
||||
|
||||
if (isAutoFixing) {
|
||||
return (
|
||||
<>
|
||||
<Separator className="mb-1 mt-2" />
|
||||
<div className="rounded-md border border-sky-200 bg-sky-50 dark:border-sky-800 dark:bg-sky-950">
|
||||
<div className="space-y-3 p-3 pt-2 text-sky-700 dark:text-sky-300">
|
||||
<div className="flex items-start gap-2">
|
||||
<Spinner className="mt-0.5 size-4 shrink-0 text-sky-700 dark:text-sky-300" />
|
||||
<div className="flex-1 text-sm text-sky-700 dark:text-sky-300">
|
||||
Auto-fixing SQL syntax errors...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// If we have parser errors (errorMessage) after validation
|
||||
if (errorMessage && !hasErrors) {
|
||||
return (
|
||||
<>
|
||||
<Separator className="mb-1 mt-2" />
|
||||
<div className="mb-1 flex shrink-0 items-center gap-2">
|
||||
<p className="text-xs text-red-700">{errorMessage}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Separator className="mb-1 mt-2" />
|
||||
|
||||
{hasErrors ? (
|
||||
<div className="rounded-md border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950">
|
||||
<ScrollArea className="h-24">
|
||||
<div className="space-y-3 p-3 pt-2 text-red-700 dark:text-red-300">
|
||||
{validation?.errors
|
||||
.slice(0, 3)
|
||||
.map((error, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex items-start gap-2"
|
||||
>
|
||||
<MessageCircleWarning className="mt-0.5 size-4 shrink-0 text-red-700 dark:text-red-300" />
|
||||
<div className="flex-1 text-sm text-red-700 dark:text-red-300">
|
||||
<button
|
||||
onClick={() =>
|
||||
onErrorClick?.(error.line)
|
||||
}
|
||||
className="rounded font-medium underline hover:text-red-600 focus:outline-none focus:ring-1 focus:ring-red-500 dark:hover:text-red-200"
|
||||
type="button"
|
||||
>
|
||||
Line {error.line}
|
||||
</button>
|
||||
<span className="mx-1">:</span>
|
||||
<span className="text-xs">
|
||||
{error.message}
|
||||
</span>
|
||||
{error.suggestion && (
|
||||
<div className="mt-1 flex items-start gap-2">
|
||||
<span className="text-xs font-medium ">
|
||||
{error.suggestion}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{validation?.errors &&
|
||||
validation?.errors.length > 3 ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<MessageCircleWarning className="mt-0.5 size-4 shrink-0 text-red-700 dark:text-red-300" />
|
||||
<span className="text-xs font-medium">
|
||||
{validation.errors.length - 3} more
|
||||
error
|
||||
{validation.errors.length - 3 > 1
|
||||
? 's'
|
||||
: ''}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{wasAutoFixed && !hasErrors ? (
|
||||
<Alert className="border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950">
|
||||
<CheckCircle className="size-4 text-green-600 dark:text-green-400" />
|
||||
<AlertDescription className="text-sm text-green-700 dark:text-green-300">
|
||||
SQL syntax errors were automatically fixed. Your SQL is
|
||||
now ready to import.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{hasWarnings && !hasErrors ? (
|
||||
<div className="rounded-md border border-sky-200 bg-sky-50 dark:border-sky-800 dark:bg-sky-950">
|
||||
<ScrollArea className="h-24">
|
||||
<div className="space-y-3 p-3 pt-2 text-sky-700 dark:text-sky-300">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-sky-700 dark:text-sky-300" />
|
||||
<div className="flex-1 text-sm text-sky-700 dark:text-sky-300">
|
||||
<div className="mb-1 font-medium">
|
||||
Import Info:
|
||||
</div>
|
||||
{validation?.warnings.map(
|
||||
(warning, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="ml-2 text-xs"
|
||||
>
|
||||
• {warning.message}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!hasErrors && !hasWarnings && !errorMessage && validation ? (
|
||||
<div className="rounded-md border border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950">
|
||||
<div className="space-y-3 p-3 pt-2 text-green-700 dark:text-green-300">
|
||||
<div className="flex items-start gap-2">
|
||||
<CheckCircle className="mt-0.5 size-4 shrink-0 text-green-700 dark:text-green-300" />
|
||||
<div className="flex-1 text-sm text-green-700 dark:text-green-300">
|
||||
SQL syntax validated successfully
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
2
src/dialogs/common/select-tables/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const MAX_TABLES_IN_DIAGRAM = 500;
|
||||
export const MAX_TABLES_WITHOUT_SHOWING_FILTER = 50;
|
||||
683
src/dialogs/common/select-tables/select-tables.tsx
Normal file
@@ -0,0 +1,683 @@
|
||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
|
||||
import { Button } from '@/components/button/button';
|
||||
import { Input } from '@/components/input/input';
|
||||
import { Search, AlertCircle, Check, X, View, Table } from 'lucide-react';
|
||||
import { Checkbox } from '@/components/checkbox/checkbox';
|
||||
import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
|
||||
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogInternalContent,
|
||||
DialogTitle,
|
||||
} from '@/components/dialog/dialog';
|
||||
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
|
||||
import { generateTableKey } from '@/lib/domain';
|
||||
import { Spinner } from '@/components/spinner/spinner';
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
} from '@/components/pagination/pagination';
|
||||
import { MAX_TABLES_IN_DIAGRAM } from './constants';
|
||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface SelectTablesProps {
|
||||
databaseMetadata?: DatabaseMetadata;
|
||||
onImport: ({
|
||||
selectedTables,
|
||||
databaseMetadata,
|
||||
}: {
|
||||
selectedTables?: SelectedTable[];
|
||||
databaseMetadata?: DatabaseMetadata;
|
||||
}) => Promise<void>;
|
||||
onBack: () => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const TABLES_PER_PAGE = 10;
|
||||
|
||||
interface TableInfo {
|
||||
key: string;
|
||||
schema?: string;
|
||||
tableName: string;
|
||||
fullName: string;
|
||||
type: 'table' | 'view';
|
||||
}
|
||||
|
||||
export const SelectTables: React.FC<SelectTablesProps> = ({
|
||||
databaseMetadata,
|
||||
onImport,
|
||||
onBack,
|
||||
isLoading = false,
|
||||
}) => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [showTables, setShowTables] = useState(true);
|
||||
const [showViews, setShowViews] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
|
||||
// Prepare all tables and views with their metadata
|
||||
const allTables = useMemo(() => {
|
||||
const tables: TableInfo[] = [];
|
||||
|
||||
// Add regular tables
|
||||
databaseMetadata?.tables.forEach((table) => {
|
||||
const schema = schemaNameToDomainSchemaName(table.schema);
|
||||
const tableName = table.table;
|
||||
|
||||
const key = `table:${generateTableKey({ tableName, schemaName: schema })}`;
|
||||
|
||||
tables.push({
|
||||
key,
|
||||
schema,
|
||||
tableName,
|
||||
fullName: schema ? `${schema}.${tableName}` : tableName,
|
||||
type: 'table',
|
||||
});
|
||||
});
|
||||
|
||||
// Add views
|
||||
databaseMetadata?.views?.forEach((view) => {
|
||||
const schema = schemaNameToDomainSchemaName(view.schema);
|
||||
const viewName = view.view_name;
|
||||
|
||||
if (!viewName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = `view:${generateTableKey({
|
||||
tableName: viewName,
|
||||
schemaName: schema,
|
||||
})}`;
|
||||
|
||||
tables.push({
|
||||
key,
|
||||
schema,
|
||||
tableName: viewName,
|
||||
fullName:
|
||||
schema === 'default' ? viewName : `${schema}.${viewName}`,
|
||||
type: 'view',
|
||||
});
|
||||
});
|
||||
|
||||
return tables.sort((a, b) => a.fullName.localeCompare(b.fullName));
|
||||
}, [databaseMetadata?.tables, databaseMetadata?.views]);
|
||||
|
||||
// Count tables and views separately
|
||||
const tableCount = useMemo(
|
||||
() => allTables.filter((t) => t.type === 'table').length,
|
||||
[allTables]
|
||||
);
|
||||
const viewCount = useMemo(
|
||||
() => allTables.filter((t) => t.type === 'view').length,
|
||||
[allTables]
|
||||
);
|
||||
|
||||
// Initialize selectedTables with all tables (not views) if less than 100 tables
|
||||
const [selectedTables, setSelectedTables] = useState<Set<string>>(() => {
|
||||
const tables = allTables.filter((t) => t.type === 'table');
|
||||
if (tables.length < MAX_TABLES_IN_DIAGRAM) {
|
||||
return new Set(tables.map((t) => t.key));
|
||||
}
|
||||
return new Set();
|
||||
});
|
||||
|
||||
// Filter tables based on search term and type filters
|
||||
const filteredTables = useMemo(() => {
|
||||
let filtered = allTables;
|
||||
|
||||
// Filter by type
|
||||
filtered = filtered.filter((table) => {
|
||||
if (table.type === 'table' && !showTables) return false;
|
||||
if (table.type === 'view' && !showViews) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Filter by search term
|
||||
if (searchTerm.trim()) {
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
(table) =>
|
||||
table.tableName.toLowerCase().includes(searchLower) ||
|
||||
table.schema?.toLowerCase().includes(searchLower) ||
|
||||
table.fullName.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}, [allTables, searchTerm, showTables, showViews]);
|
||||
|
||||
// Calculate pagination
|
||||
const totalPages = useMemo(
|
||||
() => Math.max(1, Math.ceil(filteredTables.length / TABLES_PER_PAGE)),
|
||||
[filteredTables.length]
|
||||
);
|
||||
|
||||
const paginatedTables = useMemo(() => {
|
||||
const startIndex = (currentPage - 1) * TABLES_PER_PAGE;
|
||||
const endIndex = startIndex + TABLES_PER_PAGE;
|
||||
return filteredTables.slice(startIndex, endIndex);
|
||||
}, [filteredTables, currentPage]);
|
||||
|
||||
// Get currently visible selected tables
|
||||
const visibleSelectedTables = useMemo(() => {
|
||||
return paginatedTables.filter((table) => selectedTables.has(table.key));
|
||||
}, [paginatedTables, selectedTables]);
|
||||
|
||||
const canAddMore = useMemo(
|
||||
() => selectedTables.size < MAX_TABLES_IN_DIAGRAM,
|
||||
[selectedTables.size]
|
||||
);
|
||||
const hasSearchResults = useMemo(
|
||||
() => filteredTables.length > 0,
|
||||
[filteredTables.length]
|
||||
);
|
||||
const allVisibleSelected = useMemo(
|
||||
() =>
|
||||
visibleSelectedTables.length === paginatedTables.length &&
|
||||
paginatedTables.length > 0,
|
||||
[visibleSelectedTables.length, paginatedTables.length]
|
||||
);
|
||||
const canSelectAllFiltered = useMemo(
|
||||
() =>
|
||||
filteredTables.length > 0 &&
|
||||
filteredTables.some((table) => !selectedTables.has(table.key)) &&
|
||||
canAddMore,
|
||||
[filteredTables, selectedTables, canAddMore]
|
||||
);
|
||||
|
||||
// Reset to first page when search changes
|
||||
useEffect(() => {
|
||||
setCurrentPage(1);
|
||||
}, [searchTerm]);
|
||||
|
||||
const handleTableToggle = useCallback(
|
||||
(tableKey: string) => {
|
||||
const newSelected = new Set(selectedTables);
|
||||
|
||||
if (newSelected.has(tableKey)) {
|
||||
newSelected.delete(tableKey);
|
||||
} else if (selectedTables.size < MAX_TABLES_IN_DIAGRAM) {
|
||||
newSelected.add(tableKey);
|
||||
}
|
||||
|
||||
setSelectedTables(newSelected);
|
||||
},
|
||||
[selectedTables]
|
||||
);
|
||||
|
||||
const handleTogglePageSelection = useCallback(() => {
|
||||
const newSelected = new Set(selectedTables);
|
||||
|
||||
if (allVisibleSelected) {
|
||||
// Deselect all on current page
|
||||
for (const table of paginatedTables) {
|
||||
newSelected.delete(table.key);
|
||||
}
|
||||
} else {
|
||||
// Select all on current page
|
||||
for (const table of paginatedTables) {
|
||||
if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break;
|
||||
newSelected.add(table.key);
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedTables(newSelected);
|
||||
}, [allVisibleSelected, paginatedTables, selectedTables]);
|
||||
|
||||
const handleSelectAllFiltered = useCallback(() => {
|
||||
const newSelected = new Set(selectedTables);
|
||||
|
||||
for (const table of filteredTables) {
|
||||
if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break;
|
||||
newSelected.add(table.key);
|
||||
}
|
||||
|
||||
setSelectedTables(newSelected);
|
||||
}, [filteredTables, selectedTables]);
|
||||
|
||||
const handleNextPage = useCallback(() => {
|
||||
if (currentPage < totalPages) {
|
||||
setCurrentPage(currentPage + 1);
|
||||
}
|
||||
}, [currentPage, totalPages]);
|
||||
|
||||
const handlePrevPage = useCallback(() => {
|
||||
if (currentPage > 1) {
|
||||
setCurrentPage(currentPage - 1);
|
||||
}
|
||||
}, [currentPage]);
|
||||
|
||||
const handleClearSelection = useCallback(() => {
|
||||
setSelectedTables(new Set());
|
||||
}, []);
|
||||
|
||||
const handleConfirm = useCallback(async () => {
|
||||
if (isImporting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsImporting(true);
|
||||
|
||||
try {
|
||||
const selectedTableObjects: SelectedTable[] = Array.from(
|
||||
selectedTables
|
||||
)
|
||||
.map((key): SelectedTable | null => {
|
||||
const table = allTables.find((t) => t.key === key);
|
||||
if (!table) return null;
|
||||
|
||||
return {
|
||||
schema: table.schema,
|
||||
table: table.tableName,
|
||||
type: table.type,
|
||||
} satisfies SelectedTable;
|
||||
})
|
||||
.filter((t): t is SelectedTable => t !== null);
|
||||
|
||||
await onImport({
|
||||
selectedTables: selectedTableObjects,
|
||||
databaseMetadata,
|
||||
});
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
}
|
||||
}, [selectedTables, allTables, onImport, databaseMetadata, isImporting]);
|
||||
|
||||
const { isMd: isDesktop } = useBreakpoint('md');
|
||||
|
||||
const renderPagination = useCallback(
|
||||
() => (
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={handlePrevPage}
|
||||
className={cn(
|
||||
'cursor-pointer',
|
||||
currentPage === 1 &&
|
||||
'pointer-events-none opacity-50'
|
||||
)}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<span className="px-3 text-sm text-muted-foreground">
|
||||
Page {currentPage} of {totalPages}
|
||||
</span>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={handleNextPage}
|
||||
className={cn(
|
||||
'cursor-pointer',
|
||||
(currentPage >= totalPages ||
|
||||
filteredTables.length === 0) &&
|
||||
'pointer-events-none opacity-50'
|
||||
)}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
),
|
||||
[
|
||||
currentPage,
|
||||
totalPages,
|
||||
handlePrevPage,
|
||||
handleNextPage,
|
||||
filteredTables.length,
|
||||
]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex h-[400px] items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Spinner className="mb-4" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Parsing database metadata...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Select Tables to Import</DialogTitle>
|
||||
<DialogDescription>
|
||||
{tableCount} {tableCount === 1 ? 'table' : 'tables'}
|
||||
{viewCount > 0 && (
|
||||
<>
|
||||
{' and '}
|
||||
{viewCount} {viewCount === 1 ? 'view' : 'views'}
|
||||
</>
|
||||
)}
|
||||
{' found. '}
|
||||
{allTables.length > MAX_TABLES_IN_DIAGRAM
|
||||
? `Select up to ${MAX_TABLES_IN_DIAGRAM} to import.`
|
||||
: 'Choose which ones to import.'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogInternalContent>
|
||||
<div className="flex h-full flex-col space-y-4">
|
||||
{/* Warning/Info Banner */}
|
||||
{allTables.length > MAX_TABLES_IN_DIAGRAM ? (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg p-3 text-sm',
|
||||
'bg-amber-50 text-amber-800 dark:bg-amber-950 dark:text-amber-200'
|
||||
)}
|
||||
>
|
||||
<AlertCircle className="size-4 shrink-0" />
|
||||
<span>
|
||||
Due to performance limitations, you can import a
|
||||
maximum of {MAX_TABLES_IN_DIAGRAM} tables.
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{/* Search Input */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search tables..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="px-9"
|
||||
/>
|
||||
{searchTerm && (
|
||||
<button
|
||||
onClick={() => setSearchTerm('')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Selection Status and Actions - Responsive layout */}
|
||||
<div className="flex flex-col items-center gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
||||
{/* Left side: selection count -> checkboxes -> results found */}
|
||||
<div className="flex flex-col items-center gap-3 text-sm sm:flex-row sm:items-center sm:gap-4">
|
||||
<div className="flex flex-col items-center gap-1 sm:flex-row sm:items-center sm:gap-4">
|
||||
<span className="text-center font-medium">
|
||||
{selectedTables.size} /{' '}
|
||||
{Math.min(
|
||||
MAX_TABLES_IN_DIAGRAM,
|
||||
allTables.length
|
||||
)}{' '}
|
||||
items selected
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 sm:border-x sm:px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={showTables}
|
||||
onCheckedChange={(checked) => {
|
||||
// Prevent unchecking if it's the only one checked
|
||||
if (!checked && !showViews) return;
|
||||
setShowTables(!!checked);
|
||||
}}
|
||||
/>
|
||||
<Table
|
||||
className="size-4"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<span>tables</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={showViews}
|
||||
onCheckedChange={(checked) => {
|
||||
// Prevent unchecking if it's the only one checked
|
||||
if (!checked && !showTables) return;
|
||||
setShowViews(!!checked);
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
className="size-4"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
<span>views</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className="hidden text-muted-foreground sm:inline">
|
||||
{filteredTables.length}{' '}
|
||||
{filteredTables.length === 1
|
||||
? 'result'
|
||||
: 'results'}{' '}
|
||||
found
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Right side: action buttons */}
|
||||
<div className="flex flex-wrap items-center justify-center gap-2">
|
||||
{hasSearchResults && (
|
||||
<>
|
||||
{/* Show page selection button when not searching and no selection */}
|
||||
{!searchTerm &&
|
||||
selectedTables.size === 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={
|
||||
handleTogglePageSelection
|
||||
}
|
||||
disabled={
|
||||
paginatedTables.length === 0
|
||||
}
|
||||
>
|
||||
{allVisibleSelected
|
||||
? 'Deselect'
|
||||
: 'Select'}{' '}
|
||||
page
|
||||
</Button>
|
||||
)}
|
||||
{/* Show Select all button when there are unselected tables */}
|
||||
{canSelectAllFiltered &&
|
||||
selectedTables.size === 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={
|
||||
handleSelectAllFiltered
|
||||
}
|
||||
disabled={!canSelectAllFiltered}
|
||||
title={(() => {
|
||||
const unselectedCount =
|
||||
filteredTables.filter(
|
||||
(table) =>
|
||||
!selectedTables.has(
|
||||
table.key
|
||||
)
|
||||
).length;
|
||||
const remainingCapacity =
|
||||
MAX_TABLES_IN_DIAGRAM -
|
||||
selectedTables.size;
|
||||
if (
|
||||
unselectedCount >
|
||||
remainingCapacity
|
||||
) {
|
||||
return `Can only select ${remainingCapacity} more tables (${MAX_TABLES_IN_DIAGRAM} max limit)`;
|
||||
}
|
||||
return undefined;
|
||||
})()}
|
||||
>
|
||||
{(() => {
|
||||
const unselectedCount =
|
||||
filteredTables.filter(
|
||||
(table) =>
|
||||
!selectedTables.has(
|
||||
table.key
|
||||
)
|
||||
).length;
|
||||
const remainingCapacity =
|
||||
MAX_TABLES_IN_DIAGRAM -
|
||||
selectedTables.size;
|
||||
if (
|
||||
unselectedCount >
|
||||
remainingCapacity
|
||||
) {
|
||||
return `Select ${remainingCapacity} of ${unselectedCount}`;
|
||||
}
|
||||
return `Select all ${unselectedCount}`;
|
||||
})()}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{selectedTables.size > 0 && (
|
||||
<>
|
||||
{/* Show page selection/deselection button when user has selections */}
|
||||
{paginatedTables.length > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleTogglePageSelection}
|
||||
>
|
||||
{allVisibleSelected
|
||||
? 'Deselect'
|
||||
: 'Select'}{' '}
|
||||
page
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleClearSelection}
|
||||
>
|
||||
Clear selection
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table List */}
|
||||
<div className="flex min-h-[428px] flex-1 flex-col">
|
||||
{hasSearchResults ? (
|
||||
<>
|
||||
<div className="flex-1 py-4">
|
||||
<div className="space-y-1">
|
||||
{paginatedTables.map((table) => {
|
||||
const isSelected = selectedTables.has(
|
||||
table.key
|
||||
);
|
||||
const isDisabled =
|
||||
!isSelected &&
|
||||
selectedTables.size >=
|
||||
MAX_TABLES_IN_DIAGRAM;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={table.key}
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
|
||||
{
|
||||
'cursor-not-allowed':
|
||||
isDisabled,
|
||||
|
||||
'bg-muted hover:bg-muted/80':
|
||||
isSelected,
|
||||
'hover:bg-accent':
|
||||
!isSelected &&
|
||||
!isDisabled,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
disabled={isDisabled}
|
||||
onCheckedChange={() =>
|
||||
handleTableToggle(
|
||||
table.key
|
||||
)
|
||||
}
|
||||
/>
|
||||
{table.type === 'view' ? (
|
||||
<View
|
||||
className="size-4"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
) : (
|
||||
<Table
|
||||
className="size-4"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
)}
|
||||
<span className="flex-1">
|
||||
{table.schema ? (
|
||||
<span className="text-muted-foreground">
|
||||
{table.schema}.
|
||||
</span>
|
||||
) : null}
|
||||
<span className="font-medium">
|
||||
{table.tableName}
|
||||
</span>
|
||||
{table.type === 'view' && (
|
||||
<span className="ml-2 text-xs text-muted-foreground">
|
||||
(view)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{isSelected && (
|
||||
<Check className="size-4 text-pink-600" />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center py-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{searchTerm
|
||||
? 'No tables found matching your search.'
|
||||
: 'Start typing to search for tables...'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isDesktop ? renderPagination() : null}
|
||||
</DialogInternalContent>
|
||||
<DialogFooter className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2 md:justify-between md:gap-0">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={onBack}
|
||||
disabled={isImporting}
|
||||
>
|
||||
{t('new_diagram_dialog.back')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
disabled={selectedTables.size === 0 || isImporting}
|
||||
className="bg-pink-500 text-white hover:bg-pink-600"
|
||||
>
|
||||
{isImporting ? (
|
||||
<>
|
||||
<Spinner className="mr-2 size-4 text-white" />
|
||||
Importing...
|
||||
</>
|
||||
) : (
|
||||
`Import ${selectedTables.size} Tables`
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{!isDesktop ? renderPagination() : null}
|
||||
</DialogFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum CreateDiagramDialogStep {
|
||||
SELECT_DATABASE = 'SELECT_DATABASE',
|
||||
IMPORT_DATABASE = 'IMPORT_DATABASE',
|
||||
SELECT_TABLES = 'SELECT_TABLES',
|
||||
}
|
||||
|
||||
@@ -15,8 +15,13 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||
import { SelectDatabase } from './select-database/select-database';
|
||||
import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
|
||||
import { ImportDatabase } from '../common/import-database/import-database';
|
||||
import { SelectTables } from '../common/select-tables/select-tables';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||
import { sqlImportToDiagram } from '@/lib/data/sql-import';
|
||||
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
|
||||
import { filterMetadataByTables } from '@/lib/data/import-metadata/filter-metadata';
|
||||
import { MAX_TABLES_WITHOUT_SHOWING_FILTER } from '../common/select-tables/constants';
|
||||
|
||||
export interface CreateDiagramDialogProps extends BaseDialogProps {}
|
||||
|
||||
@@ -25,6 +30,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
|
||||
);
|
||||
@@ -40,6 +46,13 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
const { listDiagrams, addDiagram } = useStorage();
|
||||
const [diagramNumber, setDiagramNumber] = useState<number>(1);
|
||||
const navigate = useNavigate();
|
||||
const [parsedMetadata, setParsedMetadata] = useState<DatabaseMetadata>();
|
||||
const [isParsingMetadata, setIsParsingMetadata] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDatabaseEdition(undefined);
|
||||
setImportMethod('query');
|
||||
}, [databaseType]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDiagrams = async () => {
|
||||
@@ -54,38 +67,73 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
setDatabaseType(DatabaseType.GENERIC);
|
||||
setDatabaseEdition(undefined);
|
||||
setScriptResult('');
|
||||
setImportMethod('query');
|
||||
setParsedMetadata(undefined);
|
||||
}, [dialog.open]);
|
||||
|
||||
const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
|
||||
|
||||
const importNewDiagram = useCallback(async () => {
|
||||
const databaseMetadata: DatabaseMetadata =
|
||||
loadDatabaseMetadata(scriptResult);
|
||||
|
||||
const diagram = await loadFromDatabaseMetadata({
|
||||
databaseType,
|
||||
const importNewDiagram = useCallback(
|
||||
async ({
|
||||
selectedTables,
|
||||
databaseMetadata,
|
||||
diagramNumber,
|
||||
databaseEdition:
|
||||
databaseEdition?.trim().length === 0
|
||||
? undefined
|
||||
: databaseEdition,
|
||||
});
|
||||
}: {
|
||||
selectedTables?: SelectedTable[];
|
||||
databaseMetadata?: DatabaseMetadata;
|
||||
} = {}) => {
|
||||
let diagram: Diagram | undefined;
|
||||
|
||||
await addDiagram({ diagram });
|
||||
await updateConfig({ defaultDiagramId: diagram.id });
|
||||
closeCreateDiagramDialog();
|
||||
navigate(`/diagrams/${diagram.id}`);
|
||||
}, [
|
||||
databaseType,
|
||||
addDiagram,
|
||||
databaseEdition,
|
||||
closeCreateDiagramDialog,
|
||||
navigate,
|
||||
updateConfig,
|
||||
scriptResult,
|
||||
diagramNumber,
|
||||
]);
|
||||
if (importMethod === 'ddl') {
|
||||
diagram = await sqlImportToDiagram({
|
||||
sqlContent: scriptResult,
|
||||
sourceDatabaseType: databaseType,
|
||||
targetDatabaseType: databaseType,
|
||||
});
|
||||
} else {
|
||||
let metadata: DatabaseMetadata | undefined = databaseMetadata;
|
||||
|
||||
if (!metadata) {
|
||||
metadata = loadDatabaseMetadata(scriptResult);
|
||||
}
|
||||
|
||||
if (selectedTables && selectedTables.length > 0) {
|
||||
metadata = filterMetadataByTables({
|
||||
metadata,
|
||||
selectedTables,
|
||||
});
|
||||
}
|
||||
|
||||
diagram = await loadFromDatabaseMetadata({
|
||||
databaseType,
|
||||
databaseMetadata: metadata,
|
||||
diagramNumber,
|
||||
databaseEdition:
|
||||
databaseEdition?.trim().length === 0
|
||||
? undefined
|
||||
: databaseEdition,
|
||||
});
|
||||
}
|
||||
|
||||
await addDiagram({ diagram });
|
||||
await updateConfig({
|
||||
config: { defaultDiagramId: diagram.id },
|
||||
});
|
||||
|
||||
closeCreateDiagramDialog();
|
||||
navigate(`/diagrams/${diagram.id}`);
|
||||
},
|
||||
[
|
||||
importMethod,
|
||||
databaseType,
|
||||
addDiagram,
|
||||
databaseEdition,
|
||||
closeCreateDiagramDialog,
|
||||
navigate,
|
||||
updateConfig,
|
||||
scriptResult,
|
||||
diagramNumber,
|
||||
]
|
||||
);
|
||||
|
||||
const createEmptyDiagram = useCallback(async () => {
|
||||
const diagram: Diagram = {
|
||||
@@ -101,7 +149,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
};
|
||||
|
||||
await addDiagram({ diagram });
|
||||
await updateConfig({ defaultDiagramId: diagram.id });
|
||||
await updateConfig({ config: { defaultDiagramId: diagram.id } });
|
||||
closeCreateDiagramDialog();
|
||||
navigate(`/diagrams/${diagram.id}`);
|
||||
setTimeout(
|
||||
@@ -119,10 +167,56 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
openImportDBMLDialog,
|
||||
]);
|
||||
|
||||
const importNewDiagramOrFilterTables = useCallback(async () => {
|
||||
try {
|
||||
setIsParsingMetadata(true);
|
||||
|
||||
if (importMethod === 'ddl') {
|
||||
await importNewDiagram();
|
||||
} else {
|
||||
// Parse metadata asynchronously to avoid blocking the UI
|
||||
const metadata = await new Promise<DatabaseMetadata>(
|
||||
(resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const result =
|
||||
loadDatabaseMetadata(scriptResult);
|
||||
resolve(result);
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
);
|
||||
|
||||
const totalTablesAndViews =
|
||||
metadata.tables.length + (metadata.views?.length || 0);
|
||||
|
||||
setParsedMetadata(metadata);
|
||||
|
||||
// Check if it's a large database that needs table selection
|
||||
if (totalTablesAndViews > MAX_TABLES_WITHOUT_SHOWING_FILTER) {
|
||||
setStep(CreateDiagramDialogStep.SELECT_TABLES);
|
||||
} else {
|
||||
await importNewDiagram({
|
||||
databaseMetadata: metadata,
|
||||
});
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
setIsParsingMetadata(false);
|
||||
}
|
||||
}, [importMethod, scriptResult, importNewDiagram]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialog}
|
||||
onOpenChange={(open) => {
|
||||
// Don't allow closing while parsing metadata
|
||||
if (isParsingMetadata) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasExistingDiagram) {
|
||||
return;
|
||||
}
|
||||
@@ -133,8 +227,10 @@ 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}
|
||||
onInteractOutside={(e) => e.preventDefault()}
|
||||
onEscapeKeyDown={(e) => e.preventDefault()}
|
||||
>
|
||||
{step === CreateDiagramDialogStep.SELECT_DATABASE ? (
|
||||
<SelectDatabase
|
||||
@@ -146,9 +242,9 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
) : step === CreateDiagramDialogStep.IMPORT_DATABASE ? (
|
||||
<ImportDatabase
|
||||
onImport={importNewDiagram}
|
||||
onImport={importNewDiagramOrFilterTables}
|
||||
onCreateEmptyDiagram={createEmptyDiagram}
|
||||
databaseEdition={databaseEdition}
|
||||
databaseType={databaseType}
|
||||
@@ -159,8 +255,20 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
}
|
||||
setScriptResult={setScriptResult}
|
||||
title={t('new_diagram_dialog.import_database.title')}
|
||||
importMethod={importMethod}
|
||||
setImportMethod={setImportMethod}
|
||||
keepDialogAfterImport={true}
|
||||
/>
|
||||
)}
|
||||
) : step === CreateDiagramDialogStep.SELECT_TABLES ? (
|
||||
<SelectTables
|
||||
isLoading={isParsingMetadata || !parsedMetadata}
|
||||
databaseMetadata={parsedMetadata}
|
||||
onImport={importNewDiagram}
|
||||
onBack={() =>
|
||||
setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,7 @@ const SUPPORTED_DB_TYPES: DatabaseType[] = [
|
||||
DatabaseType.MARIADB,
|
||||
DatabaseType.SQLITE,
|
||||
DatabaseType.SQL_SERVER,
|
||||
DatabaseType.ORACLE,
|
||||
DatabaseType.COCKROACHDB,
|
||||
DatabaseType.CLICKHOUSE,
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, {
|
||||
Suspense,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import type * as monaco from 'monaco-editor';
|
||||
import { useDialog } from '@/hooks/use-dialog';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -23,53 +23,24 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Editor } from '@/components/code-snippet/code-snippet';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { importDBMLToDiagram } from '@/lib/dbml-import';
|
||||
import {
|
||||
importDBMLToDiagram,
|
||||
sanitizeDBML,
|
||||
preprocessDBML,
|
||||
} from '@/lib/dbml/dbml-import/dbml-import';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { Parser } from '@dbml/core';
|
||||
import { useCanvas } from '@/hooks/use-canvas';
|
||||
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import { useToast } from '@/components/toast/use-toast';
|
||||
import { Spinner } from '@/components/spinner/spinner';
|
||||
import { debounce } from '@/lib/utils';
|
||||
|
||||
interface DBMLError {
|
||||
message: string;
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
function parseDBMLError(error: unknown): DBMLError | null {
|
||||
try {
|
||||
if (typeof error === 'string') {
|
||||
const parsed = JSON.parse(error);
|
||||
if (parsed.diags?.[0]) {
|
||||
const diag = parsed.diags[0];
|
||||
return {
|
||||
message: diag.message,
|
||||
line: diag.location.start.line,
|
||||
column: diag.location.start.column,
|
||||
};
|
||||
}
|
||||
} else if (error && typeof error === 'object' && 'diags' in error) {
|
||||
const parsed = error as {
|
||||
diags: Array<{
|
||||
message: string;
|
||||
location: { start: { line: number; column: number } };
|
||||
}>;
|
||||
};
|
||||
if (parsed.diags?.[0]) {
|
||||
return {
|
||||
message: parsed.diags[0].message,
|
||||
line: parsed.diags[0].location.start.line,
|
||||
column: parsed.diags[0].location.start.column,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing DBML error:', e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
|
||||
import {
|
||||
clearErrorHighlight,
|
||||
highlightErrorLine,
|
||||
} from '@/components/code-snippet/dbml/utils';
|
||||
|
||||
export interface ImportDBMLDialogProps extends BaseDialogProps {
|
||||
withCreateEmptyDiagram?: boolean;
|
||||
@@ -145,39 +116,8 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
}
|
||||
}, [reorder, reorderTables]);
|
||||
|
||||
const highlightErrorLine = useCallback((error: DBMLError) => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
const model = editorRef.current.getModel();
|
||||
if (!model) return;
|
||||
|
||||
const decorations = [
|
||||
{
|
||||
range: new monaco.Range(
|
||||
error.line,
|
||||
1,
|
||||
error.line,
|
||||
model.getLineMaxColumn(error.line)
|
||||
),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
className: 'dbml-error-line',
|
||||
glyphMarginClassName: 'dbml-error-glyph',
|
||||
hoverMessage: { value: error.message },
|
||||
overviewRuler: {
|
||||
color: '#ff0000',
|
||||
position: monaco.editor.OverviewRulerLane.Right,
|
||||
darkColor: '#ff0000',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
decorationsCollection.current?.set(decorations);
|
||||
}, []);
|
||||
|
||||
const clearDecorations = useCallback(() => {
|
||||
decorationsCollection.current?.clear();
|
||||
clearErrorHighlight(decorationsCollection.current);
|
||||
}, []);
|
||||
|
||||
const validateDBML = useCallback(
|
||||
@@ -189,8 +129,10 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
if (!content.trim()) return;
|
||||
|
||||
try {
|
||||
const preprocessedContent = preprocessDBML(content);
|
||||
const sanitizedContent = sanitizeDBML(preprocessedContent);
|
||||
const parser = new Parser();
|
||||
parser.parse(content, 'dbml');
|
||||
parser.parse(sanitizedContent, 'dbml');
|
||||
} catch (e) {
|
||||
const parsedError = parseDBMLError(e);
|
||||
if (parsedError) {
|
||||
@@ -198,7 +140,12 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
t('import_dbml_dialog.error.description') +
|
||||
` (1 error found - in line ${parsedError.line})`
|
||||
);
|
||||
highlightErrorLine(parsedError);
|
||||
highlightErrorLine({
|
||||
error: parsedError,
|
||||
model: editorRef.current?.getModel(),
|
||||
editorDecorationsCollection:
|
||||
decorationsCollection.current,
|
||||
});
|
||||
} else {
|
||||
setErrorMessage(
|
||||
e instanceof Error ? e.message : JSON.stringify(e)
|
||||
@@ -206,7 +153,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
}
|
||||
}
|
||||
},
|
||||
[clearDecorations, highlightErrorLine, t]
|
||||
[clearDecorations, t]
|
||||
);
|
||||
|
||||
const debouncedValidateRef = useRef<((value: string) => void) | null>(null);
|
||||
@@ -245,7 +192,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
const tableIdsToRemove = tables
|
||||
.filter((table) =>
|
||||
importedDiagram.tables?.some(
|
||||
(t) =>
|
||||
(t: DBTable) =>
|
||||
t.name === table.name && t.schema === table.schema
|
||||
)
|
||||
)
|
||||
@@ -254,19 +201,21 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
const relationshipIdsToRemove = relationships
|
||||
.filter((relationship) => {
|
||||
const sourceTable = tables.find(
|
||||
(table) => table.id === relationship.sourceTableId
|
||||
(table: DBTable) =>
|
||||
table.id === relationship.sourceTableId
|
||||
);
|
||||
const targetTable = tables.find(
|
||||
(table) => table.id === relationship.targetTableId
|
||||
(table: DBTable) =>
|
||||
table.id === relationship.targetTableId
|
||||
);
|
||||
if (!sourceTable || !targetTable) return true;
|
||||
const replacementSourceTable = importedDiagram.tables?.find(
|
||||
(table) =>
|
||||
(table: DBTable) =>
|
||||
table.name === sourceTable.name &&
|
||||
table.schema === sourceTable.schema
|
||||
);
|
||||
const replacementTargetTable = importedDiagram.tables?.find(
|
||||
(table) =>
|
||||
(table: DBTable) =>
|
||||
table.name === targetTable.name &&
|
||||
table.schema === targetTable.schema
|
||||
);
|
||||
@@ -330,7 +279,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="flex h-[80vh] max-h-screen flex-col"
|
||||
className="flex h-[80vh] max-h-screen w-full flex-col md:max-w-[900px]"
|
||||
showClose
|
||||
>
|
||||
<DialogHeader>
|
||||
|
||||
@@ -65,7 +65,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
||||
const openDiagram = useCallback(
|
||||
(diagramId: string) => {
|
||||
if (diagramId) {
|
||||
updateConfig({ defaultDiagramId: diagramId });
|
||||
updateConfig({ config: { defaultDiagramId: diagramId } });
|
||||
navigate(`/diagrams/${diagramId}`);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDialog } from '@/hooks/use-dialog';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -17,11 +17,23 @@ import type { DBSchema } from '@/lib/domain/db-schema';
|
||||
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
|
||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '@/components/input/input';
|
||||
import { Separator } from '@/components/separator/separator';
|
||||
import { Group, SquarePlus } from 'lucide-react';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/tooltip/tooltip';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
import { Label } from '@/components/label/label';
|
||||
|
||||
export interface TableSchemaDialogProps extends BaseDialogProps {
|
||||
table?: DBTable;
|
||||
schemas: DBSchema[];
|
||||
onConfirm: (schema: string) => void;
|
||||
onConfirm: ({ schema }: { schema: DBSchema }) => void;
|
||||
allowSchemaCreation?: boolean;
|
||||
}
|
||||
|
||||
export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
@@ -29,27 +41,90 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
table,
|
||||
schemas,
|
||||
onConfirm,
|
||||
allowSchemaCreation = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [selectedSchema, setSelectedSchema] = React.useState<string>(
|
||||
const { databaseType, filteredSchemas, filterSchemas } = useChartDB();
|
||||
const [selectedSchemaId, setSelectedSchemaId] = useState<string>(
|
||||
table?.schema
|
||||
? schemaNameToSchemaId(table.schema)
|
||||
: (schemas?.[0]?.id ?? '')
|
||||
);
|
||||
const allowSchemaSelection = useMemo(
|
||||
() => schemas && schemas.length > 0,
|
||||
[schemas]
|
||||
);
|
||||
|
||||
const defaultSchemaName = useMemo(
|
||||
() => defaultSchemas?.[databaseType],
|
||||
[databaseType]
|
||||
);
|
||||
|
||||
const [isCreatingNew, setIsCreatingNew] =
|
||||
useState<boolean>(!allowSchemaSelection);
|
||||
const [newSchemaName, setNewSchemaName] = useState<string>(
|
||||
allowSchemaCreation && !allowSchemaSelection
|
||||
? (defaultSchemaName ?? '')
|
||||
: ''
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialog.open) return;
|
||||
setSelectedSchema(
|
||||
setSelectedSchemaId(
|
||||
table?.schema
|
||||
? schemaNameToSchemaId(table.schema)
|
||||
: (schemas?.[0]?.id ?? '')
|
||||
);
|
||||
}, [dialog.open, schemas, table?.schema]);
|
||||
setIsCreatingNew(!allowSchemaSelection);
|
||||
setNewSchemaName(
|
||||
allowSchemaCreation && !allowSchemaSelection
|
||||
? (defaultSchemaName ?? '')
|
||||
: ''
|
||||
);
|
||||
}, [
|
||||
defaultSchemaName,
|
||||
dialog.open,
|
||||
schemas,
|
||||
table?.schema,
|
||||
allowSchemaSelection,
|
||||
allowSchemaCreation,
|
||||
]);
|
||||
|
||||
const { closeTableSchemaDialog } = useDialog();
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
onConfirm(selectedSchema);
|
||||
}, [onConfirm, selectedSchema]);
|
||||
let createdSchemaId: string;
|
||||
if (isCreatingNew && newSchemaName.trim()) {
|
||||
const newSchema: DBSchema = {
|
||||
id: schemaNameToSchemaId(newSchemaName.trim()),
|
||||
name: newSchemaName.trim(),
|
||||
tableCount: 0,
|
||||
};
|
||||
|
||||
createdSchemaId = newSchema.id;
|
||||
|
||||
onConfirm({ schema: newSchema });
|
||||
} else {
|
||||
const schema = schemas.find((s) => s.id === selectedSchemaId);
|
||||
if (!schema) return;
|
||||
|
||||
createdSchemaId = schema.id;
|
||||
onConfirm({ schema });
|
||||
}
|
||||
|
||||
filterSchemas([
|
||||
...(filteredSchemas ?? schemas.map((s) => s.id)),
|
||||
createdSchemaId,
|
||||
]);
|
||||
}, [
|
||||
onConfirm,
|
||||
selectedSchemaId,
|
||||
schemas,
|
||||
isCreatingNew,
|
||||
newSchemaName,
|
||||
filteredSchemas,
|
||||
filterSchemas,
|
||||
]);
|
||||
|
||||
const schemaOptions: SelectBoxOption[] = useMemo(
|
||||
() =>
|
||||
@@ -60,6 +135,25 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
[schemas]
|
||||
);
|
||||
|
||||
const renderSwitchCreateOrSelectButton = useCallback(
|
||||
() => (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={() => setIsCreatingNew(!isCreatingNew)}
|
||||
disabled={!allowSchemaSelection || !allowSchemaCreation}
|
||||
>
|
||||
{!isCreatingNew ? (
|
||||
<SquarePlus className="mr-2 size-4 " />
|
||||
) : (
|
||||
<Group className="mr-2 size-4 " />
|
||||
)}
|
||||
{isCreatingNew ? 'Select existing schema' : 'Create new schema'}
|
||||
</Button>
|
||||
),
|
||||
[isCreatingNew, allowSchemaSelection, allowSchemaCreation]
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialog}
|
||||
@@ -67,48 +161,106 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
if (!open) {
|
||||
closeTableSchemaDialog();
|
||||
}
|
||||
|
||||
setTimeout(() => (document.body.style.pointerEvents = ''), 500);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="flex flex-col" showClose>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{table
|
||||
? t('update_table_schema_dialog.title')
|
||||
: t('new_table_schema_dialog.title')}
|
||||
{!allowSchemaSelection && allowSchemaCreation
|
||||
? t('create_table_schema_dialog.title')
|
||||
: table
|
||||
? t('update_table_schema_dialog.title')
|
||||
: t('new_table_schema_dialog.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{table
|
||||
? t('update_table_schema_dialog.description', {
|
||||
tableName: table.name,
|
||||
})
|
||||
: t('new_table_schema_dialog.description')}
|
||||
{!allowSchemaSelection && allowSchemaCreation
|
||||
? t('create_table_schema_dialog.description')
|
||||
: table
|
||||
? t('update_table_schema_dialog.description', {
|
||||
tableName: table.name,
|
||||
})
|
||||
: t('new_table_schema_dialog.description')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-1">
|
||||
<div className="grid w-full items-center gap-4">
|
||||
<SelectBox
|
||||
options={schemaOptions}
|
||||
multiple={false}
|
||||
value={selectedSchema}
|
||||
onChange={(value) =>
|
||||
setSelectedSchema(value as string)
|
||||
}
|
||||
/>
|
||||
{!isCreatingNew ? (
|
||||
<SelectBox
|
||||
options={schemaOptions}
|
||||
multiple={false}
|
||||
value={selectedSchemaId}
|
||||
onChange={(value) =>
|
||||
setSelectedSchemaId(value as string)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{allowSchemaCreation &&
|
||||
!allowSchemaSelection ? (
|
||||
<Label htmlFor="new-schema-name">
|
||||
Schema Name
|
||||
</Label>
|
||||
) : null}
|
||||
<Input
|
||||
id="new-schema-name"
|
||||
value={newSchemaName}
|
||||
onChange={(e) =>
|
||||
setNewSchemaName(e.target.value)
|
||||
}
|
||||
placeholder={`Enter schema name.${defaultSchemaName ? ` e.g. ${defaultSchemaName}.` : ''}`}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{allowSchemaCreation && allowSchemaSelection ? (
|
||||
<>
|
||||
<div className="relative">
|
||||
<Separator className="my-2" />
|
||||
<span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-background px-2 text-xs text-muted-foreground">
|
||||
or
|
||||
</span>
|
||||
</div>
|
||||
{allowSchemaSelection ? (
|
||||
renderSwitchCreateOrSelectButton()
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
{renderSwitchCreateOrSelectButton()}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>No existing schemas available</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="flex gap-1 md:justify-between">
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">
|
||||
{table
|
||||
? t('update_table_schema_dialog.cancel')
|
||||
: t('new_table_schema_dialog.cancel')}
|
||||
{isCreatingNew
|
||||
? t('create_table_schema_dialog.cancel')
|
||||
: table
|
||||
? t('update_table_schema_dialog.cancel')
|
||||
: t('new_table_schema_dialog.cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={handleConfirm}>
|
||||
{table
|
||||
? t('update_table_schema_dialog.confirm')
|
||||
: t('new_table_schema_dialog.confirm')}
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
disabled={isCreatingNew && !newSchemaName.trim()}
|
||||
>
|
||||
{isCreatingNew
|
||||
? t('create_table_schema_dialog.create')
|
||||
: table
|
||||
? t('update_table_schema_dialog.confirm')
|
||||
: t('new_table_schema_dialog.confirm')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +83,7 @@
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
overscroll-behavior-x: none;
|
||||
}
|
||||
|
||||
.text-editable {
|
||||
@@ -138,3 +155,29 @@
|
||||
background-size: 650%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Edit button emphasis animation */
|
||||
@keyframes dbml_edit-button-emphasis {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
|
||||
background-color: rgba(59, 130, 246, 0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
|
||||
background-color: rgba(59, 130, 246, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.dbml-edit-button-emphasis {
|
||||
animation: dbml_edit-button-emphasis 0.6s ease-in-out;
|
||||
animation-iteration-count: 1;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
23
src/hooks/use-mobile.tsx
Normal 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;
|
||||
}
|
||||
@@ -23,23 +23,25 @@ import { bn, bnMetadata } from './locales/bn';
|
||||
import { gu, guMetadata } from './locales/gu';
|
||||
import { vi, viMetadata } from './locales/vi';
|
||||
import { ar, arMetadata } from './locales/ar';
|
||||
import { hr, hrMetadata } from './locales/hr';
|
||||
|
||||
export const languages: LanguageMetadata[] = [
|
||||
enMetadata,
|
||||
esMetadata,
|
||||
frMetadata,
|
||||
deMetadata,
|
||||
esMetadata,
|
||||
ukMetadata,
|
||||
ruMetadata,
|
||||
trMetadata,
|
||||
hrMetadata,
|
||||
pt_BRMetadata,
|
||||
hiMetadata,
|
||||
jaMetadata,
|
||||
ko_KRMetadata,
|
||||
pt_BRMetadata,
|
||||
ukMetadata,
|
||||
ruMetadata,
|
||||
zh_CNMetadata,
|
||||
zh_TWMetadata,
|
||||
neMetadata,
|
||||
mrMetadata,
|
||||
trMetadata,
|
||||
id_IDMetadata,
|
||||
teMetadata,
|
||||
bnMetadata,
|
||||
@@ -70,6 +72,7 @@ const resources = {
|
||||
gu,
|
||||
vi,
|
||||
ar,
|
||||
hr,
|
||||
};
|
||||
|
||||
i18n.use(LanguageDetector)
|
||||
|
||||
@@ -26,6 +26,8 @@ export const ar: LanguageTranslation = {
|
||||
hide_sidebar: 'إخفاء الشريط الجانبي',
|
||||
hide_cardinality: 'إخفاء الكاردينالية',
|
||||
show_cardinality: 'إظهار الكاردينالية',
|
||||
hide_field_attributes: 'إخفاء خصائص الحقل',
|
||||
show_field_attributes: 'إظهار خصائص الحقل',
|
||||
zoom_on_scroll: 'تكبير/تصغير عند التمرير',
|
||||
theme: 'المظهر',
|
||||
show_dependencies: 'إظهار الاعتمادات',
|
||||
@@ -42,9 +44,7 @@ export const ar: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'مساعدة',
|
||||
docs_website: 'الوثائق',
|
||||
visit_website: 'ChartDB قم بزيارة',
|
||||
join_discord: 'Discord انضم إلينا على',
|
||||
schedule_a_call: '!تحدث معنا',
|
||||
join_discord: 'انضم إلينا على Discord',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -76,8 +76,8 @@ export const ar: LanguageTranslation = {
|
||||
title: 'مخططات متعددة',
|
||||
description:
|
||||
'{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
|
||||
dont_show_again: 'لا تظهره مجدداً',
|
||||
change_schema: 'تغيير',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'لا شيء',
|
||||
},
|
||||
|
||||
@@ -153,6 +153,10 @@ export const ar: LanguageTranslation = {
|
||||
delete_field: 'حذف الحقل',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
precision: 'الدقة',
|
||||
scale: 'النطاق',
|
||||
default_value: 'Default Value',
|
||||
no_default: 'No default',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'خصائص الفهرس',
|
||||
@@ -212,6 +216,57 @@ 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',
|
||||
no_fields_tooltip: 'No fields defined for this custom type',
|
||||
custom_type_actions: {
|
||||
title: 'Actions',
|
||||
highlight_fields: 'Highlight Fields',
|
||||
delete_custom_type: 'Delete',
|
||||
clear_field_highlight: 'Clear Highlight',
|
||||
},
|
||||
delete_custom_type: 'Delete Type',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -223,6 +278,11 @@ export const ar: LanguageTranslation = {
|
||||
redo: 'إعادة',
|
||||
reorder_diagram: 'إعادة ترتيب الرسم البياني',
|
||||
highlight_overlapping_tables: 'تمييز الجداول المتداخلة',
|
||||
// TODO: Translate
|
||||
filter: 'Filter Tables',
|
||||
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||
custom_type_highlight_tooltip:
|
||||
'Highlighting "{{typeName}}" - Click to clear',
|
||||
},
|
||||
|
||||
new_diagram_dialog: {
|
||||
@@ -238,7 +298,7 @@ export const ar: LanguageTranslation = {
|
||||
title: 'إسترد قاعدة بياناتك',
|
||||
database_edition: ':إصدار قاعدة البيانات',
|
||||
step_1: ':قم بتشغيل هذا البرنامج النصي في قاعدة بياناتك',
|
||||
step_2: ':إلصق نتيجة البرنامج النصي هنا',
|
||||
step_2: ':إلصق نتيجة البرنامج النصي هنا →',
|
||||
script_results_placeholder: '...نتيجة البرنامج النصي هنا',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS تعليمات',
|
||||
@@ -332,6 +392,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: {
|
||||
@@ -348,6 +414,13 @@ export const ar: LanguageTranslation = {
|
||||
cancel: 'إلغاء',
|
||||
confirm: 'تغيير',
|
||||
},
|
||||
create_table_schema_dialog: {
|
||||
title: 'إنشاء مخطط جديد',
|
||||
description:
|
||||
'لا توجد مخططات حتى الآن. قم بإنشاء أول مخطط لتنظيم جداولك.',
|
||||
create: 'إنشاء',
|
||||
cancel: 'إلغاء',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: '!ساعدنا على التحسن',
|
||||
@@ -364,7 +437,7 @@ export const ar: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'حدث خطأ أثناء التصدير',
|
||||
description:
|
||||
'chartdb.io@gmail.com حدث خطأ ما. هل تحتاج إلى مساعدة؟',
|
||||
'support@chartdb.io حدث خطأ ما. هل تحتاج إلى مساعدة؟',
|
||||
},
|
||||
},
|
||||
import_diagram_dialog: {
|
||||
@@ -375,7 +448,7 @@ export const ar: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'حدث خطأ أثناء الاستيراد',
|
||||
description:
|
||||
'chartdb.io@gmail.com و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
|
||||
'support@chartdb.io و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
|
||||
},
|
||||
},
|
||||
import_dbml_dialog: {
|
||||
@@ -402,6 +475,8 @@ export const ar: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'جدول جديد',
|
||||
new_relationship: 'علاقة جديدة',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
||||
@@ -26,6 +26,8 @@ export const bn: LanguageTranslation = {
|
||||
hide_sidebar: 'সাইডবার লুকান',
|
||||
hide_cardinality: 'কার্ডিনালিটি লুকান',
|
||||
show_cardinality: 'কার্ডিনালিটি দেখান',
|
||||
hide_field_attributes: 'ফিল্ড অ্যাট্রিবিউট লুকান',
|
||||
show_field_attributes: 'ফিল্ড অ্যাট্রিবিউট দেখান',
|
||||
zoom_on_scroll: 'স্ক্রলে জুম করুন',
|
||||
theme: 'থিম',
|
||||
show_dependencies: 'নির্ভরতাগুলি দেখান',
|
||||
@@ -43,9 +45,7 @@ export const bn: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'সাহায্য',
|
||||
docs_website: 'ডকুমেন্টেশন',
|
||||
visit_website: 'ChartDB ওয়েবসাইটে যান',
|
||||
join_discord: 'আমাদের Discord-এ যোগ দিন',
|
||||
schedule_a_call: 'আমাদের সাথে কথা বলুন!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -77,8 +77,8 @@ export const bn: LanguageTranslation = {
|
||||
title: 'বহু স্কিমা',
|
||||
description:
|
||||
'{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।',
|
||||
dont_show_again: 'পুনরায় দেখাবেন না',
|
||||
change_schema: 'পরিবর্তন করুন',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'কিছুই না',
|
||||
},
|
||||
|
||||
@@ -153,7 +153,12 @@ export const bn: LanguageTranslation = {
|
||||
no_comments: 'কোনো মন্তব্য নেই',
|
||||
delete_field: 'ফিল্ড মুছুন',
|
||||
// TODO: Translate
|
||||
default_value: 'Default Value',
|
||||
no_default: 'No default',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
precision: 'নির্ভুলতা',
|
||||
scale: 'স্কেল',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'ইনডেক্স কর্ম',
|
||||
@@ -213,6 +218,56 @@ 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',
|
||||
no_fields_tooltip: 'No fields defined for this custom type',
|
||||
custom_type_actions: {
|
||||
title: 'Actions',
|
||||
highlight_fields: 'Highlight Fields',
|
||||
delete_custom_type: 'Delete',
|
||||
clear_field_highlight: 'Clear Highlight',
|
||||
},
|
||||
delete_custom_type: 'Delete Type',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -224,6 +279,12 @@ export const bn: LanguageTranslation = {
|
||||
redo: 'পুনরায় করুন',
|
||||
reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন',
|
||||
highlight_overlapping_tables: 'ওভারল্যাপিং টেবিল হাইলাইট করুন',
|
||||
|
||||
// TODO: Translate
|
||||
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||
custom_type_highlight_tooltip:
|
||||
'Highlighting "{{typeName}}" - Click to clear',
|
||||
filter: 'Filter Tables',
|
||||
},
|
||||
|
||||
new_diagram_dialog: {
|
||||
@@ -239,7 +300,7 @@ export const bn: LanguageTranslation = {
|
||||
title: 'আপনার ডাটাবেস আমদানি করুন',
|
||||
database_edition: 'ডাটাবেস সংস্করণ:',
|
||||
step_1: 'আপনার ডাটাবেসে এই স্ক্রিপ্ট চালান:',
|
||||
step_2: 'স্ক্রিপ্টের ফলাফল এখানে পেস্ট করুন:',
|
||||
step_2: 'স্ক্রিপ্টের ফলাফল এখানে পেস্ট করুন →',
|
||||
script_results_placeholder: 'স্ক্রিপ্টের ফলাফল এখানে...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS নির্দেশনা',
|
||||
@@ -333,6 +394,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: {
|
||||
@@ -349,6 +416,13 @@ export const bn: LanguageTranslation = {
|
||||
cancel: 'বাতিল করুন',
|
||||
confirm: 'পরিবর্তন করুন',
|
||||
},
|
||||
create_table_schema_dialog: {
|
||||
title: 'নতুন স্কিমা তৈরি করুন',
|
||||
description:
|
||||
'এখনও কোনো স্কিমা নেই। আপনার টেবিলগুলি সংগঠিত করতে আপনার প্রথম স্কিমা তৈরি করুন।',
|
||||
create: 'তৈরি করুন',
|
||||
cancel: 'বাতিল করুন',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'আমাদের উন্নত করতে সাহায্য করুন!',
|
||||
@@ -367,7 +441,7 @@ export const bn: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'চিত্র রপ্তানিতে ত্রুটি',
|
||||
description:
|
||||
'কিছু ভুল হয়েছে। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
|
||||
'কিছু ভুল হয়েছে। সাহায্যের প্রয়োজন? support@chartdb.io-এ যোগাযোগ করুন।',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -379,7 +453,7 @@ export const bn: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'চিত্র আমদানিতে ত্রুটি',
|
||||
description:
|
||||
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
|
||||
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? support@chartdb.io-এ যোগাযোগ করুন।',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -406,6 +480,8 @@ export const bn: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'নতুন টেবিল',
|
||||
new_relationship: 'নতুন সম্পর্ক',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
||||
@@ -26,6 +26,8 @@ export const de: LanguageTranslation = {
|
||||
hide_sidebar: 'Seitenleiste ausblenden',
|
||||
hide_cardinality: 'Kardinalität ausblenden',
|
||||
show_cardinality: 'Kardinalität anzeigen',
|
||||
hide_field_attributes: 'Feldattribute ausblenden',
|
||||
show_field_attributes: 'Feldattribute anzeigen',
|
||||
zoom_on_scroll: 'Zoom beim Scrollen',
|
||||
theme: 'Stil',
|
||||
show_dependencies: 'Abhängigkeiten anzeigen',
|
||||
@@ -43,9 +45,7 @@ export const de: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'Hilfe',
|
||||
docs_website: 'Dokumentation',
|
||||
visit_website: 'ChartDB Webseite',
|
||||
join_discord: 'Auf Discord beitreten',
|
||||
schedule_a_call: 'Gespräch vereinbaren',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -77,8 +77,8 @@ export const de: LanguageTranslation = {
|
||||
title: 'Mehrere Schemas',
|
||||
description:
|
||||
'{{schemasCount}} Schemas in diesem Diagramm. Derzeit angezeigt: {{formattedSchemas}}.',
|
||||
dont_show_again: 'Nicht erneut anzeigen',
|
||||
change_schema: 'Schema ändern',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'Keine',
|
||||
},
|
||||
|
||||
@@ -154,7 +154,12 @@ export const de: LanguageTranslation = {
|
||||
no_comments: 'Keine Kommentare',
|
||||
delete_field: 'Feld löschen',
|
||||
// TODO: Translate
|
||||
default_value: 'Default Value',
|
||||
no_default: 'No default',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
precision: 'Präzision',
|
||||
scale: 'Skalierung',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Indexattribute',
|
||||
@@ -215,6 +220,56 @@ 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',
|
||||
no_fields_tooltip: 'No fields defined for this custom type',
|
||||
custom_type_actions: {
|
||||
title: 'Actions',
|
||||
highlight_fields: 'Highlight Fields',
|
||||
delete_custom_type: 'Delete',
|
||||
clear_field_highlight: 'Clear Highlight',
|
||||
},
|
||||
delete_custom_type: 'Delete Type',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -225,7 +280,14 @@ export const de: LanguageTranslation = {
|
||||
undo: 'Rückgängig',
|
||||
redo: 'Wiederholen',
|
||||
reorder_diagram: 'Diagramm neu anordnen',
|
||||
|
||||
// TODO: Translate
|
||||
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||
custom_type_highlight_tooltip:
|
||||
'Highlighting "{{typeName}}" - Click to clear',
|
||||
highlight_overlapping_tables: 'Überlappende Tabellen hervorheben',
|
||||
// TODO: Translate
|
||||
filter: 'Filter Tables',
|
||||
},
|
||||
|
||||
new_diagram_dialog: {
|
||||
@@ -241,7 +303,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',
|
||||
@@ -336,6 +398,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: {
|
||||
@@ -352,6 +420,13 @@ export const de: LanguageTranslation = {
|
||||
cancel: 'Abbrechen',
|
||||
confirm: 'Ändern',
|
||||
},
|
||||
create_table_schema_dialog: {
|
||||
title: 'Neues Schema erstellen',
|
||||
description:
|
||||
'Es existieren noch keine Schemas. Erstellen Sie Ihr erstes Schema, um Ihre Tabellen zu organisieren.',
|
||||
create: 'Erstellen',
|
||||
cancel: 'Abbrechen',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'Hilf uns, uns zu verbessern!',
|
||||
@@ -370,7 +445,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
|
||||
@@ -382,7 +457,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
|
||||
@@ -409,6 +484,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: {
|
||||
|
||||
@@ -26,6 +26,8 @@ export const en = {
|
||||
hide_sidebar: 'Hide Sidebar',
|
||||
hide_cardinality: 'Hide Cardinality',
|
||||
show_cardinality: 'Show Cardinality',
|
||||
hide_field_attributes: 'Hide Field Attributes',
|
||||
show_field_attributes: 'Show Field Attributes',
|
||||
zoom_on_scroll: 'Zoom on Scroll',
|
||||
theme: 'Theme',
|
||||
show_dependencies: 'Show Dependencies',
|
||||
@@ -41,9 +43,7 @@ export const en = {
|
||||
help: {
|
||||
help: 'Help',
|
||||
docs_website: 'Docs',
|
||||
visit_website: 'Visit ChartDB',
|
||||
join_discord: 'Join us on Discord',
|
||||
schedule_a_call: 'Talk with us!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -75,8 +75,7 @@ export const en = {
|
||||
title: 'Multiple Schemas',
|
||||
description:
|
||||
'{{schemasCount}} schemas in this diagram. Currently displaying: {{formattedSchemas}}.',
|
||||
dont_show_again: "Don't show again",
|
||||
change_schema: 'Change',
|
||||
show_me: 'Show me',
|
||||
none: 'none',
|
||||
},
|
||||
|
||||
@@ -146,8 +145,12 @@ export const en = {
|
||||
title: 'Field Attributes',
|
||||
unique: 'Unique',
|
||||
character_length: 'Max Length',
|
||||
precision: 'Precision',
|
||||
scale: 'Scale',
|
||||
comments: 'Comments',
|
||||
no_comments: 'No comments',
|
||||
default_value: 'Default Value',
|
||||
no_default: 'No default',
|
||||
delete_field: 'Delete Field',
|
||||
},
|
||||
index_actions: {
|
||||
@@ -208,6 +211,55 @@ 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',
|
||||
no_fields_tooltip: 'No fields defined for this custom type',
|
||||
custom_type_actions: {
|
||||
title: 'Actions',
|
||||
highlight_fields: 'Highlight Fields',
|
||||
clear_field_highlight: 'Clear Highlight',
|
||||
delete_custom_type: 'Delete',
|
||||
},
|
||||
delete_custom_type: 'Delete Type',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -219,6 +271,10 @@ export const en = {
|
||||
redo: 'Redo',
|
||||
reorder_diagram: 'Reorder Diagram',
|
||||
highlight_overlapping_tables: 'Highlight Overlapping Tables',
|
||||
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||
custom_type_highlight_tooltip:
|
||||
'Highlighting "{{typeName}}" - Click to clear',
|
||||
filter: 'Filter Tables',
|
||||
},
|
||||
|
||||
new_diagram_dialog: {
|
||||
@@ -234,7 +290,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',
|
||||
@@ -328,6 +384,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: {
|
||||
@@ -345,6 +406,14 @@ export const en = {
|
||||
confirm: 'Change',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: 'Create New Schema',
|
||||
description:
|
||||
'No schemas exist yet. Create your first schema to organize your tables.',
|
||||
create: 'Create',
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'Help us improve!',
|
||||
description:
|
||||
@@ -361,7 +430,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',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -373,7 +442,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',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -400,6 +469,7 @@ export const en = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'New Table',
|
||||
new_relationship: 'New Relationship',
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
||||
@@ -24,6 +24,8 @@ export const es: LanguageTranslation = {
|
||||
view: 'Ver',
|
||||
hide_cardinality: 'Ocultar Cardinalidad',
|
||||
show_cardinality: 'Mostrar Cardinalidad',
|
||||
show_field_attributes: 'Mostrar Atributos de Campo',
|
||||
hide_field_attributes: 'Ocultar Atributos de Campo',
|
||||
show_sidebar: 'Mostrar Barra Lateral',
|
||||
hide_sidebar: 'Ocultar Barra Lateral',
|
||||
zoom_on_scroll: 'Zoom al Desplazarse',
|
||||
@@ -42,9 +44,7 @@ export const es: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'Ayuda',
|
||||
docs_website: 'Documentación',
|
||||
visit_website: 'Visitar ChartDB',
|
||||
join_discord: 'Únete a nosotros en Discord',
|
||||
schedule_a_call: '¡Habla con nosotros!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -143,7 +143,12 @@ export const es: LanguageTranslation = {
|
||||
no_comments: 'Sin comentarios',
|
||||
delete_field: 'Eliminar Campo',
|
||||
// TODO: Translate
|
||||
default_value: 'Default Value',
|
||||
no_default: 'No default',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
precision: 'Precisión',
|
||||
scale: 'Escala',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Atributos del Índice',
|
||||
@@ -203,6 +208,56 @@ 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',
|
||||
no_fields_tooltip: 'No fields defined for this custom type',
|
||||
custom_type_actions: {
|
||||
title: 'Actions',
|
||||
highlight_fields: 'Highlight Fields',
|
||||
delete_custom_type: 'Delete',
|
||||
clear_field_highlight: 'Clear Highlight',
|
||||
},
|
||||
delete_custom_type: 'Delete Type',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -213,7 +268,13 @@ export const es: LanguageTranslation = {
|
||||
undo: 'Deshacer',
|
||||
redo: 'Rehacer',
|
||||
reorder_diagram: 'Reordenar Diagrama',
|
||||
// TODO: Translate
|
||||
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||
custom_type_highlight_tooltip:
|
||||
'Highlighting "{{typeName}}" - Click to clear',
|
||||
highlight_overlapping_tables: 'Resaltar tablas superpuestas',
|
||||
// TODO: Translate
|
||||
filter: 'Filter Tables',
|
||||
},
|
||||
|
||||
new_diagram_dialog: {
|
||||
@@ -229,7 +290,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',
|
||||
@@ -325,6 +386,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: {
|
||||
@@ -341,6 +408,13 @@ export const es: LanguageTranslation = {
|
||||
cancel: 'Cancelar',
|
||||
confirm: 'Cambiar',
|
||||
},
|
||||
create_table_schema_dialog: {
|
||||
title: 'Crear Nuevo Esquema',
|
||||
description:
|
||||
'Aún no existen esquemas. Crea tu primer esquema para organizar tus tablas.',
|
||||
create: 'Crear',
|
||||
cancel: 'Cancelar',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: '¡Ayúdanos a mejorar!',
|
||||
@@ -354,8 +428,8 @@ export const es: LanguageTranslation = {
|
||||
title: 'Múltiples Esquemas',
|
||||
description:
|
||||
'{{schemasCount}} esquemas en este diagrama. Actualmente mostrando: {{formattedSchemas}}.',
|
||||
dont_show_again: 'No mostrar de nuevo',
|
||||
change_schema: 'Cambiar',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'nada',
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -368,7 +442,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
|
||||
@@ -380,7 +454,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
|
||||
@@ -407,6 +481,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: {
|
||||
|
||||
@@ -26,6 +26,8 @@ export const fr: LanguageTranslation = {
|
||||
hide_sidebar: 'Cacher la Barre Latérale',
|
||||
hide_cardinality: 'Cacher la Cardinalité',
|
||||
show_cardinality: 'Afficher la Cardinalité',
|
||||
hide_field_attributes: 'Masquer les Attributs de Champ',
|
||||
show_field_attributes: 'Afficher les Attributs de Champ',
|
||||
zoom_on_scroll: 'Zoom sur le Défilement',
|
||||
theme: 'Thème',
|
||||
show_dependencies: 'Afficher les Dépendances',
|
||||
@@ -41,9 +43,7 @@ export const fr: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'Aide',
|
||||
docs_website: 'Documentation',
|
||||
visit_website: 'Visitez ChartDB',
|
||||
join_discord: 'Rejoignez-nous sur Discord',
|
||||
schedule_a_call: 'Parlez avec nous !',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -141,7 +141,12 @@ export const fr: LanguageTranslation = {
|
||||
no_comments: 'Pas de commentaires',
|
||||
delete_field: 'Supprimer le Champ',
|
||||
// TODO: Translate
|
||||
default_value: 'Default Value',
|
||||
no_default: 'No default',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
precision: 'Précision',
|
||||
scale: 'Échelle',
|
||||
},
|
||||
index_actions: {
|
||||
title: "Attributs de l'Index",
|
||||
@@ -201,6 +206,56 @@ 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',
|
||||
no_fields_tooltip: 'No fields defined for this custom type',
|
||||
custom_type_actions: {
|
||||
title: 'Actions',
|
||||
highlight_fields: 'Highlight Fields',
|
||||
delete_custom_type: 'Delete',
|
||||
clear_field_highlight: 'Clear Highlight',
|
||||
},
|
||||
delete_custom_type: 'Delete Type',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -211,7 +266,13 @@ export const fr: LanguageTranslation = {
|
||||
undo: 'Annuler',
|
||||
redo: 'Rétablir',
|
||||
reorder_diagram: 'Réorganiser le Diagramme',
|
||||
// TODO: Translate
|
||||
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||
custom_type_highlight_tooltip:
|
||||
'Highlighting "{{typeName}}" - Click to clear',
|
||||
highlight_overlapping_tables: 'Surligner les tables chevauchées',
|
||||
// TODO: Translate
|
||||
filter: 'Filter Tables',
|
||||
},
|
||||
|
||||
new_diagram_dialog: {
|
||||
@@ -227,7 +288,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',
|
||||
@@ -288,14 +349,20 @@ 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: {
|
||||
title: 'Schémas Multiples',
|
||||
description:
|
||||
'{{schemasCount}} schémas dans ce diagramme. Actuellement affiché(s) : {{formattedSchemas}}.',
|
||||
dont_show_again: 'Ne plus afficher',
|
||||
change_schema: 'Changer',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'Aucun',
|
||||
},
|
||||
|
||||
@@ -321,6 +388,13 @@ export const fr: LanguageTranslation = {
|
||||
cancel: 'Annuler',
|
||||
confirm: 'Modifier',
|
||||
},
|
||||
create_table_schema_dialog: {
|
||||
title: 'Créer un Nouveau Schéma',
|
||||
description:
|
||||
"Aucun schéma n'existe encore. Créez votre premier schéma pour organiser vos tables.",
|
||||
create: 'Créer',
|
||||
cancel: 'Annuler',
|
||||
},
|
||||
|
||||
create_relationship_dialog: {
|
||||
title: 'Créer une Relation',
|
||||
@@ -365,7 +439,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: {
|
||||
@@ -376,7 +450,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: {
|
||||
@@ -404,6 +478,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: {
|
||||
|
||||
@@ -26,6 +26,8 @@ export const gu: LanguageTranslation = {
|
||||
hide_sidebar: 'સાઇડબાર છુપાવો',
|
||||
hide_cardinality: 'કાર્ડિનાલિટી છુપાવો',
|
||||
show_cardinality: 'કાર્ડિનાલિટી બતાવો',
|
||||
hide_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ છુપાવો',
|
||||
show_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ બતાવો',
|
||||
zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો',
|
||||
theme: 'થિમ',
|
||||
show_dependencies: 'નિર્ભરતાઓ બતાવો',
|
||||
@@ -43,9 +45,7 @@ export const gu: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'મદદ',
|
||||
docs_website: 'દસ્તાવેજીકરણ',
|
||||
visit_website: 'ChartDB વેબસાઇટ પર જાઓ',
|
||||
join_discord: 'અમારા Discordમાં જોડાઓ',
|
||||
schedule_a_call: 'અમારી સાથે વાત કરો!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -77,8 +77,8 @@ export const gu: LanguageTranslation = {
|
||||
title: 'કઈંક વધારે સ્કીમા',
|
||||
description:
|
||||
'{{schemasCount}} સ્કીમા આ ડાયાગ્રામમાં છે. હાલમાં દર્શાવેલ છે: {{formattedSchemas}}.',
|
||||
dont_show_again: 'ફરીથી ન બતાવો',
|
||||
change_schema: 'બદલો',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'કઈ નહીં',
|
||||
},
|
||||
|
||||
@@ -154,7 +154,12 @@ export const gu: LanguageTranslation = {
|
||||
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
|
||||
delete_field: 'ફીલ્ડ કાઢી નાખો',
|
||||
// TODO: Translate
|
||||
default_value: 'Default Value',
|
||||
no_default: 'No default',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
precision: 'ચોકસાઈ',
|
||||
scale: 'માપ',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'ઇન્ડેક્સ લક્ષણો',
|
||||
@@ -214,6 +219,56 @@ 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',
|
||||
no_fields_tooltip: 'No fields defined for this custom type',
|
||||
custom_type_actions: {
|
||||
title: 'Actions',
|
||||
highlight_fields: 'Highlight Fields',
|
||||
delete_custom_type: 'Delete',
|
||||
clear_field_highlight: 'Clear Highlight',
|
||||
},
|
||||
delete_custom_type: 'Delete Type',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -224,7 +279,13 @@ export const gu: LanguageTranslation = {
|
||||
undo: 'અનડુ',
|
||||
redo: 'રીડુ',
|
||||
reorder_diagram: 'ડાયાગ્રામ ફરીથી વ્યવસ્થિત કરો',
|
||||
// TODO: Translate
|
||||
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||
custom_type_highlight_tooltip:
|
||||
'Highlighting "{{typeName}}" - Click to clear',
|
||||
highlight_overlapping_tables: 'ઓવરલેપ કરતો ટેબલ હાઇલાઇટ કરો',
|
||||
// TODO: Translate
|
||||
filter: 'Filter Tables',
|
||||
},
|
||||
|
||||
new_diagram_dialog: {
|
||||
@@ -239,7 +300,7 @@ export const gu: LanguageTranslation = {
|
||||
title: 'તમારું ડેટાબેસ આયાત કરો',
|
||||
database_edition: 'ડેટાબેસ આવૃત્તિ:',
|
||||
step_1: 'તમારા ડેટાબેસમાં આ સ્ક્રિપ્ટ ચલાવો:',
|
||||
step_2: 'સ્ક્રિપ્ટનો પરિણામ અહીં પેસ્ટ કરો:',
|
||||
step_2: 'સ્ક્રિપ્ટનો પરિણામ અહીં પેસ્ટ કરો →',
|
||||
script_results_placeholder: 'સ્ક્રિપ્ટના પરિણામ અહીં...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS સૂચનાઓ',
|
||||
@@ -333,6 +394,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: {
|
||||
@@ -350,6 +417,14 @@ export const gu: LanguageTranslation = {
|
||||
confirm: 'બદલો',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: 'નવું સ્કીમા બનાવો',
|
||||
description:
|
||||
'હજી સુધી કોઈ સ્કીમા અસ્તિત્વમાં નથી. તમારા ટેબલ્સ ને વ્યવસ્થિત કરવા માટે તમારું પહેલું સ્કીમા બનાવો.',
|
||||
create: 'બનાવો',
|
||||
cancel: 'રદ કરો',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'અમને સુધારવામાં મદદ કરો!',
|
||||
description:
|
||||
@@ -367,7 +442,7 @@ export const gu: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'ડાયાગ્રામ નિકાસમાં ભૂલ',
|
||||
description:
|
||||
'કશુક તો ખોટું થયું. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
|
||||
'કશુક તો ખોટું થયું. મદદ જોઈએ? support@chartdb.io પર સંપર્ક કરો.',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -379,7 +454,7 @@ export const gu: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'ડાયાગ્રામ આયાતમાં ભૂલ',
|
||||
description:
|
||||
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
|
||||
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? support@chartdb.io પર સંપર્ક કરો.',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -406,6 +481,8 @@ export const gu: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'નવું ટેબલ',
|
||||
new_relationship: 'નવો સંબંધ',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
||||
@@ -26,6 +26,8 @@ export const hi: LanguageTranslation = {
|
||||
hide_sidebar: 'साइडबार छिपाएँ',
|
||||
hide_cardinality: 'कार्डिनैलिटी छिपाएँ',
|
||||
show_cardinality: 'कार्डिनैलिटी दिखाएँ',
|
||||
hide_field_attributes: 'फ़ील्ड विशेषताएँ छिपाएँ',
|
||||
show_field_attributes: 'फ़ील्ड विशेषताएँ दिखाएँ',
|
||||
zoom_on_scroll: 'स्क्रॉल पर ज़ूम',
|
||||
theme: 'थीम',
|
||||
show_dependencies: 'निर्भरता दिखाएँ',
|
||||
@@ -42,9 +44,7 @@ export const hi: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'मदद',
|
||||
docs_website: 'દસ્તાવેજીકરણ',
|
||||
visit_website: 'ChartDB वेबसाइट पर जाएँ',
|
||||
join_discord: 'हमसे Discord पर जुड़ें',
|
||||
schedule_a_call: 'हमसे बात करें!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -76,8 +76,8 @@ export const hi: LanguageTranslation = {
|
||||
title: 'एकाधिक स्कीमा',
|
||||
description:
|
||||
'{{schemasCount}} स्कीमा इस आरेख में हैं। वर्तमान में प्रदर्शित: {{formattedSchemas}}।',
|
||||
dont_show_again: 'फिर से न दिखाएँ',
|
||||
change_schema: 'बदलें',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'कोई नहीं',
|
||||
},
|
||||
|
||||
@@ -153,7 +153,12 @@ export const hi: LanguageTranslation = {
|
||||
no_comments: 'कोई टिप्पणी नहीं',
|
||||
delete_field: 'फ़ील्ड हटाएँ',
|
||||
// TODO: Translate
|
||||
default_value: 'Default Value',
|
||||
no_default: 'No default',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
precision: 'Precision',
|
||||
scale: 'Scale',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'सूचकांक विशेषताएँ',
|
||||
@@ -214,6 +219,56 @@ 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',
|
||||
no_fields_tooltip: 'No fields defined for this custom type',
|
||||
custom_type_actions: {
|
||||
title: 'Actions',
|
||||
highlight_fields: 'Highlight Fields',
|
||||
delete_custom_type: 'Delete',
|
||||
clear_field_highlight: 'Clear Highlight',
|
||||
},
|
||||
delete_custom_type: 'Delete Type',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -224,7 +279,13 @@ export const hi: LanguageTranslation = {
|
||||
undo: 'पूर्ववत करें',
|
||||
redo: 'पुनः करें',
|
||||
reorder_diagram: 'आरेख पुनः व्यवस्थित करें',
|
||||
// TODO: Translate
|
||||
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||
custom_type_highlight_tooltip:
|
||||
'Highlighting "{{typeName}}" - Click to clear',
|
||||
highlight_overlapping_tables: 'ओवरलैपिंग तालिकाओं को हाइलाइट करें',
|
||||
// TODO: Translate
|
||||
filter: 'Filter Tables',
|
||||
},
|
||||
|
||||
new_diagram_dialog: {
|
||||
@@ -240,7 +301,7 @@ export const hi: LanguageTranslation = {
|
||||
title: 'अपना डेटाबेस आयात करें',
|
||||
database_edition: 'डेटाबेस संस्करण:',
|
||||
step_1: 'अपने डेटाबेस में यह स्क्रिप्ट चलाएँ:',
|
||||
step_2: 'यहाँ स्क्रिप्ट का परिणाम पेस्ट करें:',
|
||||
step_2: 'यहाँ स्क्रिप्ट का परिणाम पेस्ट करें →',
|
||||
script_results_placeholder: 'स्क्रिप्ट के परिणाम यहाँ...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS निर्देश',
|
||||
@@ -336,6 +397,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: {
|
||||
@@ -353,6 +420,14 @@ export const hi: LanguageTranslation = {
|
||||
confirm: 'बदलें',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: 'नया स्कीमा बनाएं',
|
||||
description:
|
||||
'अभी तक कोई स्कीमा मौजूद नहीं है। अपनी तालिकाओं को व्यवस्थित करने के लिए अपना पहला स्कीमा बनाएं।',
|
||||
create: 'बनाएं',
|
||||
cancel: 'रद्द करें',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'हमें सुधारने में मदद करें!',
|
||||
description:
|
||||
@@ -370,7 +445,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
|
||||
@@ -382,7 +457,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
|
||||
@@ -409,6 +484,8 @@ export const hi: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'नई तालिका',
|
||||
new_relationship: 'नया संबंध',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
||||
503
src/i18n/locales/hr.ts
Normal file
@@ -0,0 +1,503 @@
|
||||
import type { LanguageMetadata, LanguageTranslation } from '../types';
|
||||
|
||||
export const hr: LanguageTranslation = {
|
||||
translation: {
|
||||
menu: {
|
||||
file: {
|
||||
file: 'Datoteka',
|
||||
new: 'Nova',
|
||||
open: 'Otvori',
|
||||
save: 'Spremi',
|
||||
import: 'Uvezi',
|
||||
export_sql: 'Izvezi SQL',
|
||||
export_as: 'Izvezi kao',
|
||||
delete_diagram: 'Izbriši dijagram',
|
||||
exit: 'Izađi',
|
||||
},
|
||||
edit: {
|
||||
edit: 'Uredi',
|
||||
undo: 'Poništi',
|
||||
redo: 'Ponovi',
|
||||
clear: 'Očisti',
|
||||
},
|
||||
view: {
|
||||
view: 'Prikaz',
|
||||
show_sidebar: 'Prikaži bočnu traku',
|
||||
hide_sidebar: 'Sakrij bočnu traku',
|
||||
hide_cardinality: 'Sakrij kardinalnost',
|
||||
show_cardinality: 'Prikaži kardinalnost',
|
||||
hide_field_attributes: 'Sakrij atribute polja',
|
||||
show_field_attributes: 'Prikaži atribute polja',
|
||||
zoom_on_scroll: 'Zumiranje pri skrolanju',
|
||||
theme: 'Tema',
|
||||
show_dependencies: 'Prikaži ovisnosti',
|
||||
hide_dependencies: 'Sakrij ovisnosti',
|
||||
show_minimap: 'Prikaži mini kartu',
|
||||
hide_minimap: 'Sakrij mini kartu',
|
||||
},
|
||||
backup: {
|
||||
backup: 'Sigurnosna kopija',
|
||||
export_diagram: 'Izvezi dijagram',
|
||||
restore_diagram: 'Vrati dijagram',
|
||||
},
|
||||
help: {
|
||||
help: 'Pomoć',
|
||||
docs_website: 'Dokumentacija',
|
||||
join_discord: 'Pridružite nam se na Discordu',
|
||||
},
|
||||
},
|
||||
|
||||
delete_diagram_alert: {
|
||||
title: 'Izbriši dijagram',
|
||||
description:
|
||||
'Ova radnja se ne može poništiti. Ovo će trajno izbrisati dijagram.',
|
||||
cancel: 'Odustani',
|
||||
delete: 'Izbriši',
|
||||
},
|
||||
|
||||
clear_diagram_alert: {
|
||||
title: 'Očisti dijagram',
|
||||
description:
|
||||
'Ova radnja se ne može poništiti. Ovo će trajno izbrisati sve podatke u dijagramu.',
|
||||
cancel: 'Odustani',
|
||||
clear: 'Očisti',
|
||||
},
|
||||
|
||||
reorder_diagram_alert: {
|
||||
title: 'Preuredi dijagram',
|
||||
description:
|
||||
'Ova radnja će preurediti sve tablice u dijagramu. Želite li nastaviti?',
|
||||
reorder: 'Preuredi',
|
||||
cancel: 'Odustani',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'Više shema',
|
||||
description:
|
||||
'{{schemasCount}} shema u ovom dijagramu. Trenutno prikazano: {{formattedSchemas}}.',
|
||||
show_me: 'Prikaži mi',
|
||||
none: 'nijedna',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'Kopiranje neuspješno',
|
||||
description: 'Međuspremnik nije podržan.',
|
||||
},
|
||||
failed: {
|
||||
title: 'Kopiranje neuspješno',
|
||||
description: 'Nešto je pošlo po zlu. Molimo pokušajte ponovno.',
|
||||
},
|
||||
},
|
||||
|
||||
theme: {
|
||||
system: 'Sustav',
|
||||
light: 'Svijetla',
|
||||
dark: 'Tamna',
|
||||
},
|
||||
|
||||
zoom: {
|
||||
on: 'Uključeno',
|
||||
off: 'Isključeno',
|
||||
},
|
||||
|
||||
last_saved: 'Zadnje spremljeno',
|
||||
saved: 'Spremljeno',
|
||||
loading_diagram: 'Učitavanje dijagrama...',
|
||||
deselect_all: 'Odznači sve',
|
||||
select_all: 'Označi sve',
|
||||
clear: 'Očisti',
|
||||
show_more: 'Prikaži više',
|
||||
show_less: 'Prikaži manje',
|
||||
copy_to_clipboard: 'Kopiraj u međuspremnik',
|
||||
copied: 'Kopirano!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'Shema:',
|
||||
filter_by_schema: 'Filtriraj po shemi',
|
||||
search_schema: 'Pretraži shemu...',
|
||||
no_schemas_found: 'Nema pronađenih shema.',
|
||||
view_all_options: 'Prikaži sve opcije...',
|
||||
tables_section: {
|
||||
tables: 'Tablice',
|
||||
add_table: 'Dodaj tablicu',
|
||||
filter: 'Filtriraj',
|
||||
collapse: 'Sažmi sve',
|
||||
clear: 'Očisti filter',
|
||||
no_results:
|
||||
'Nema pronađenih tablica koje odgovaraju vašem filteru.',
|
||||
show_list: 'Prikaži popis tablica',
|
||||
show_dbml: 'Prikaži DBML uređivač',
|
||||
|
||||
table: {
|
||||
fields: 'Polja',
|
||||
nullable: 'Može biti null?',
|
||||
primary_key: 'Primarni ključ',
|
||||
indexes: 'Indeksi',
|
||||
comments: 'Komentari',
|
||||
no_comments: 'Nema komentara',
|
||||
add_field: 'Dodaj polje',
|
||||
add_index: 'Dodaj indeks',
|
||||
index_select_fields: 'Odaberi polja',
|
||||
no_types_found: 'Nema pronađenih tipova',
|
||||
field_name: 'Naziv',
|
||||
field_type: 'Tip',
|
||||
field_actions: {
|
||||
title: 'Atributi polja',
|
||||
unique: 'Jedinstven',
|
||||
character_length: 'Maksimalna dužina',
|
||||
precision: 'Preciznost',
|
||||
scale: 'Skala',
|
||||
comments: 'Komentari',
|
||||
no_comments: 'Nema komentara',
|
||||
default_value: 'Zadana vrijednost',
|
||||
no_default: 'Nema zadane vrijednosti',
|
||||
delete_field: 'Izbriši polje',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Atributi indeksa',
|
||||
name: 'Naziv',
|
||||
unique: 'Jedinstven',
|
||||
delete_index: 'Izbriši indeks',
|
||||
},
|
||||
table_actions: {
|
||||
title: 'Radnje nad tablicom',
|
||||
change_schema: 'Promijeni shemu',
|
||||
add_field: 'Dodaj polje',
|
||||
add_index: 'Dodaj indeks',
|
||||
duplicate_table: 'Dupliciraj tablicu',
|
||||
delete_table: 'Izbriši tablicu',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'Nema tablica',
|
||||
description: 'Stvorite tablicu za početak',
|
||||
},
|
||||
},
|
||||
relationships_section: {
|
||||
relationships: 'Veze',
|
||||
filter: 'Filtriraj',
|
||||
add_relationship: 'Dodaj vezu',
|
||||
collapse: 'Sažmi sve',
|
||||
relationship: {
|
||||
primary: 'Primarna tablica',
|
||||
foreign: 'Referentna tablica',
|
||||
cardinality: 'Kardinalnost',
|
||||
delete_relationship: 'Izbriši',
|
||||
relationship_actions: {
|
||||
title: 'Radnje',
|
||||
delete_relationship: 'Izbriši',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'Nema veza',
|
||||
description: 'Stvorite vezu za povezivanje tablica',
|
||||
},
|
||||
},
|
||||
dependencies_section: {
|
||||
dependencies: 'Ovisnosti',
|
||||
filter: 'Filtriraj',
|
||||
collapse: 'Sažmi sve',
|
||||
dependency: {
|
||||
table: 'Tablica',
|
||||
dependent_table: 'Ovisni pogled',
|
||||
delete_dependency: 'Izbriši',
|
||||
dependency_actions: {
|
||||
title: 'Radnje',
|
||||
delete_dependency: 'Izbriši',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'Nema ovisnosti',
|
||||
description: 'Stvorite pogled za početak',
|
||||
},
|
||||
},
|
||||
|
||||
areas_section: {
|
||||
areas: 'Područja',
|
||||
add_area: 'Dodaj područje',
|
||||
filter: 'Filtriraj',
|
||||
clear: 'Očisti filter',
|
||||
no_results:
|
||||
'Nema pronađenih područja koja odgovaraju vašem filteru.',
|
||||
|
||||
area: {
|
||||
area_actions: {
|
||||
title: 'Radnje nad područjem',
|
||||
edit_name: 'Uredi naziv',
|
||||
delete_area: 'Izbriši područje',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'Nema područja',
|
||||
description: 'Stvorite područje za početak',
|
||||
},
|
||||
},
|
||||
|
||||
custom_types_section: {
|
||||
custom_types: 'Prilagođeni tipovi',
|
||||
filter: 'Filtriraj',
|
||||
clear: 'Očisti filter',
|
||||
no_results:
|
||||
'Nema pronađenih prilagođenih tipova koji odgovaraju vašem filteru.',
|
||||
empty_state: {
|
||||
title: 'Nema prilagođenih tipova',
|
||||
description:
|
||||
'Prilagođeni tipovi će se pojaviti ovdje kada budu dostupni u vašoj bazi podataka',
|
||||
},
|
||||
custom_type: {
|
||||
kind: 'Vrsta',
|
||||
enum_values: 'Enum vrijednosti',
|
||||
composite_fields: 'Polja',
|
||||
no_fields: 'Nema definiranih polja',
|
||||
field_name_placeholder: 'Naziv polja',
|
||||
field_type_placeholder: 'Odaberi tip',
|
||||
add_field: 'Dodaj polje',
|
||||
no_fields_tooltip:
|
||||
'Nema definiranih polja za ovaj prilagođeni tip',
|
||||
custom_type_actions: {
|
||||
title: 'Radnje',
|
||||
highlight_fields: 'Istakni polja',
|
||||
clear_field_highlight: 'Ukloni isticanje',
|
||||
delete_custom_type: 'Izbriši',
|
||||
},
|
||||
delete_custom_type: 'Izbriši tip',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
zoom_in: 'Uvećaj',
|
||||
zoom_out: 'Smanji',
|
||||
save: 'Spremi',
|
||||
show_all: 'Prikaži sve',
|
||||
undo: 'Poništi',
|
||||
redo: 'Ponovi',
|
||||
reorder_diagram: 'Preuredi dijagram',
|
||||
highlight_overlapping_tables: 'Istakni preklapajuće tablice',
|
||||
clear_custom_type_highlight: 'Ukloni isticanje za "{{typeName}}"',
|
||||
custom_type_highlight_tooltip:
|
||||
'Isticanje "{{typeName}}" - Kliknite za uklanjanje',
|
||||
filter: 'Filtriraj tablice',
|
||||
},
|
||||
|
||||
new_diagram_dialog: {
|
||||
database_selection: {
|
||||
title: 'Koja je vaša baza podataka?',
|
||||
description:
|
||||
'Svaka baza podataka ima svoje jedinstvene značajke i mogućnosti.',
|
||||
check_examples_long: 'Pogledaj primjere',
|
||||
check_examples_short: 'Primjeri',
|
||||
},
|
||||
|
||||
import_database: {
|
||||
title: 'Uvezite svoju bazu podataka',
|
||||
database_edition: 'Verzija baze podataka:',
|
||||
step_1: 'Pokrenite ovu skriptu u svojoj bazi podataka:',
|
||||
step_2: 'Zalijepite rezultat skripte u ovaj dio →',
|
||||
script_results_placeholder: 'Rezultati skripte ovdje...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS upute',
|
||||
title: 'Upute',
|
||||
step_1: 'Idite na Tools > Options > Query Results > SQL Server.',
|
||||
step_2: 'Ako koristite "Results to Grid," promijenite Maximum Characters Retrieved za Non-XML podatke (postavite na 9999999).',
|
||||
},
|
||||
instructions_link: 'Trebate pomoć? Pogledajte kako',
|
||||
check_script_result: 'Provjeri rezultat skripte',
|
||||
},
|
||||
|
||||
cancel: 'Odustani',
|
||||
import_from_file: 'Uvezi iz datoteke',
|
||||
back: 'Natrag',
|
||||
empty_diagram: 'Prazan dijagram',
|
||||
continue: 'Nastavi',
|
||||
import: 'Uvezi',
|
||||
},
|
||||
|
||||
open_diagram_dialog: {
|
||||
title: 'Otvori dijagram',
|
||||
description: 'Odaberite dijagram za otvaranje iz popisa ispod.',
|
||||
table_columns: {
|
||||
name: 'Naziv',
|
||||
created_at: 'Stvoreno',
|
||||
last_modified: 'Zadnje izmijenjeno',
|
||||
tables_count: 'Tablice',
|
||||
},
|
||||
cancel: 'Odustani',
|
||||
open: 'Otvori',
|
||||
},
|
||||
|
||||
export_sql_dialog: {
|
||||
title: 'Izvezi SQL',
|
||||
description:
|
||||
'Izvezite shemu vašeg dijagrama u {{databaseType}} skriptu',
|
||||
close: 'Zatvori',
|
||||
loading: {
|
||||
text: 'AI generira SQL za {{databaseType}}...',
|
||||
description: 'Ovo bi trebalo potrajati do 30 sekundi.',
|
||||
},
|
||||
error: {
|
||||
message:
|
||||
'Greška pri generiranju SQL skripte. Molimo pokušajte ponovno kasnije ili <0>kontaktirajte nas</0>.',
|
||||
description:
|
||||
'Slobodno koristite svoj OPENAI_TOKEN, pogledajte priručnik <0>ovdje</0>.',
|
||||
},
|
||||
},
|
||||
|
||||
create_relationship_dialog: {
|
||||
title: 'Kreiraj vezu',
|
||||
primary_table: 'Primarna tablica',
|
||||
primary_field: 'Primarno polje',
|
||||
referenced_table: 'Referentna tablica',
|
||||
referenced_field: 'Referentno polje',
|
||||
primary_table_placeholder: 'Odaberi tablicu',
|
||||
primary_field_placeholder: 'Odaberi polje',
|
||||
referenced_table_placeholder: 'Odaberi tablicu',
|
||||
referenced_field_placeholder: 'Odaberi polje',
|
||||
no_tables_found: 'Nema pronađenih tablica',
|
||||
no_fields_found: 'Nema pronađenih polja',
|
||||
create: 'Kreiraj',
|
||||
cancel: 'Odustani',
|
||||
},
|
||||
|
||||
import_database_dialog: {
|
||||
title: 'Uvezi u trenutni dijagram',
|
||||
override_alert: {
|
||||
title: 'Uvezi bazu podataka',
|
||||
content: {
|
||||
alert: 'Uvoz ovog dijagrama će utjecati na postojeće tablice i veze.',
|
||||
new_tables:
|
||||
'<bold>{{newTablesNumber}}</bold> novih tablica će biti dodano.',
|
||||
new_relationships:
|
||||
'<bold>{{newRelationshipsNumber}}</bold> novih veza će biti stvoreno.',
|
||||
tables_override:
|
||||
'<bold>{{tablesOverrideNumber}}</bold> tablica će biti prepisano.',
|
||||
proceed: 'Želite li nastaviti?',
|
||||
},
|
||||
import: 'Uvezi',
|
||||
cancel: 'Odustani',
|
||||
},
|
||||
},
|
||||
|
||||
export_image_dialog: {
|
||||
title: 'Izvezi sliku',
|
||||
description: 'Odaberite faktor veličine za izvoz:',
|
||||
scale_1x: '1x Obično',
|
||||
scale_2x: '2x (Preporučeno)',
|
||||
scale_3x: '3x',
|
||||
scale_4x: '4x',
|
||||
cancel: 'Odustani',
|
||||
export: 'Izvezi',
|
||||
advanced_options: 'Napredne opcije',
|
||||
pattern: 'Uključi pozadinski uzorak',
|
||||
pattern_description: 'Dodaj suptilni mrežni uzorak u pozadinu.',
|
||||
transparent: 'Prozirna pozadina',
|
||||
transparent_description: 'Ukloni boju pozadine iz slike.',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
title: 'Odaberi shemu',
|
||||
description:
|
||||
'Trenutno je prikazano više shema. Odaberite jednu za novu tablicu.',
|
||||
cancel: 'Odustani',
|
||||
confirm: 'Potvrdi',
|
||||
},
|
||||
|
||||
update_table_schema_dialog: {
|
||||
title: 'Promijeni shemu',
|
||||
description: 'Ažuriraj shemu tablice "{{tableName}}"',
|
||||
cancel: 'Odustani',
|
||||
confirm: 'Promijeni',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: 'Stvori novu shemu',
|
||||
description:
|
||||
'Još ne postoje sheme. Stvorite svoju prvu shemu za organiziranje tablica.',
|
||||
create: 'Stvori',
|
||||
cancel: 'Odustani',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'Pomozite nam da se poboljšamo!',
|
||||
description:
|
||||
'Želite li nam dati zvjezdicu na GitHubu? Samo je jedan klik!',
|
||||
close: 'Ne sada',
|
||||
confirm: 'Naravno!',
|
||||
},
|
||||
export_diagram_dialog: {
|
||||
title: 'Izvezi dijagram',
|
||||
description: 'Odaberite format za izvoz:',
|
||||
format_json: 'JSON',
|
||||
cancel: 'Odustani',
|
||||
export: 'Izvezi',
|
||||
error: {
|
||||
title: 'Greška pri izvozu dijagrama',
|
||||
description:
|
||||
'Nešto je pošlo po zlu. Trebate pomoć? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
|
||||
import_diagram_dialog: {
|
||||
title: 'Uvezi dijagram',
|
||||
description: 'Uvezite dijagram iz JSON datoteke.',
|
||||
cancel: 'Odustani',
|
||||
import: 'Uvezi',
|
||||
error: {
|
||||
title: 'Greška pri uvozu dijagrama',
|
||||
description:
|
||||
'JSON dijagrama je nevažeći. Molimo provjerite JSON i pokušajte ponovno. Trebate pomoć? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Uvezi primjer DBML-a',
|
||||
title: 'Uvezi DBML',
|
||||
description: 'Uvezite shemu baze podataka iz DBML formata.',
|
||||
import: 'Uvezi',
|
||||
cancel: 'Odustani',
|
||||
skip_and_empty: 'Preskoči i isprazni',
|
||||
show_example: 'Prikaži primjer',
|
||||
error: {
|
||||
title: 'Greška pri uvozu DBML-a',
|
||||
description:
|
||||
'Neuspješno parsiranje DBML-a. Molimo provjerite sintaksu.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: 'Jedan na jedan',
|
||||
one_to_many: 'Jedan na više',
|
||||
many_to_one: 'Više na jedan',
|
||||
many_to_many: 'Više na više',
|
||||
},
|
||||
|
||||
canvas_context_menu: {
|
||||
new_table: 'Nova tablica',
|
||||
new_relationship: 'Nova veza',
|
||||
new_area: 'Novo područje',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
edit_table: 'Uredi tablicu',
|
||||
duplicate_table: 'Dupliciraj tablicu',
|
||||
delete_table: 'Izbriši tablicu',
|
||||
add_relationship: 'Dodaj vezu',
|
||||
},
|
||||
|
||||
snap_to_grid_tooltip: 'Priljepljivanje na mrežu (Drži {{key}})',
|
||||
|
||||
tool_tips: {
|
||||
double_click_to_edit: 'Dvostruki klik za uređivanje',
|
||||
},
|
||||
|
||||
language_select: {
|
||||
change_language: 'Jezik',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const hrMetadata: LanguageMetadata = {
|
||||
name: 'Croatian',
|
||||
nativeName: 'Hrvatski',
|
||||
code: 'hr',
|
||||
};
|
||||
@@ -26,6 +26,8 @@ export const id_ID: LanguageTranslation = {
|
||||
hide_sidebar: 'Sembunyikan Sidebar',
|
||||
hide_cardinality: 'Sembunyikan Kardinalitas',
|
||||
show_cardinality: 'Tampilkan Kardinalitas',
|
||||
hide_field_attributes: 'Sembunyikan Atribut Kolom',
|
||||
show_field_attributes: 'Tampilkan Atribut Kolom',
|
||||
zoom_on_scroll: 'Perbesar saat Scroll',
|
||||
theme: 'Tema',
|
||||
show_dependencies: 'Tampilkan Dependensi',
|
||||
@@ -41,10 +43,8 @@ export const id_ID: LanguageTranslation = {
|
||||
},
|
||||
help: {
|
||||
help: 'Bantuan',
|
||||
docs_website: 'દસ્તાવેજીકરણ',
|
||||
visit_website: 'Kunjungi ChartDB',
|
||||
docs_website: 'Dokumentasi',
|
||||
join_discord: 'Bergabunglah di Discord kami',
|
||||
schedule_a_call: 'Berbicara dengan kami!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -76,8 +76,8 @@ export const id_ID: LanguageTranslation = {
|
||||
title: 'Schema Lebih dari satu',
|
||||
description:
|
||||
'{{schemasCount}} schema di diagram ini. Sedang ditampilkan: {{formattedSchemas}}.',
|
||||
dont_show_again: 'Jangan tampilkan lagi',
|
||||
change_schema: 'Ubah',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'Tidak ada',
|
||||
},
|
||||
|
||||
@@ -152,7 +152,12 @@ export const id_ID: LanguageTranslation = {
|
||||
no_comments: 'Tidak ada komentar',
|
||||
delete_field: 'Hapus Kolom',
|
||||
// TODO: Translate
|
||||
default_value: 'Default Value',
|
||||
no_default: 'No default',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
precision: 'Presisi',
|
||||
scale: 'Skala',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Atribut Indeks',
|
||||
@@ -212,6 +217,56 @@ 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',
|
||||
no_fields_tooltip: 'No fields defined for this custom type',
|
||||
custom_type_actions: {
|
||||
title: 'Actions',
|
||||
highlight_fields: 'Highlight Fields',
|
||||
delete_custom_type: 'Delete',
|
||||
clear_field_highlight: 'Clear Highlight',
|
||||
},
|
||||
delete_custom_type: 'Delete Type',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -222,7 +277,13 @@ export const id_ID: LanguageTranslation = {
|
||||
undo: 'Undo',
|
||||
redo: 'Redo',
|
||||
reorder_diagram: 'Atur Ulang Diagram',
|
||||
// TODO: Translate
|
||||
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||
custom_type_highlight_tooltip:
|
||||
'Highlighting "{{typeName}}" - Click to clear',
|
||||
highlight_overlapping_tables: 'Sorot Tabel yang Tumpang Tindih',
|
||||
// TODO: Translate
|
||||
filter: 'Filter Tables',
|
||||
},
|
||||
|
||||
new_diagram_dialog: {
|
||||
@@ -238,7 +299,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',
|
||||
@@ -331,6 +392,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: {
|
||||
@@ -348,6 +415,14 @@ export const id_ID: LanguageTranslation = {
|
||||
confirm: 'Ubah',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: 'Buat Skema Baru',
|
||||
description:
|
||||
'Belum ada skema yang tersedia. Buat skema pertama Anda untuk mengatur tabel-tabel Anda.',
|
||||
create: 'Buat',
|
||||
cancel: 'Batal',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'Bantu kami meningkatkan!',
|
||||
description:
|
||||
@@ -365,7 +440,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',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -377,7 +452,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
|
||||
@@ -405,6 +480,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: {
|
||||
|
||||
@@ -26,6 +26,8 @@ export const ja: LanguageTranslation = {
|
||||
hide_sidebar: 'サイドバーを非表示',
|
||||
hide_cardinality: 'カーディナリティを非表示',
|
||||
show_cardinality: 'カーディナリティを表示',
|
||||
hide_field_attributes: 'フィールド属性を非表示',
|
||||
show_field_attributes: 'フィールド属性を表示',
|
||||
zoom_on_scroll: 'スクロールでズーム',
|
||||
theme: 'テーマ',
|
||||
// TODO: Translate
|
||||
@@ -44,9 +46,7 @@ export const ja: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'ヘルプ',
|
||||
docs_website: 'ドキュメント',
|
||||
visit_website: 'ChartDBにアクセス',
|
||||
join_discord: 'Discordに参加',
|
||||
schedule_a_call: '話しかけてください!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -78,8 +78,8 @@ export const ja: LanguageTranslation = {
|
||||
title: '複数のスキーマ',
|
||||
description:
|
||||
'このダイアグラムには{{schemasCount}}個のスキーマがあります。現在表示中: {{formattedSchemas}}。',
|
||||
dont_show_again: '再表示しない',
|
||||
change_schema: '変更',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'なし',
|
||||
},
|
||||
|
||||
@@ -156,7 +156,12 @@ export const ja: LanguageTranslation = {
|
||||
no_comments: 'コメントがありません',
|
||||
delete_field: 'フィールドを削除',
|
||||
// TODO: Translate
|
||||
default_value: 'Default Value',
|
||||
no_default: 'No default',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
precision: '精度',
|
||||
scale: '小数点以下桁数',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'インデックス属性',
|
||||
@@ -218,6 +223,56 @@ 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',
|
||||
no_fields_tooltip: 'No fields defined for this custom type',
|
||||
custom_type_actions: {
|
||||
title: 'Actions',
|
||||
highlight_fields: 'Highlight Fields',
|
||||
delete_custom_type: 'Delete',
|
||||
clear_field_highlight: 'Clear Highlight',
|
||||
},
|
||||
delete_custom_type: 'Delete Type',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -230,6 +285,10 @@ export const ja: LanguageTranslation = {
|
||||
reorder_diagram: 'ダイアグラムを並べ替え',
|
||||
// TODO: Translate
|
||||
highlight_overlapping_tables: 'Highlight Overlapping Tables',
|
||||
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||
custom_type_highlight_tooltip:
|
||||
'Highlighting "{{typeName}}" - Click to clear', // TODO: Translate
|
||||
filter: 'Filter Tables',
|
||||
},
|
||||
|
||||
new_diagram_dialog: {
|
||||
@@ -244,7 +303,7 @@ export const ja: LanguageTranslation = {
|
||||
title: 'データベースをインポート',
|
||||
database_edition: 'データベースエディション:',
|
||||
step_1: 'このスクリプトをデータベースで実行してください:',
|
||||
step_2: 'ここにスクリプトの結果を貼り付けてください:',
|
||||
step_2: 'ここにスクリプトの結果を貼り付けてください →',
|
||||
script_results_placeholder: 'ここにスクリプトの結果...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMSの手順',
|
||||
@@ -340,6 +399,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: {
|
||||
@@ -357,6 +422,14 @@ export const ja: LanguageTranslation = {
|
||||
confirm: '変更',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: '新しいスキーマを作成',
|
||||
description:
|
||||
'スキーマがまだ存在しません。テーブルを整理するために最初のスキーマを作成してください。',
|
||||
create: '作成',
|
||||
cancel: 'キャンセル',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: '改善をサポートしてください!',
|
||||
description:
|
||||
@@ -374,7 +447,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
|
||||
@@ -386,7 +459,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
|
||||
@@ -413,6 +486,8 @@ export const ja: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: '新しいテーブル',
|
||||
new_relationship: '新しいリレーションシップ',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
||||
@@ -26,6 +26,8 @@ export const ko_KR: LanguageTranslation = {
|
||||
hide_sidebar: '사이드바 숨기기',
|
||||
hide_cardinality: '카디널리티 숨기기',
|
||||
show_cardinality: '카디널리티 보이기',
|
||||
hide_field_attributes: '필드 속성 숨기기',
|
||||
show_field_attributes: '필드 속성 보이기',
|
||||
zoom_on_scroll: '스크롤 시 확대',
|
||||
theme: '테마',
|
||||
show_dependencies: '종속성 보이기',
|
||||
@@ -42,9 +44,7 @@ export const ko_KR: LanguageTranslation = {
|
||||
help: {
|
||||
help: '도움말',
|
||||
docs_website: '선적 서류 비치',
|
||||
visit_website: 'ChartDB 사이트 방문',
|
||||
join_discord: 'Discord 가입',
|
||||
schedule_a_call: 'Talk with us!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -76,8 +76,8 @@ export const ko_KR: LanguageTranslation = {
|
||||
title: '다중 스키마',
|
||||
description:
|
||||
'현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.',
|
||||
dont_show_again: '다시 보여주지 마세요',
|
||||
change_schema: '변경',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: '없음',
|
||||
},
|
||||
|
||||
@@ -152,7 +152,12 @@ export const ko_KR: LanguageTranslation = {
|
||||
no_comments: '주석 없음',
|
||||
delete_field: '필드 삭제',
|
||||
// TODO: Translate
|
||||
default_value: 'Default Value',
|
||||
no_default: 'No default',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
precision: '정밀도',
|
||||
scale: '소수점 자릿수',
|
||||
},
|
||||
index_actions: {
|
||||
title: '인덱스 속성',
|
||||
@@ -212,6 +217,56 @@ 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',
|
||||
no_fields_tooltip: 'No fields defined for this custom type',
|
||||
custom_type_actions: {
|
||||
title: 'Actions',
|
||||
highlight_fields: 'Highlight Fields',
|
||||
delete_custom_type: 'Delete',
|
||||
clear_field_highlight: 'Clear Highlight',
|
||||
},
|
||||
delete_custom_type: 'Delete Type',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -222,7 +277,13 @@ export const ko_KR: LanguageTranslation = {
|
||||
undo: '실행 취소',
|
||||
redo: '다시 실행',
|
||||
reorder_diagram: '다이어그램 재정렬',
|
||||
// TODO: Translate
|
||||
clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"',
|
||||
custom_type_highlight_tooltip:
|
||||
'Highlighting "{{typeName}}" - Click to clear',
|
||||
highlight_overlapping_tables: '겹치는 테이블 강조 표시',
|
||||
// TODO: Translate
|
||||
filter: 'Filter Tables',
|
||||
},
|
||||
|
||||
new_diagram_dialog: {
|
||||
@@ -238,7 +299,7 @@ export const ko_KR: LanguageTranslation = {
|
||||
title: '당신의 데이터베이스를 가져오세요',
|
||||
database_edition: '데이터베이스 세부 종류:',
|
||||
step_1: '데이터베이스에서 아래의 SQL을 실행해주세요:',
|
||||
step_2: '이곳에 결과를 붙여넣어주세요:',
|
||||
step_2: '이곳에 결과를 붙여넣어주세요 →',
|
||||
script_results_placeholder: '이곳에 스크립트 결과를 입력...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS을 사용하시는 경우',
|
||||
@@ -331,6 +392,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: {
|
||||
@@ -348,6 +415,14 @@ export const ko_KR: LanguageTranslation = {
|
||||
confirm: '변경',
|
||||
},
|
||||
|
||||
create_table_schema_dialog: {
|
||||
title: '새 스키마 생성',
|
||||
description:
|
||||
'아직 스키마가 없습니다. 테이블을 정리하기 위해 첫 번째 스키마를 생성하세요.',
|
||||
create: '생성',
|
||||
cancel: '취소',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: '개선할 수 있도록 도와주세요!',
|
||||
description:
|
||||
@@ -364,7 +439,7 @@ export const ko_KR: LanguageTranslation = {
|
||||
error: {
|
||||
title: '다이어그램 내보내기 오류',
|
||||
description:
|
||||
'무언가 문제가 발생하였습니다. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
|
||||
'무언가 문제가 발생하였습니다. 도움이 필요하신 경우 support@chartdb.io으로 연락해주세요.',
|
||||
},
|
||||
},
|
||||
import_diagram_dialog: {
|
||||
@@ -375,7 +450,7 @@ export const ko_KR: LanguageTranslation = {
|
||||
error: {
|
||||
title: '다이어그램 가져오기 오류',
|
||||
description:
|
||||
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
|
||||
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 support@chartdb.io으로 연락해주세요.',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -402,6 +477,8 @@ export const ko_KR: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: '새 테이블',
|
||||
new_relationship: '새 연관관계',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
||||