Compare commits
69 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 | ||
|
44eac7daff | ||
|
502472b083 | ||
|
52d2ea596c | ||
|
bd67ccfbcf | ||
|
62beb68fa1 | ||
|
09b1275475 | ||
|
5dd7fe75d1 | ||
|
2939320a15 | ||
|
a643852837 |
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 }}
|
||||
|
92
CHANGELOG.md
@@ -1,5 +1,97 @@
|
||||
# Changelog
|
||||
|
||||
## [1.12.0](https://github.com/chartdb/chartdb/compare/v1.11.0...v1.12.0) (2025-05-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **areas:** implement area to enable logical diagram arrangement ([#661](https://github.com/chartdb/chartdb/issues/661)) ([92e3ec7](https://github.com/chartdb/chartdb/commit/92e3ec785c91f7f19881c6d9d0692257af4651bc))
|
||||
* **examples:** update examples to have areas ([#677](https://github.com/chartdb/chartdb/issues/677)) ([21c9129](https://github.com/chartdb/chartdb/commit/21c9129e14670c744950cd43a5cbdd4b7d47c639))
|
||||
* **image-export:** add transparent and pattern export image toggles ([#671](https://github.com/chartdb/chartdb/issues/671)) ([6b8d637](https://github.com/chartdb/chartdb/commit/6b8d637b757b94630ecd7521b4a2c99634afae69))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add sorting based on how common the datatype on side-panel ([#651](https://github.com/chartdb/chartdb/issues/651)) ([3a1b8d1](https://github.com/chartdb/chartdb/commit/3a1b8d1db13d8dd7cb6cbe5ef8c5a60faccfeae5))
|
||||
* **canvas:** disable edit area name on read only ([#666](https://github.com/chartdb/chartdb/issues/666)) ([9402822](https://github.com/chartdb/chartdb/commit/9402822fa31f8cd94fe7971277839ee5425e29bf))
|
||||
* **canvas:** read only mode ([#665](https://github.com/chartdb/chartdb/issues/665)) ([651fe36](https://github.com/chartdb/chartdb/commit/651fe361fce61fe0577d2593f268131e9ca359d0))
|
||||
* **clone:** add areas to clone diagram ([#664](https://github.com/chartdb/chartdb/issues/664)) ([aee1713](https://github.com/chartdb/chartdb/commit/aee1713aecdd5e54228a16cbc3c4fc184661c56b))
|
||||
* **dbml-editor:** add inline refs mode + fix issues with DBML syntax ([#687](https://github.com/chartdb/chartdb/issues/687)) ([fbf2fe9](https://github.com/chartdb/chartdb/commit/fbf2fe919c2168c715f8231c0246753b19635f14))
|
||||
* **dbml-editor:** remove invalid fields before showing DBML + warning ([#683](https://github.com/chartdb/chartdb/issues/683)) ([5759241](https://github.com/chartdb/chartdb/commit/5759241573db204183c92599588d59f4aadaeafb))
|
||||
* **ddl-import:** fix datatypes when importing via ddl ([#696](https://github.com/chartdb/chartdb/issues/696)) ([a1144bb](https://github.com/chartdb/chartdb/commit/a1144bbf761a0daedd546b5d9b92300be59e0157))
|
||||
* **ddl:** inline fks ddl script ([#701](https://github.com/chartdb/chartdb/issues/701)) ([5849e45](https://github.com/chartdb/chartdb/commit/5849e4586c7c2a7cd86bd064df8916b130fc6234))
|
||||
* **dependencies:** hide icon when diagram has no dependencies ([#684](https://github.com/chartdb/chartdb/issues/684)) ([547149d](https://github.com/chartdb/chartdb/commit/547149da44db6d3d1e36d619d475fe52ff83a472))
|
||||
* **examples:** add loader ([#678](https://github.com/chartdb/chartdb/issues/678)) ([90a20dd](https://github.com/chartdb/chartdb/commit/90a20dd1b0277c4aee848fae5ed7a8347c5ba77d))
|
||||
* **examples:** fix clone examples ([#679](https://github.com/chartdb/chartdb/issues/679)) ([1778abb](https://github.com/chartdb/chartdb/commit/1778abb683d575af244edcd9a11f8d03f903f719))
|
||||
* **expanded-table:** persist expanded state across renders ([#707](https://github.com/chartdb/chartdb/issues/707)) ([54d5e96](https://github.com/chartdb/chartdb/commit/54d5e96a6db1e3abd52229a89ac503ff31885386))
|
||||
* **export image:** Fix usage of advanced options accordion ([#703](https://github.com/chartdb/chartdb/issues/703)) ([0ce85cf](https://github.com/chartdb/chartdb/commit/0ce85cf76b733f441f661608278c0db3122c5074))
|
||||
* **import-database:** auto detect when user try to import ddl script ([#698](https://github.com/chartdb/chartdb/issues/698)) ([5a5e64a](https://github.com/chartdb/chartdb/commit/5a5e64abef510cff28b3d8972520d0b9df29b024))
|
||||
* **import-database:** remove view_definition when importing via query ([#702](https://github.com/chartdb/chartdb/issues/702)) ([481ad3c](https://github.com/chartdb/chartdb/commit/481ad3c8449f469bf2b4418e4cdcc5b5608dfd36))
|
||||
* **import-json:** for broken json imports ([#697](https://github.com/chartdb/chartdb/issues/697)) ([2368e0d](https://github.com/chartdb/chartdb/commit/2368e0d2639021c4a11a8e5131d6af44fb6a47db))
|
||||
* **import-json:** simplify import script for fixing invalid JSON ([#681](https://github.com/chartdb/chartdb/issues/681)) ([226e6cf](https://github.com/chartdb/chartdb/commit/226e6cf1ce4d2edcfbee6a4de7ab0bc0cfeb17fe))
|
||||
* **import:** dbml and query - senetize before import ([#699](https://github.com/chartdb/chartdb/issues/699)) ([34c0a71](https://github.com/chartdb/chartdb/commit/34c0a7163f47bde7ddfaa8f044341e3c971b7e03))
|
||||
* **navbar:** open diagram directly from diagram icon ([#694](https://github.com/chartdb/chartdb/issues/694)) ([7db86dc](https://github.com/chartdb/chartdb/commit/7db86dcf8c97d34b056e4b5b85a0dda0438322ea))
|
||||
* **performance:** Only render visible ([#672](https://github.com/chartdb/chartdb/issues/672)) ([83c4333](https://github.com/chartdb/chartdb/commit/83c43332d497e9fc148a18b9cb4d9ecc85e44183))
|
||||
* **performance:** update field only when changed ([#685](https://github.com/chartdb/chartdb/issues/685)) ([d3ddf7c](https://github.com/chartdb/chartdb/commit/d3ddf7c51eaa4b9cddb961defd52d423f39f281d))
|
||||
* **postgres:** fix import of postgres fks ([#700](https://github.com/chartdb/chartdb/issues/700)) ([89e3cea](https://github.com/chartdb/chartdb/commit/89e3ceab00defaabc079e165fc90e92ca00722cf))
|
||||
* **schema:** add areas to diagram schema ([#663](https://github.com/chartdb/chartdb/issues/663)) ([ecfa148](https://github.com/chartdb/chartdb/commit/ecfa14829bcb1b813c7b154b4bd59f24e3032d8f))
|
||||
* **sql-script:** change ddl to be sql-script ([#710](https://github.com/chartdb/chartdb/issues/710)) ([487fb2d](https://github.com/chartdb/chartdb/commit/487fb2d5c17b70ac54aa17af9a2ac9aded6b40ba))
|
||||
* **table:** enhance field focus behavior to include table hover state ([#676](https://github.com/chartdb/chartdb/issues/676)) ([19d2d0b](https://github.com/chartdb/chartdb/commit/19d2d0bddd3a464995b79e97e6caf6e652836081))
|
||||
* **translations:** Add some translations for ru-RU language ([#690](https://github.com/chartdb/chartdb/issues/690)) ([97d01d7](https://github.com/chartdb/chartdb/commit/97d01d72014e473c42348c9ebcbe7a0b973d31aa))
|
||||
|
||||
## [1.11.0](https://github.com/chartdb/chartdb/compare/v1.10.0...v1.11.0) (2025-04-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add sidebar footer help buttons ([#650](https://github.com/chartdb/chartdb/issues/650)) ([fc46cbb](https://github.com/chartdb/chartdb/commit/fc46cbb8933761c7bac3604664f7de812f6f5b6b))
|
||||
* **import-sql:** import postgresql via SQL (DDL script) ([#639](https://github.com/chartdb/chartdb/issues/639)) ([f7a6e0c](https://github.com/chartdb/chartdb/commit/f7a6e0cb5e4921dd9540739f9da269858e7ca7be))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **import:** display query result formatted ([#644](https://github.com/chartdb/chartdb/issues/644)) ([caa81c2](https://github.com/chartdb/chartdb/commit/caa81c24a6535bc87129c38622aac5a62a6d479d))
|
||||
* **import:** strict parse of database metadata ([#635](https://github.com/chartdb/chartdb/issues/635)) ([0940d72](https://github.com/chartdb/chartdb/commit/0940d72d5d3726650213257639f24ba47e729854))
|
||||
* **mobile:** fix create diagram modal on mobile ([#646](https://github.com/chartdb/chartdb/issues/646)) ([25c4b42](https://github.com/chartdb/chartdb/commit/25c4b4253849575d7a781ed197281e2a35e7184a))
|
||||
* **mysql-ddl:** update the script to import - for create fks ([#642](https://github.com/chartdb/chartdb/issues/642)) ([cf81253](https://github.com/chartdb/chartdb/commit/cf81253535ca5a3b8a65add78287c1bdb283a1c7))
|
||||
* **performance:** Import deps dynamically ([#652](https://github.com/chartdb/chartdb/issues/652)) ([e3cb627](https://github.com/chartdb/chartdb/commit/e3cb62788c13f149e35e1a5020191bd43d14b52f))
|
||||
* remove unused links from help menu ([#623](https://github.com/chartdb/chartdb/issues/623)) ([85275e5](https://github.com/chartdb/chartdb/commit/85275e5dd6e7845f06f682eeceda7932fc87e875))
|
||||
* **sidebar:** turn sidebar to responsive for mobile ([#658](https://github.com/chartdb/chartdb/issues/658)) ([ce2389f](https://github.com/chartdb/chartdb/commit/ce2389f135d399d82c9848335d31174bac8a3791))
|
||||
|
||||
## [1.10.0](https://github.com/chartdb/chartdb/compare/v1.9.0...v1.10.0) (2025-03-25)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cloudflare-d1:** add support to cloudflare-d1 + wrangler cli ([#632](https://github.com/chartdb/chartdb/issues/632)) ([794f226](https://github.com/chartdb/chartdb/commit/794f2262092fbe36e27e92220221ed98cb51ae37))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **dbml-editor:** dealing with dbml editor for non-generic db-type ([#624](https://github.com/chartdb/chartdb/issues/624)) ([14de30b](https://github.com/chartdb/chartdb/commit/14de30b7aaa0ccaca8372f0213b692266d53f0de))
|
||||
* **export-sql:** move from AI sql-export for MySQL&MariaDB to deterministic script ([#628](https://github.com/chartdb/chartdb/issues/628)) ([2fbf347](https://github.com/chartdb/chartdb/commit/2fbf3476b87f1177af17de8242a74d195dae5f35))
|
||||
* **export-sql:** move from AI sql-export for postgres to deterministic script ([#626](https://github.com/chartdb/chartdb/issues/626)) ([18f228c](https://github.com/chartdb/chartdb/commit/18f228ca1d5a6c6056cb7c3bfc24d04ec470edf1))
|
||||
* **export-sql:** move from AI sql-export for sqlite to deterministic script ([#627](https://github.com/chartdb/chartdb/issues/627)) ([897ac60](https://github.com/chartdb/chartdb/commit/897ac60a829a00e9453d670cceeb2282e9e93f1c))
|
||||
* **sidebar:** add sidebar for diagram objects ([#618](https://github.com/chartdb/chartdb/issues/618)) ([63b5ba0](https://github.com/chartdb/chartdb/commit/63b5ba0bb9934c4e5c5d0d1b6f995afbbd3acf36))
|
||||
* **sidebar:** opens sidepanel in case its closed and click on sidebar ([#620](https://github.com/chartdb/chartdb/issues/620)) ([3faa39e](https://github.com/chartdb/chartdb/commit/3faa39e7875d836dfe526d94a10f8aed070ac1c1))
|
||||
|
||||
## [1.9.0](https://github.com/chartdb/chartdb/compare/v1.8.1...v1.9.0) (2025-03-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **canvas:** highlight the Show-All button when No-Tables are visible in the canvas ([#612](https://github.com/chartdb/chartdb/issues/612)) ([62beb68](https://github.com/chartdb/chartdb/commit/62beb68fa1ec22ccd4fe5e59a8ceb9d3e8f6d374))
|
||||
* **chart max length:** add support for edit char max length ([#613](https://github.com/chartdb/chartdb/issues/613)) ([09b1275](https://github.com/chartdb/chartdb/commit/09b12754757b9625ca287d91a92cf0d83c9e2b89))
|
||||
* **chart max length:** enable edit length from data type select box ([#616](https://github.com/chartdb/chartdb/issues/616)) ([bd67ccf](https://github.com/chartdb/chartdb/commit/bd67ccfbcf66b919453ca6c0bfd71e16772b3d8e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cardinality:** set true as default ([#583](https://github.com/chartdb/chartdb/issues/583)) ([2939320](https://github.com/chartdb/chartdb/commit/2939320a15a9ccd9eccfe46c26e04ca1edca2420))
|
||||
* **performance:** Optimize performance of field comments editing ([#610](https://github.com/chartdb/chartdb/issues/610)) ([5dd7fe7](https://github.com/chartdb/chartdb/commit/5dd7fe75d1b0378ba406c75183c5e2356730c3b4))
|
||||
* remove Buckle dialog ([#617](https://github.com/chartdb/chartdb/issues/617)) ([502472b](https://github.com/chartdb/chartdb/commit/502472b08342be425e66e2b6c94e5fe37ba14aa9))
|
||||
* **shorcuts:** add shortcut to toggle the theme ([#602](https://github.com/chartdb/chartdb/issues/602)) ([a643852](https://github.com/chartdb/chartdb/commit/a6438528375ab54d3ec7d80ac6b6ddd65ea8cf1e))
|
||||
|
||||
## [1.8.1](https://github.com/chartdb/chartdb/compare/v1.8.0...v1.8.1) (2025-03-02)
|
||||
|
||||
|
||||
|
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)
|
||||
|
||||
---
|
||||
|
||||
|
40
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
|
||||
@@ -145,9 +147,9 @@ VITE_LLM_MODEL_NAME=Qwen/Qwen2.5-32B-Instruct-AWQ
|
||||
|
||||
## 💚 Community & Support
|
||||
|
||||
- [Discord](https://discord.gg/QeFwyWSKwC) (For live discussion with the community and the ChartDB team)
|
||||
- [GitHub Issues](https://github.com/chartdb/chartdb/issues) (For any bugs and errors you encounter using ChartDB)
|
||||
- [Twitter](https://x.com/chartdb_io) (Get news fast)
|
||||
- [Discord](https://discord.gg/QeFwyWSKwC) (For live discussion with the community and the ChartDB team)
|
||||
- [GitHub Issues](https://github.com/chartdb/chartdb/issues) (For any bugs and errors you encounter using ChartDB)
|
||||
- [Twitter](https://x.com/intent/follow?screen_name=jonathanfishner) (Get news fast)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
482
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "chartdb",
|
||||
"version": "1.8.1",
|
||||
"version": "1.12.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "chartdb",
|
||||
"version": "1.8.1",
|
||||
"version": "1.12.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^0.0.51",
|
||||
"@dbml/core": "^3.9.5",
|
||||
@@ -18,7 +18,7 @@
|
||||
"@radix-ui/react-checkbox": "^1.1.1",
|
||||
"@radix-ui/react-collapsible": "^1.1.0",
|
||||
"@radix-ui/react-context-menu": "^2.2.1",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-hover-card": "^1.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
@@ -27,18 +27,18 @@
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
"@radix-ui/react-scroll-area": "1.2.0",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@xyflow/react": "^12.3.1",
|
||||
"ahooks": "^3.8.1",
|
||||
"ai": "^3.3.14",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"dexie": "^4.0.8",
|
||||
@@ -1834,6 +1834,60 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.5.tgz",
|
||||
"integrity": "sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.4",
|
||||
"@radix-ui/react-focus-guards": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.1",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-portal": "1.1.3",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
|
||||
@@ -1969,6 +2023,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
|
||||
@@ -2028,25 +2100,124 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.5.tgz",
|
||||
"integrity": "sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw==",
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz",
|
||||
"integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.4",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||
"@radix-ui/react-focus-guards": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.1",
|
||||
"@radix-ui/react-focus-scope": "1.1.2",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-portal": "1.1.3",
|
||||
"@radix-ui/react-portal": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"aria-hidden": "^1.2.4",
|
||||
"react-remove-scroll": "^2.6.2"
|
||||
"react-remove-scroll": "^2.6.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
|
||||
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz",
|
||||
"integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
|
||||
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
||||
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -2295,6 +2466,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-menubar": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.5.tgz",
|
||||
@@ -2364,6 +2553,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
|
||||
@@ -2467,6 +2674,24 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
|
||||
@@ -2658,13 +2883,54 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator": {
|
||||
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.1.tgz",
|
||||
"integrity": "sha512-RRiNRSrD8iUiXriq/Y5n4/3iE8HzqgLHsusUSg5jVpU2+3tqcUFPJXHDymwEypunc2sWxDUS3UC+rkZRlHedsw==",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.1"
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz",
|
||||
"integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
||||
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
@@ -2682,9 +2948,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
|
||||
"integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.1"
|
||||
@@ -2818,23 +3084,175 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.7.tgz",
|
||||
"integrity": "sha512-ss0s80BC0+g0+Zc53MvilcnTYSOi4mSuFWBPYPuTOFGjx+pUU+ZrmamMNwS56t8MTFlniA5ocjd4jYm/CdhbOg==",
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz",
|
||||
"integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.4",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.5",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-popper": "1.2.1",
|
||||
"@radix-ui/react-portal": "1.1.3",
|
||||
"@radix-ui/react-popper": "1.2.2",
|
||||
"@radix-ui/react-portal": "1.1.4",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-slot": "1.1.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0",
|
||||
"@radix-ui/react-visually-hidden": "1.1.1"
|
||||
"@radix-ui/react-visually-hidden": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz",
|
||||
"integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz",
|
||||
"integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz",
|
||||
"integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
"@radix-ui/react-arrow": "1.1.2",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-use-rect": "1.1.0",
|
||||
"@radix-ui/react-use-size": "1.1.0",
|
||||
"@radix-ui/rect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz",
|
||||
"integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.2",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz",
|
||||
"integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.1.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-visually-hidden": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz",
|
||||
"integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
|
12
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chartdb",
|
||||
"private": true,
|
||||
"version": "1.8.1",
|
||||
"version": "1.12.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -22,7 +22,7 @@
|
||||
"@radix-ui/react-checkbox": "^1.1.1",
|
||||
"@radix-ui/react-collapsible": "^1.1.0",
|
||||
"@radix-ui/react-context-menu": "^2.2.1",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||
"@radix-ui/react-hover-card": "^1.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
@@ -31,18 +31,18 @@
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
"@radix-ui/react-scroll-area": "1.2.0",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@xyflow/react": "^12.3.1",
|
||||
"ahooks": "^3.8.1",
|
||||
"ai": "^3.3.14",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"dexie": "^4.0.8",
|
||||
|
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 |
@@ -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,43 @@ export const Editor = lazy(() =>
|
||||
}))
|
||||
);
|
||||
|
||||
export const DiffEditor = lazy(() =>
|
||||
import('./code-editor').then((module) => ({
|
||||
default: module.DiffEditor,
|
||||
}))
|
||||
);
|
||||
|
||||
type EditorType = typeof Editor;
|
||||
|
||||
export interface CodeSnippetAction {
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export interface CodeSnippetProps {
|
||||
className?: string;
|
||||
code: string;
|
||||
codeToCopy?: string;
|
||||
language?: 'sql' | 'shell';
|
||||
loading?: boolean;
|
||||
autoScroll?: boolean;
|
||||
isComplete?: boolean;
|
||||
editorProps?: React.ComponentProps<EditorType>;
|
||||
actions?: CodeSnippetAction[];
|
||||
}
|
||||
|
||||
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
({
|
||||
className,
|
||||
code,
|
||||
codeToCopy,
|
||||
loading,
|
||||
language = 'sql',
|
||||
autoScroll = false,
|
||||
isComplete = true,
|
||||
editorProps,
|
||||
actions,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const monaco = useMonaco();
|
||||
@@ -85,7 +102,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
await navigator.clipboard.writeText(codeToCopy ?? code);
|
||||
setIsCopied(true);
|
||||
} catch {
|
||||
setIsCopied(false);
|
||||
@@ -97,7 +114,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
),
|
||||
});
|
||||
}
|
||||
}, [code, t, toast]);
|
||||
}, [code, codeToCopy, t, toast]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -111,36 +128,58 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
) : (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
{isComplete ? (
|
||||
<Tooltip
|
||||
onOpenChange={setTooltipOpen}
|
||||
open={isCopied || tooltipOpen}
|
||||
>
|
||||
<TooltipTrigger
|
||||
asChild
|
||||
className="absolute right-1 top-1 z-10"
|
||||
<div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
|
||||
<Tooltip
|
||||
onOpenChange={setTooltipOpen}
|
||||
open={isCopied || tooltipOpen}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
className=" h-fit p-1.5"
|
||||
variant="outline"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{isCopied ? (
|
||||
<CopyCheck size={16} />
|
||||
) : (
|
||||
<Copy size={16} />
|
||||
)}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t(
|
||||
isCopied
|
||||
? 'copied'
|
||||
: 'copy_to_clipboard'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
className="h-fit p-1.5"
|
||||
variant="outline"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{isCopied ? (
|
||||
<CopyCheck size={16} />
|
||||
) : (
|
||||
<Copy size={16} />
|
||||
)}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t(
|
||||
isCopied
|
||||
? 'copied'
|
||||
: 'copy_to_clipboard'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
{actions &&
|
||||
actions.length > 0 &&
|
||||
actions.map((action, index) => (
|
||||
<Tooltip key={index}>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
className="h-fit p-1.5"
|
||||
variant="outline"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
<action.icon
|
||||
size={16}
|
||||
/>
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{action.label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Editor
|
||||
|
@@ -22,7 +22,7 @@ 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>
|
||||
@@ -30,6 +30,7 @@ export const DiagramIcon = React.forwardRef<
|
||||
src={databaseEditionToImageMap[databaseEdition]}
|
||||
className={cn('h-5 max-w-fit rounded-full', imgClassName)}
|
||||
alt="database"
|
||||
onClick={onClick}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -43,6 +44,7 @@ export const DiagramIcon = React.forwardRef<
|
||||
src={databaseSecondaryLogoMap[databaseType]}
|
||||
className={cn('h-5 max-w-fit', imgClassName)}
|
||||
alt="database"
|
||||
onClick={onClick}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
|
@@ -24,12 +24,19 @@ export interface SelectBoxOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
regex?: string;
|
||||
extractRegex?: RegExp;
|
||||
}
|
||||
|
||||
export interface SelectBoxProps {
|
||||
options: SelectBoxOption[];
|
||||
value?: string[] | string;
|
||||
onChange?: (values: string[] | string) => void;
|
||||
valueSuffix?: string;
|
||||
optionSuffix?: (option: SelectBoxOption) => string;
|
||||
onChange?: (
|
||||
values: string[] | string,
|
||||
regexMatches?: string[] | string
|
||||
) => void;
|
||||
placeholder?: string;
|
||||
inputPlaceholder?: string;
|
||||
emptyPlaceholder?: string;
|
||||
@@ -55,10 +62,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
className,
|
||||
options,
|
||||
value,
|
||||
valueSuffix,
|
||||
onChange,
|
||||
multiple,
|
||||
oneLine,
|
||||
selectAll,
|
||||
optionSuffix,
|
||||
deselectAll,
|
||||
clearText,
|
||||
showClear,
|
||||
@@ -86,7 +95,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
);
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(selectedValue: string) => {
|
||||
(selectedValue: string, regexMatches?: string[]) => {
|
||||
if (multiple) {
|
||||
const newValue =
|
||||
value?.includes(selectedValue) && Array.isArray(value)
|
||||
@@ -94,7 +103,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
: [...(value ?? []), selectedValue];
|
||||
onChange?.(newValue);
|
||||
} else {
|
||||
onChange?.(selectedValue);
|
||||
onChange?.(selectedValue, regexMatches);
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
@@ -199,6 +208,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
(opt) => opt.value === value
|
||||
)?.label
|
||||
}
|
||||
{valueSuffix ? valueSuffix : ''}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
@@ -239,11 +249,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
align="center"
|
||||
>
|
||||
<Command
|
||||
filter={(value, search) =>
|
||||
value.toLowerCase().includes(search.toLowerCase())
|
||||
filter={(value, search, keywords) => {
|
||||
if (
|
||||
keywords?.length &&
|
||||
keywords.some((keyword) =>
|
||||
new RegExp(keyword).test(search)
|
||||
)
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return value
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase())
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
: 0;
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<CommandInput
|
||||
@@ -302,14 +323,35 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
const isSelected =
|
||||
Array.isArray(value) &&
|
||||
value.includes(option.value);
|
||||
|
||||
const isRegexMatch =
|
||||
option.regex &&
|
||||
new RegExp(option.regex)?.test(
|
||||
searchTerm
|
||||
);
|
||||
|
||||
const matches = option.extractRegex
|
||||
? searchTerm.match(
|
||||
option.extractRegex
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
className="flex items-center"
|
||||
key={option.value}
|
||||
// value={option.value}
|
||||
keywords={
|
||||
option.regex
|
||||
? [option.regex]
|
||||
: undefined
|
||||
}
|
||||
onSelect={() =>
|
||||
handleSelect(
|
||||
option.value
|
||||
option.value,
|
||||
matches?.map(
|
||||
(match) =>
|
||||
match.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -327,7 +369,15 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
)}
|
||||
<div className="flex items-center truncate">
|
||||
<span>
|
||||
{option.label}
|
||||
{isRegexMatch
|
||||
? searchTerm
|
||||
: option.label}
|
||||
{!isRegexMatch &&
|
||||
optionSuffix
|
||||
? optionSuffix(
|
||||
option
|
||||
)
|
||||
: ''}
|
||||
</span>
|
||||
{option.description && (
|
||||
<span className="ml-1 text-xs text-muted-foreground">
|
||||
@@ -337,19 +387,20 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!multiple &&
|
||||
{((!multiple &&
|
||||
option.value ===
|
||||
value && (
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
option.value ===
|
||||
value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
value) ||
|
||||
isRegexMatch) && (
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
option.value ===
|
||||
value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
|
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 };
|
@@ -10,6 +10,7 @@ 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';
|
||||
|
||||
export type ChartDBEventType =
|
||||
| 'add_tables'
|
||||
@@ -70,6 +71,7 @@ export interface ChartDBContext {
|
||||
schemas: DBSchema[];
|
||||
relationships: DBRelationship[];
|
||||
dependencies: DBDependency[];
|
||||
areas: Area[];
|
||||
currentDiagram: Diagram;
|
||||
events: EventEmitter<ChartDBEvent>;
|
||||
readonly?: boolean;
|
||||
@@ -221,6 +223,31 @@ 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>;
|
||||
}
|
||||
|
||||
export const chartDBContext = createContext<ChartDBContext>({
|
||||
@@ -230,6 +257,7 @@ export const chartDBContext = createContext<ChartDBContext>({
|
||||
tables: [],
|
||||
relationships: [],
|
||||
dependencies: [],
|
||||
areas: [],
|
||||
schemas: [],
|
||||
filteredSchemas: [],
|
||||
filterSchemas: emptyFn,
|
||||
@@ -296,4 +324,13 @@ 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,
|
||||
});
|
||||
|
@@ -21,7 +21,10 @@ 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';
|
||||
|
||||
export interface ChartDBProviderProps {
|
||||
diagram?: Diagram;
|
||||
@@ -30,7 +33,8 @@ export interface ChartDBProviderProps {
|
||||
|
||||
export const ChartDBProvider: React.FC<
|
||||
React.PropsWithChildren<ChartDBProviderProps>
|
||||
> = ({ children, diagram, readonly }) => {
|
||||
> = ({ children, diagram, readonly: readonlyProp }) => {
|
||||
const { hasDiff } = useDiff();
|
||||
let db = useStorage();
|
||||
const events = useEventEmitter<ChartDBEvent>();
|
||||
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
||||
@@ -53,9 +57,34 @@ export const ChartDBProvider: React.FC<
|
||||
const [dependencies, setDependencies] = useState<DBDependency[]>(
|
||||
diagram?.dependencies ?? []
|
||||
);
|
||||
const [areas, setAreas] = useState<Area[]>(diagram?.areas ?? []);
|
||||
const { events: diffEvents } = useDiff();
|
||||
|
||||
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
|
||||
const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
|
||||
setTables((tables) =>
|
||||
[...tables, ...(tablesAdded ?? [])].map((table) => {
|
||||
const fields = fieldsAdded.get(table.id);
|
||||
return fields
|
||||
? { ...table, fields: [...table.fields, ...fields] }
|
||||
: table;
|
||||
})
|
||||
);
|
||||
setRelationships((relationships) => [
|
||||
...relationships,
|
||||
...(relationshipsAdded ?? []),
|
||||
]);
|
||||
}, []);
|
||||
|
||||
diffEvents.useSubscription(diffCalculatedHandler);
|
||||
|
||||
const defaultSchemaName = defaultSchemas[databaseType];
|
||||
|
||||
const readonly = useMemo(
|
||||
() => readonlyProp ?? hasDiff ?? false,
|
||||
[readonlyProp, hasDiff]
|
||||
);
|
||||
|
||||
if (readonly) {
|
||||
db = storageInitialValue;
|
||||
}
|
||||
@@ -125,6 +154,7 @@ export const ChartDBProvider: React.FC<
|
||||
tables,
|
||||
relationships,
|
||||
dependencies,
|
||||
areas,
|
||||
}),
|
||||
[
|
||||
diagramId,
|
||||
@@ -134,6 +164,7 @@ export const ChartDBProvider: React.FC<
|
||||
tables,
|
||||
relationships,
|
||||
dependencies,
|
||||
areas,
|
||||
diagramCreatedAt,
|
||||
diagramUpdatedAt,
|
||||
]
|
||||
@@ -145,6 +176,7 @@ export const ChartDBProvider: React.FC<
|
||||
setTables([]);
|
||||
setRelationships([]);
|
||||
setDependencies([]);
|
||||
setAreas([]);
|
||||
setDiagramUpdatedAt(updatedAt);
|
||||
|
||||
resetRedoStack();
|
||||
@@ -155,6 +187,7 @@ export const ChartDBProvider: React.FC<
|
||||
db.deleteDiagramTables(diagramId),
|
||||
db.deleteDiagramRelationships(diagramId),
|
||||
db.deleteDiagramDependencies(diagramId),
|
||||
db.deleteDiagramAreas(diagramId),
|
||||
]);
|
||||
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
||||
|
||||
@@ -167,6 +200,7 @@ export const ChartDBProvider: React.FC<
|
||||
setTables([]);
|
||||
setRelationships([]);
|
||||
setDependencies([]);
|
||||
setAreas([]);
|
||||
resetRedoStack();
|
||||
resetUndoStack();
|
||||
|
||||
@@ -175,6 +209,7 @@ export const ChartDBProvider: React.FC<
|
||||
db.deleteDiagramRelationships(diagramId),
|
||||
db.deleteDiagram(diagramId),
|
||||
db.deleteDiagramDependencies(diagramId),
|
||||
db.deleteDiagramAreas(diagramId),
|
||||
]);
|
||||
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
||||
|
||||
@@ -1336,6 +1371,130 @@ export const ChartDBProvider: React.FC<
|
||||
]
|
||||
);
|
||||
|
||||
// Area operations
|
||||
const addAreas: ChartDBContext['addAreas'] = useCallback(
|
||||
async (areas: Area[], options = { updateHistory: true }) => {
|
||||
setAreas((currentAreas) => [...currentAreas, ...areas]);
|
||||
|
||||
const updatedAt = new Date();
|
||||
setDiagramUpdatedAt(updatedAt);
|
||||
|
||||
await Promise.all([
|
||||
...areas.map((area) => db.addArea({ diagramId, area })),
|
||||
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||
]);
|
||||
|
||||
if (options.updateHistory) {
|
||||
addUndoAction({
|
||||
action: 'addAreas',
|
||||
redoData: { areas },
|
||||
undoData: { areaIds: areas.map((a) => a.id) },
|
||||
});
|
||||
resetRedoStack();
|
||||
}
|
||||
},
|
||||
[db, diagramId, setAreas, addUndoAction, resetRedoStack]
|
||||
);
|
||||
|
||||
const addArea: ChartDBContext['addArea'] = useCallback(
|
||||
async (area: Area, options = { updateHistory: true }) => {
|
||||
return addAreas([area], options);
|
||||
},
|
||||
[addAreas]
|
||||
);
|
||||
|
||||
const createArea: ChartDBContext['createArea'] = useCallback(
|
||||
async (attributes) => {
|
||||
const area: Area = {
|
||||
id: generateId(),
|
||||
name: `Area ${areas.length + 1}`,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 300,
|
||||
height: 200,
|
||||
color: randomColor(),
|
||||
...attributes,
|
||||
};
|
||||
|
||||
await addArea(area);
|
||||
|
||||
return area;
|
||||
},
|
||||
[areas, addArea]
|
||||
);
|
||||
|
||||
const getArea: ChartDBContext['getArea'] = useCallback(
|
||||
(id: string) => areas.find((area) => area.id === id) ?? null,
|
||||
[areas]
|
||||
);
|
||||
|
||||
const removeAreas: ChartDBContext['removeAreas'] = useCallback(
|
||||
async (ids: string[], options = { updateHistory: true }) => {
|
||||
const prevAreas = [
|
||||
...areas.filter((area) => ids.includes(area.id)),
|
||||
];
|
||||
|
||||
setAreas((areas) => areas.filter((area) => !ids.includes(area.id)));
|
||||
|
||||
const updatedAt = new Date();
|
||||
setDiagramUpdatedAt(updatedAt);
|
||||
|
||||
await Promise.all([
|
||||
...ids.map((id) => db.deleteArea({ diagramId, id })),
|
||||
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||
]);
|
||||
|
||||
if (prevAreas.length > 0 && options.updateHistory) {
|
||||
addUndoAction({
|
||||
action: 'removeAreas',
|
||||
redoData: { areaIds: ids },
|
||||
undoData: { areas: prevAreas },
|
||||
});
|
||||
resetRedoStack();
|
||||
}
|
||||
},
|
||||
[db, diagramId, setAreas, areas, addUndoAction, resetRedoStack]
|
||||
);
|
||||
|
||||
const removeArea: ChartDBContext['removeArea'] = useCallback(
|
||||
async (id: string, options = { updateHistory: true }) => {
|
||||
return removeAreas([id], options);
|
||||
},
|
||||
[removeAreas]
|
||||
);
|
||||
|
||||
const updateArea: ChartDBContext['updateArea'] = useCallback(
|
||||
async (
|
||||
id: string,
|
||||
area: Partial<Area>,
|
||||
options = { updateHistory: true }
|
||||
) => {
|
||||
const prevArea = getArea(id);
|
||||
|
||||
setAreas((areas) =>
|
||||
areas.map((a) => (a.id === id ? { ...a, ...area } : a))
|
||||
);
|
||||
|
||||
const updatedAt = new Date();
|
||||
setDiagramUpdatedAt(updatedAt);
|
||||
|
||||
await Promise.all([
|
||||
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||
db.updateArea({ id, attributes: area }),
|
||||
]);
|
||||
|
||||
if (!!prevArea && options.updateHistory) {
|
||||
addUndoAction({
|
||||
action: 'updateArea',
|
||||
redoData: { areaId: id, area },
|
||||
undoData: { areaId: id, area: prevArea },
|
||||
});
|
||||
resetRedoStack();
|
||||
}
|
||||
},
|
||||
[db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack]
|
||||
);
|
||||
|
||||
const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
|
||||
useCallback(
|
||||
async (diagram) => {
|
||||
@@ -1346,6 +1505,7 @@ export const ChartDBProvider: React.FC<
|
||||
setTables(diagram?.tables ?? []);
|
||||
setRelationships(diagram?.relationships ?? []);
|
||||
setDependencies(diagram?.dependencies ?? []);
|
||||
setAreas(diagram?.areas ?? []);
|
||||
setDiagramCreatedAt(diagram.createdAt);
|
||||
setDiagramUpdatedAt(diagram.updatedAt);
|
||||
|
||||
@@ -1359,6 +1519,7 @@ export const ChartDBProvider: React.FC<
|
||||
setTables,
|
||||
setRelationships,
|
||||
setDependencies,
|
||||
setAreas,
|
||||
setDiagramCreatedAt,
|
||||
setDiagramUpdatedAt,
|
||||
events,
|
||||
@@ -1371,6 +1532,7 @@ export const ChartDBProvider: React.FC<
|
||||
includeRelationships: true,
|
||||
includeTables: true,
|
||||
includeDependencies: true,
|
||||
includeAreas: true,
|
||||
});
|
||||
|
||||
if (diagram) {
|
||||
@@ -1391,6 +1553,7 @@ export const ChartDBProvider: React.FC<
|
||||
tables,
|
||||
relationships,
|
||||
dependencies,
|
||||
areas,
|
||||
currentDiagram,
|
||||
schemas,
|
||||
filteredSchemas,
|
||||
@@ -1438,6 +1601,13 @@ export const ChartDBProvider: React.FC<
|
||||
removeDependency,
|
||||
removeDependencies,
|
||||
updateDependency,
|
||||
createArea,
|
||||
addArea,
|
||||
addAreas,
|
||||
getArea,
|
||||
removeArea,
|
||||
removeAreas,
|
||||
updateArea,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@@ -4,7 +4,10 @@ import type { ChartDBConfig } from '@/lib/domain/config';
|
||||
|
||||
export interface ConfigContext {
|
||||
config?: ChartDBConfig;
|
||||
updateConfig: (config: Partial<ChartDBConfig>) => Promise<void>;
|
||||
updateConfig: (params: {
|
||||
config?: Partial<ChartDBConfig>;
|
||||
updateFn?: (config: ChartDBConfig) => ChartDBConfig;
|
||||
}) => Promise<void>;
|
||||
}
|
||||
|
||||
export const ConfigContext = createContext<ConfigContext>({
|
||||
|
@@ -19,15 +19,29 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
loadConfig();
|
||||
}, [getConfig]);
|
||||
|
||||
const updateConfig: ConfigContext['updateConfig'] = async (
|
||||
config: Partial<ChartDBConfig>
|
||||
) => {
|
||||
await updateDataConfig(config);
|
||||
setConfig((prevConfig) =>
|
||||
prevConfig
|
||||
? { ...prevConfig, ...config }
|
||||
: { ...{ defaultDiagramId: '' }, ...config }
|
||||
);
|
||||
const updateConfig: ConfigContext['updateConfig'] = async ({
|
||||
config,
|
||||
updateFn,
|
||||
}) => {
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
setConfig((prevConfig) => {
|
||||
let baseConfig: ChartDBConfig = { defaultDiagramId: '' };
|
||||
if (prevConfig) {
|
||||
baseConfig = prevConfig;
|
||||
}
|
||||
|
||||
const updatedConfig = updateFn
|
||||
? updateFn(baseConfig)
|
||||
: { ...baseConfig, ...config };
|
||||
|
||||
updateDataConfig(updatedConfig).then(() => {
|
||||
resolve();
|
||||
});
|
||||
return updatedConfig;
|
||||
});
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
|
||||
return (
|
||||
|
@@ -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
|
||||
@@ -47,10 +50,6 @@ export interface DialogContext {
|
||||
openStarUsDialog: () => void;
|
||||
closeStarUsDialog: () => void;
|
||||
|
||||
// Buckle dialog
|
||||
openBuckleDialog: () => void;
|
||||
closeBuckleDialog: () => void;
|
||||
|
||||
// Export image dialog
|
||||
openExportImageDialog: (
|
||||
params: Omit<ExportImageDialogProps, 'dialog'>
|
||||
@@ -97,8 +96,6 @@ export const dialogContext = createContext<DialogContext>({
|
||||
closeExportDiagramDialog: emptyFn,
|
||||
openImportDiagramDialog: emptyFn,
|
||||
closeImportDiagramDialog: emptyFn,
|
||||
openBuckleDialog: emptyFn,
|
||||
closeBuckleDialog: emptyFn,
|
||||
openImportDBMLDialog: emptyFn,
|
||||
closeImportDBMLDialog: emptyFn,
|
||||
});
|
||||
|
@@ -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';
|
||||
@@ -19,7 +20,6 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
|
||||
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
|
||||
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
|
||||
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
|
||||
import { BuckleDialog } from '@/dialogs/buckle-dialog/buckle-dialog';
|
||||
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
||||
import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
||||
|
||||
@@ -27,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'>>();
|
||||
@@ -54,7 +65,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
);
|
||||
|
||||
const [openStarUsDialog, setOpenStarUsDialog] = useState(false);
|
||||
const [openBuckleDialog, setOpenBuckleDialog] = useState(false);
|
||||
|
||||
// Export image dialog
|
||||
const [openExportImageDialog, setOpenExportImageDialog] = useState(false);
|
||||
@@ -130,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),
|
||||
@@ -147,8 +157,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
closeTableSchemaDialog: () => setOpenTableSchemaDialog(false),
|
||||
openStarUsDialog: () => setOpenStarUsDialog(true),
|
||||
closeStarUsDialog: () => setOpenStarUsDialog(false),
|
||||
closeBuckleDialog: () => setOpenBuckleDialog(false),
|
||||
openBuckleDialog: () => setOpenBuckleDialog(true),
|
||||
closeExportImageDialog: () => setOpenExportImageDialog(false),
|
||||
openExportImageDialog: openExportImageDialogHandler,
|
||||
openExportDiagramDialog: () => setOpenExportDiagramDialog(true),
|
||||
@@ -165,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}
|
||||
@@ -193,7 +204,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
/>
|
||||
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
|
||||
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
|
||||
<BuckleDialog dialog={{ open: openBuckleDialog }} />
|
||||
<ImportDBMLDialog
|
||||
dialog={{ open: openImportDBMLDialog }}
|
||||
{...importDBMLDialogParams}
|
||||
|
455
src/context/diff-context/diff-check/diff-check.ts
Normal file
@@ -0,0 +1,455 @@
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import type { DBIndex } from '@/lib/domain/db-index';
|
||||
import type { ChartDBDiff, DiffMap, DiffObject } from '@/lib/domain/diff/diff';
|
||||
import type { FieldDiffAttribute } from '@/lib/domain/diff/field-diff';
|
||||
|
||||
export function getDiffMapKey({
|
||||
diffObject,
|
||||
objectId,
|
||||
attribute,
|
||||
}: {
|
||||
diffObject: DiffObject;
|
||||
objectId: string;
|
||||
attribute?: string;
|
||||
}): string {
|
||||
return attribute
|
||||
? `${diffObject}-${attribute}-${objectId}`
|
||||
: `${diffObject}-${objectId}`;
|
||||
}
|
||||
|
||||
export function generateDiff({
|
||||
diagram,
|
||||
newDiagram,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
newDiagram: Diagram;
|
||||
}): {
|
||||
diffMap: DiffMap;
|
||||
changedTables: Map<string, boolean>;
|
||||
changedFields: Map<string, boolean>;
|
||||
} {
|
||||
const newDiffs = new Map<string, ChartDBDiff>();
|
||||
const changedTables = new Map<string, boolean>();
|
||||
const changedFields = new Map<string, boolean>();
|
||||
|
||||
// Compare tables
|
||||
compareTables({ diagram, newDiagram, diffMap: newDiffs, changedTables });
|
||||
|
||||
// Compare fields and indexes for matching tables
|
||||
compareTableContents({
|
||||
diagram,
|
||||
newDiagram,
|
||||
diffMap: newDiffs,
|
||||
changedTables,
|
||||
changedFields,
|
||||
});
|
||||
|
||||
// Compare relationships
|
||||
compareRelationships({ diagram, newDiagram, diffMap: newDiffs });
|
||||
|
||||
return { diffMap: newDiffs, changedTables, changedFields };
|
||||
}
|
||||
|
||||
// Compare tables between diagrams
|
||||
function compareTables({
|
||||
diagram,
|
||||
newDiagram,
|
||||
diffMap,
|
||||
changedTables,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
newDiagram: Diagram;
|
||||
diffMap: DiffMap;
|
||||
changedTables: Map<string, boolean>;
|
||||
}) {
|
||||
const oldTables = diagram.tables || [];
|
||||
const newTables = newDiagram.tables || [];
|
||||
|
||||
// Check for added tables
|
||||
for (const newTable of newTables) {
|
||||
if (!oldTables.find((t) => t.id === newTable.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({ diffObject: 'table', objectId: newTable.id }),
|
||||
{
|
||||
object: 'table',
|
||||
type: 'added',
|
||||
tableAdded: newTable,
|
||||
}
|
||||
);
|
||||
changedTables.set(newTable.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for removed tables
|
||||
for (const oldTable of oldTables) {
|
||||
if (!newTables.find((t) => t.id === oldTable.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({ diffObject: 'table', objectId: oldTable.id }),
|
||||
{
|
||||
object: 'table',
|
||||
type: 'removed',
|
||||
tableId: oldTable.id,
|
||||
}
|
||||
);
|
||||
changedTables.set(oldTable.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for table name, comments and color changes
|
||||
for (const oldTable of oldTables) {
|
||||
const newTable = newTables.find((t) => t.id === oldTable.id);
|
||||
|
||||
if (!newTable) continue;
|
||||
|
||||
if (oldTable.name !== newTable.name) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: oldTable.id,
|
||||
attribute: 'name',
|
||||
}),
|
||||
{
|
||||
object: 'table',
|
||||
type: 'changed',
|
||||
tableId: oldTable.id,
|
||||
attribute: 'name',
|
||||
newValue: newTable.name,
|
||||
oldValue: oldTable.name,
|
||||
}
|
||||
);
|
||||
|
||||
changedTables.set(oldTable.id, true);
|
||||
}
|
||||
|
||||
if (
|
||||
(oldTable.comments || newTable.comments) &&
|
||||
oldTable.comments !== newTable.comments
|
||||
) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: oldTable.id,
|
||||
attribute: 'comments',
|
||||
}),
|
||||
{
|
||||
object: 'table',
|
||||
type: 'changed',
|
||||
tableId: oldTable.id,
|
||||
attribute: 'comments',
|
||||
newValue: newTable.comments,
|
||||
oldValue: oldTable.comments,
|
||||
}
|
||||
);
|
||||
|
||||
changedTables.set(oldTable.id, true);
|
||||
}
|
||||
|
||||
if (oldTable.color !== newTable.color) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: oldTable.id,
|
||||
attribute: 'color',
|
||||
}),
|
||||
{
|
||||
object: 'table',
|
||||
type: 'changed',
|
||||
tableId: oldTable.id,
|
||||
attribute: 'color',
|
||||
newValue: newTable.color,
|
||||
oldValue: oldTable.color,
|
||||
}
|
||||
);
|
||||
|
||||
changedTables.set(oldTable.id, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compare fields and indexes for matching tables
|
||||
function compareTableContents({
|
||||
diagram,
|
||||
newDiagram,
|
||||
diffMap,
|
||||
changedTables,
|
||||
changedFields,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
newDiagram: Diagram;
|
||||
diffMap: DiffMap;
|
||||
changedTables: Map<string, boolean>;
|
||||
changedFields: Map<string, boolean>;
|
||||
}) {
|
||||
const oldTables = diagram.tables || [];
|
||||
const newTables = newDiagram.tables || [];
|
||||
|
||||
// For each table that exists in both diagrams
|
||||
for (const oldTable of oldTables) {
|
||||
const newTable = newTables.find((t) => t.id === oldTable.id);
|
||||
if (!newTable) continue;
|
||||
|
||||
// Compare fields
|
||||
compareFields({
|
||||
tableId: oldTable.id,
|
||||
oldFields: oldTable.fields,
|
||||
newFields: newTable.fields,
|
||||
diffMap,
|
||||
changedTables,
|
||||
changedFields,
|
||||
});
|
||||
|
||||
// Compare indexes
|
||||
compareIndexes({
|
||||
tableId: oldTable.id,
|
||||
oldIndexes: oldTable.indexes,
|
||||
newIndexes: newTable.indexes,
|
||||
diffMap,
|
||||
changedTables,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Compare fields between tables
|
||||
function compareFields({
|
||||
tableId,
|
||||
oldFields,
|
||||
newFields,
|
||||
diffMap,
|
||||
changedTables,
|
||||
changedFields,
|
||||
}: {
|
||||
tableId: string;
|
||||
oldFields: DBField[];
|
||||
newFields: DBField[];
|
||||
diffMap: DiffMap;
|
||||
changedTables: Map<string, boolean>;
|
||||
changedFields: Map<string, boolean>;
|
||||
}) {
|
||||
// Check for added fields
|
||||
for (const newField of newFields) {
|
||||
if (!oldFields.find((f) => f.id === newField.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: newField.id,
|
||||
}),
|
||||
{
|
||||
object: 'field',
|
||||
type: 'added',
|
||||
newField,
|
||||
tableId,
|
||||
}
|
||||
);
|
||||
changedTables.set(tableId, true);
|
||||
changedFields.set(newField.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for removed fields
|
||||
for (const oldField of oldFields) {
|
||||
if (!newFields.find((f) => f.id === oldField.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: oldField.id,
|
||||
}),
|
||||
{
|
||||
object: 'field',
|
||||
type: 'removed',
|
||||
fieldId: oldField.id,
|
||||
tableId,
|
||||
}
|
||||
);
|
||||
|
||||
changedTables.set(tableId, true);
|
||||
changedFields.set(oldField.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for field changes
|
||||
for (const oldField of oldFields) {
|
||||
const newField = newFields.find((f) => f.id === oldField.id);
|
||||
if (!newField) continue;
|
||||
|
||||
// Compare basic field properties
|
||||
compareFieldProperties({
|
||||
tableId,
|
||||
oldField,
|
||||
newField,
|
||||
diffMap,
|
||||
changedTables,
|
||||
changedFields,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Compare field properties
|
||||
function compareFieldProperties({
|
||||
tableId,
|
||||
oldField,
|
||||
newField,
|
||||
diffMap,
|
||||
changedTables,
|
||||
changedFields,
|
||||
}: {
|
||||
tableId: string;
|
||||
oldField: DBField;
|
||||
newField: DBField;
|
||||
diffMap: DiffMap;
|
||||
changedTables: Map<string, boolean>;
|
||||
changedFields: Map<string, boolean>;
|
||||
}) {
|
||||
const changedAttributes: FieldDiffAttribute[] = [];
|
||||
|
||||
if (oldField.name !== newField.name) {
|
||||
changedAttributes.push('name');
|
||||
}
|
||||
|
||||
if (oldField.type.id !== newField.type.id) {
|
||||
changedAttributes.push('type');
|
||||
}
|
||||
|
||||
if (oldField.primaryKey !== newField.primaryKey) {
|
||||
changedAttributes.push('primaryKey');
|
||||
}
|
||||
|
||||
if (oldField.unique !== newField.unique) {
|
||||
changedAttributes.push('unique');
|
||||
}
|
||||
|
||||
if (oldField.nullable !== newField.nullable) {
|
||||
changedAttributes.push('nullable');
|
||||
}
|
||||
|
||||
if (
|
||||
(newField.comments || oldField.comments) &&
|
||||
oldField.comments !== newField.comments
|
||||
) {
|
||||
changedAttributes.push('comments');
|
||||
}
|
||||
|
||||
if (changedAttributes.length > 0) {
|
||||
for (const attribute of changedAttributes) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: oldField.id,
|
||||
attribute,
|
||||
}),
|
||||
{
|
||||
object: 'field',
|
||||
type: 'changed',
|
||||
fieldId: oldField.id,
|
||||
tableId,
|
||||
attribute,
|
||||
oldValue: oldField[attribute] ?? '',
|
||||
newValue: newField[attribute] ?? '',
|
||||
}
|
||||
);
|
||||
}
|
||||
changedTables.set(tableId, true);
|
||||
changedFields.set(oldField.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Compare indexes between tables
|
||||
function compareIndexes({
|
||||
tableId,
|
||||
oldIndexes,
|
||||
newIndexes,
|
||||
diffMap,
|
||||
changedTables,
|
||||
}: {
|
||||
tableId: string;
|
||||
oldIndexes: DBIndex[];
|
||||
newIndexes: DBIndex[];
|
||||
diffMap: DiffMap;
|
||||
changedTables: Map<string, boolean>;
|
||||
}) {
|
||||
// Check for added indexes
|
||||
for (const newIndex of newIndexes) {
|
||||
if (!oldIndexes.find((i) => i.id === newIndex.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'index',
|
||||
objectId: newIndex.id,
|
||||
}),
|
||||
{
|
||||
object: 'index',
|
||||
type: 'added',
|
||||
newIndex,
|
||||
tableId,
|
||||
}
|
||||
);
|
||||
changedTables.set(tableId, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for removed indexes
|
||||
for (const oldIndex of oldIndexes) {
|
||||
if (!newIndexes.find((i) => i.id === oldIndex.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'index',
|
||||
objectId: oldIndex.id,
|
||||
}),
|
||||
{
|
||||
object: 'index',
|
||||
type: 'removed',
|
||||
indexId: oldIndex.id,
|
||||
tableId,
|
||||
}
|
||||
);
|
||||
changedTables.set(tableId, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compare relationships between diagrams
|
||||
function compareRelationships({
|
||||
diagram,
|
||||
newDiagram,
|
||||
diffMap,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
newDiagram: Diagram;
|
||||
diffMap: DiffMap;
|
||||
}) {
|
||||
const oldRelationships = diagram.relationships || [];
|
||||
const newRelationships = newDiagram.relationships || [];
|
||||
|
||||
// Check for added relationships
|
||||
for (const newRelationship of newRelationships) {
|
||||
if (!oldRelationships.find((r) => r.id === newRelationship.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'relationship',
|
||||
objectId: newRelationship.id,
|
||||
}),
|
||||
{
|
||||
object: 'relationship',
|
||||
type: 'added',
|
||||
newRelationship,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for removed relationships
|
||||
for (const oldRelationship of oldRelationships) {
|
||||
if (!newRelationships.find((r) => r.id === oldRelationship.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'relationship',
|
||||
objectId: oldRelationship.id,
|
||||
}),
|
||||
{
|
||||
object: 'relationship',
|
||||
type: 'removed',
|
||||
relationshipId: oldRelationship.id,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
79
src/context/diff-context/diff-context.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { createContext } from 'react';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { EventEmitter } from 'ahooks/lib/useEventEmitter';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||
import type { DiffMap } from '@/lib/domain/diff/diff';
|
||||
|
||||
export type DiffEventType = 'diff_calculated';
|
||||
|
||||
export type DiffEventBase<T extends DiffEventType, D> = {
|
||||
action: T;
|
||||
data: D;
|
||||
};
|
||||
|
||||
export type DiffCalculatedData = {
|
||||
tablesAdded: DBTable[];
|
||||
fieldsAdded: Map<string, DBField[]>;
|
||||
relationshipsAdded: DBRelationship[];
|
||||
};
|
||||
|
||||
export type DiffCalculatedEvent = DiffEventBase<
|
||||
'diff_calculated',
|
||||
DiffCalculatedData
|
||||
>;
|
||||
|
||||
export type DiffEvent = DiffCalculatedEvent;
|
||||
|
||||
export interface DiffContext {
|
||||
newDiagram: Diagram | null;
|
||||
originalDiagram: Diagram | null;
|
||||
diffMap: DiffMap;
|
||||
hasDiff: boolean;
|
||||
|
||||
calculateDiff: ({
|
||||
diagram,
|
||||
newDiagram,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
newDiagram: Diagram;
|
||||
}) => void;
|
||||
|
||||
// table diff
|
||||
checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
|
||||
checkIfNewTable: ({ tableId }: { tableId: string }) => boolean;
|
||||
checkIfTableRemoved: ({ tableId }: { tableId: string }) => boolean;
|
||||
getTableNewName: ({ tableId }: { tableId: string }) => string | null;
|
||||
getTableNewColor: ({ tableId }: { tableId: string }) => string | null;
|
||||
|
||||
// field diff
|
||||
checkIfFieldHasChange: ({
|
||||
tableId,
|
||||
fieldId,
|
||||
}: {
|
||||
tableId: string;
|
||||
fieldId: string;
|
||||
}) => boolean;
|
||||
checkIfFieldRemoved: ({ fieldId }: { fieldId: string }) => boolean;
|
||||
checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
|
||||
getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
|
||||
getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
|
||||
|
||||
// relationship diff
|
||||
checkIfNewRelationship: ({
|
||||
relationshipId,
|
||||
}: {
|
||||
relationshipId: string;
|
||||
}) => boolean;
|
||||
checkIfRelationshipRemoved: ({
|
||||
relationshipId,
|
||||
}: {
|
||||
relationshipId: string;
|
||||
}) => boolean;
|
||||
|
||||
events: EventEmitter<DiffEvent>;
|
||||
}
|
||||
|
||||
export const diffContext = createContext<DiffContext | undefined>(undefined);
|
373
src/context/diff-context/diff-provider.tsx
Normal file
@@ -0,0 +1,373 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import type {
|
||||
DiffCalculatedData,
|
||||
DiffContext,
|
||||
DiffEvent,
|
||||
} from './diff-context';
|
||||
import { diffContext } from './diff-context';
|
||||
|
||||
import { generateDiff, getDiffMapKey } from './diff-check/diff-check';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import { useEventEmitter } from 'ahooks';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||
import type { ChartDBDiff, DiffMap } from '@/lib/domain/diff/diff';
|
||||
|
||||
export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [newDiagram, setNewDiagram] = React.useState<Diagram | null>(null);
|
||||
const [originalDiagram, setOriginalDiagram] =
|
||||
React.useState<Diagram | null>(null);
|
||||
const [diffMap, setDiffMap] = React.useState<DiffMap>(
|
||||
new Map<string, ChartDBDiff>()
|
||||
);
|
||||
const [tablesChanged, setTablesChanged] = React.useState<
|
||||
Map<string, boolean>
|
||||
>(new Map<string, boolean>());
|
||||
const [fieldsChanged, setFieldsChanged] = React.useState<
|
||||
Map<string, boolean>
|
||||
>(new Map<string, boolean>());
|
||||
|
||||
const events = useEventEmitter<DiffEvent>();
|
||||
|
||||
const generateNewFieldsMap = useCallback(
|
||||
({
|
||||
diffMap,
|
||||
newDiagram,
|
||||
}: {
|
||||
diffMap: DiffMap;
|
||||
newDiagram: Diagram;
|
||||
}) => {
|
||||
const newFieldsMap = new Map<string, DBField[]>();
|
||||
|
||||
diffMap.forEach((diff) => {
|
||||
if (diff.object === 'field' && diff.type === 'added') {
|
||||
const field = newDiagram?.tables
|
||||
?.find((table) => table.id === diff.tableId)
|
||||
?.fields.find((f) => f.id === diff.newField.id);
|
||||
|
||||
if (field) {
|
||||
newFieldsMap.set(diff.tableId, [
|
||||
...(newFieldsMap.get(diff.tableId) ?? []),
|
||||
field,
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return newFieldsMap;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const findNewRelationships = useCallback(
|
||||
({
|
||||
diffMap,
|
||||
newDiagram,
|
||||
}: {
|
||||
diffMap: DiffMap;
|
||||
newDiagram: Diagram;
|
||||
}) => {
|
||||
const relationships: DBRelationship[] = [];
|
||||
diffMap.forEach((diff) => {
|
||||
if (diff.object === 'relationship' && diff.type === 'added') {
|
||||
const relationship = newDiagram?.relationships?.find(
|
||||
(rel) => rel.id === diff.newRelationship.id
|
||||
);
|
||||
|
||||
if (relationship) {
|
||||
relationships.push(relationship);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return relationships;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const generateDiffCalculatedData = useCallback(
|
||||
({
|
||||
newDiagram,
|
||||
diffMap,
|
||||
}: {
|
||||
newDiagram: Diagram;
|
||||
diffMap: DiffMap;
|
||||
}): DiffCalculatedData => {
|
||||
return {
|
||||
tablesAdded:
|
||||
newDiagram?.tables?.filter((table) => {
|
||||
const tableKey = getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: table.id,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(tableKey) &&
|
||||
diffMap.get(tableKey)?.type === 'added'
|
||||
);
|
||||
}) ?? [],
|
||||
|
||||
fieldsAdded: generateNewFieldsMap({
|
||||
diffMap: diffMap,
|
||||
newDiagram: newDiagram,
|
||||
}),
|
||||
relationshipsAdded: findNewRelationships({
|
||||
diffMap: diffMap,
|
||||
newDiagram: newDiagram,
|
||||
}),
|
||||
};
|
||||
},
|
||||
[findNewRelationships, generateNewFieldsMap]
|
||||
);
|
||||
|
||||
const calculateDiff: DiffContext['calculateDiff'] = useCallback(
|
||||
({ diagram, newDiagram: newDiagramArg }) => {
|
||||
const {
|
||||
diffMap: newDiffs,
|
||||
changedTables: newChangedTables,
|
||||
changedFields: newChangedFields,
|
||||
} = generateDiff({ diagram, newDiagram: newDiagramArg });
|
||||
|
||||
setDiffMap(newDiffs);
|
||||
setTablesChanged(newChangedTables);
|
||||
setFieldsChanged(newChangedFields);
|
||||
setNewDiagram(newDiagramArg);
|
||||
setOriginalDiagram(diagram);
|
||||
|
||||
events.emit({
|
||||
action: 'diff_calculated',
|
||||
data: generateDiffCalculatedData({
|
||||
diffMap: newDiffs,
|
||||
newDiagram: newDiagramArg,
|
||||
}),
|
||||
});
|
||||
},
|
||||
[setDiffMap, events, generateDiffCalculatedData]
|
||||
);
|
||||
|
||||
const getTableNewName = useCallback<DiffContext['getTableNewName']>(
|
||||
({ tableId }) => {
|
||||
const tableNameKey = getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: tableId,
|
||||
attribute: 'name',
|
||||
});
|
||||
|
||||
if (diffMap.has(tableNameKey)) {
|
||||
const diff = diffMap.get(tableNameKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as string;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getTableNewColor = useCallback<DiffContext['getTableNewColor']>(
|
||||
({ tableId }) => {
|
||||
const tableColorKey = getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: tableId,
|
||||
attribute: 'color',
|
||||
});
|
||||
|
||||
if (diffMap.has(tableColorKey)) {
|
||||
const diff = diffMap.get(tableColorKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as string;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfTableHasChange = useCallback<
|
||||
DiffContext['checkIfTableHasChange']
|
||||
>(({ tableId }) => tablesChanged.get(tableId) ?? false, [tablesChanged]);
|
||||
|
||||
const checkIfNewTable = useCallback<DiffContext['checkIfNewTable']>(
|
||||
({ tableId }) => {
|
||||
const tableKey = getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: tableId,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(tableKey) && diffMap.get(tableKey)?.type === 'added'
|
||||
);
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfTableRemoved = useCallback<DiffContext['checkIfTableRemoved']>(
|
||||
({ tableId }) => {
|
||||
const tableKey = getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: tableId,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(tableKey) &&
|
||||
diffMap.get(tableKey)?.type === 'removed'
|
||||
);
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfFieldHasChange = useCallback<
|
||||
DiffContext['checkIfFieldHasChange']
|
||||
>(
|
||||
({ fieldId }) => {
|
||||
return fieldsChanged.get(fieldId) ?? false;
|
||||
},
|
||||
[fieldsChanged]
|
||||
);
|
||||
|
||||
const checkIfFieldRemoved = useCallback<DiffContext['checkIfFieldRemoved']>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(fieldKey) &&
|
||||
diffMap.get(fieldKey)?.type === 'removed'
|
||||
);
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfNewField = useCallback<DiffContext['checkIfNewField']>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(fieldKey) && diffMap.get(fieldKey)?.type === 'added'
|
||||
);
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getFieldNewName = useCallback<DiffContext['getFieldNewName']>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
attribute: 'name',
|
||||
});
|
||||
|
||||
if (diffMap.has(fieldKey)) {
|
||||
const diff = diffMap.get(fieldKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as string;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getFieldNewType = useCallback<DiffContext['getFieldNewType']>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
attribute: 'type',
|
||||
});
|
||||
|
||||
if (diffMap.has(fieldKey)) {
|
||||
const diff = diffMap.get(fieldKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as DataType;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfNewRelationship = useCallback<
|
||||
DiffContext['checkIfNewRelationship']
|
||||
>(
|
||||
({ relationshipId }) => {
|
||||
const relationshipKey = getDiffMapKey({
|
||||
diffObject: 'relationship',
|
||||
objectId: relationshipId,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(relationshipKey) &&
|
||||
diffMap.get(relationshipKey)?.type === 'added'
|
||||
);
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfRelationshipRemoved = useCallback<
|
||||
DiffContext['checkIfRelationshipRemoved']
|
||||
>(
|
||||
({ relationshipId }) => {
|
||||
const relationshipKey = getDiffMapKey({
|
||||
diffObject: 'relationship',
|
||||
objectId: relationshipId,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(relationshipKey) &&
|
||||
diffMap.get(relationshipKey)?.type === 'removed'
|
||||
);
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
return (
|
||||
<diffContext.Provider
|
||||
value={{
|
||||
newDiagram,
|
||||
originalDiagram,
|
||||
diffMap,
|
||||
hasDiff: diffMap.size > 0,
|
||||
|
||||
calculateDiff,
|
||||
|
||||
// table diff
|
||||
getTableNewName,
|
||||
checkIfNewTable,
|
||||
checkIfTableRemoved,
|
||||
checkIfTableHasChange,
|
||||
getTableNewColor,
|
||||
|
||||
// field diff
|
||||
checkIfFieldHasChange,
|
||||
checkIfFieldRemoved,
|
||||
checkIfNewField,
|
||||
getFieldNewName,
|
||||
getFieldNewType,
|
||||
|
||||
// relationship diff
|
||||
checkIfNewRelationship,
|
||||
checkIfRelationshipRemoved,
|
||||
|
||||
events,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</diffContext.Provider>
|
||||
);
|
||||
};
|
10
src/context/diff-context/use-diff.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
import { diffContext } from './diff-context';
|
||||
|
||||
export const useDiff = () => {
|
||||
const context = useContext(diffContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useDiff must be used within an DiffProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
@@ -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,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
||||
removeIndex,
|
||||
updateIndex,
|
||||
removeRelationships,
|
||||
addAreas,
|
||||
removeAreas,
|
||||
updateArea,
|
||||
} = useChartDB();
|
||||
|
||||
const redoActionHandlers = useMemo(
|
||||
@@ -107,6 +110,15 @@ 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 });
|
||||
},
|
||||
}),
|
||||
[
|
||||
addTables,
|
||||
@@ -126,6 +138,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
||||
addDependencies,
|
||||
removeDependencies,
|
||||
updateDependency,
|
||||
addAreas,
|
||||
removeAreas,
|
||||
updateArea,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -215,6 +230,15 @@ 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 });
|
||||
},
|
||||
}),
|
||||
[
|
||||
addTables,
|
||||
@@ -234,6 +258,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
||||
addDependencies,
|
||||
removeDependencies,
|
||||
updateDependency,
|
||||
addAreas,
|
||||
removeAreas,
|
||||
updateArea,
|
||||
]
|
||||
);
|
||||
|
||||
|
@@ -4,6 +4,7 @@ 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';
|
||||
|
||||
type Action = keyof ChartDBContext;
|
||||
|
||||
@@ -123,6 +124,24 @@ 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[] }
|
||||
>;
|
||||
|
||||
export type RedoUndoAction =
|
||||
| RedoUndoActionAddTables
|
||||
| RedoUndoActionRemoveTables
|
||||
@@ -140,7 +159,10 @@ export type RedoUndoAction =
|
||||
| RedoUndoActionRemoveRelationships
|
||||
| RedoUndoActionAddDependencies
|
||||
| RedoUndoActionUpdateDependency
|
||||
| RedoUndoActionRemoveDependencies;
|
||||
| RedoUndoActionRemoveDependencies
|
||||
| RedoUndoActionAddAreas
|
||||
| RedoUndoActionUpdateArea
|
||||
| RedoUndoActionRemoveAreas;
|
||||
|
||||
export type RedoActionData<T extends Action> = Extract<
|
||||
RedoUndoAction,
|
||||
|
@@ -7,6 +7,7 @@ export enum KeyboardShortcutAction {
|
||||
SAVE_DIAGRAM = 'save_diagram',
|
||||
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
|
||||
SHOW_ALL = 'show_all',
|
||||
TOGGLE_THEME = 'toggle_theme',
|
||||
}
|
||||
|
||||
export interface KeyboardShortcut {
|
||||
@@ -63,6 +64,13 @@ export const keyboardShortcuts: Record<
|
||||
keyCombinationMac: 'meta+0',
|
||||
keyCombinationWin: 'ctrl+0',
|
||||
},
|
||||
[KeyboardShortcutAction.TOGGLE_THEME]: {
|
||||
action: KeyboardShortcutAction.TOGGLE_THEME,
|
||||
keyCombinationLabelMac: '⌘M',
|
||||
keyCombinationLabelWin: 'Ctrl+M',
|
||||
keyCombinationMac: 'meta+m',
|
||||
keyCombinationWin: 'ctrl+m',
|
||||
},
|
||||
};
|
||||
|
||||
export interface KeyboardShortcutForOS {
|
||||
|
@@ -1,7 +1,11 @@
|
||||
import { emptyFn } from '@/lib/utils';
|
||||
import { createContext } from 'react';
|
||||
|
||||
export type SidebarSection = 'tables' | 'relationships' | 'dependencies';
|
||||
export type SidebarSection =
|
||||
| 'tables'
|
||||
| 'relationships'
|
||||
| 'dependencies'
|
||||
| 'areas';
|
||||
|
||||
export interface LayoutContext {
|
||||
openedTableInSidebar: string | undefined;
|
||||
@@ -16,6 +20,10 @@ export interface LayoutContext {
|
||||
openDependencyFromSidebar: (dependencyId: string) => void;
|
||||
closeAllDependenciesInSidebar: () => void;
|
||||
|
||||
openedAreaInSidebar: string | undefined;
|
||||
openAreaFromSidebar: (areaId: string) => void;
|
||||
closeAllAreasInSidebar: () => void;
|
||||
|
||||
selectedSidebarSection: SidebarSection;
|
||||
selectSidebarSection: (section: SidebarSection) => void;
|
||||
|
||||
@@ -41,6 +49,10 @@ export const layoutContext = createContext<LayoutContext>({
|
||||
openDependencyFromSidebar: emptyFn,
|
||||
closeAllDependenciesInSidebar: emptyFn,
|
||||
|
||||
openedAreaInSidebar: undefined,
|
||||
openAreaFromSidebar: emptyFn,
|
||||
closeAllAreasInSidebar: emptyFn,
|
||||
|
||||
selectSidebarSection: emptyFn,
|
||||
openTableFromSidebar: emptyFn,
|
||||
closeAllTablesInSidebar: emptyFn,
|
||||
|
@@ -14,6 +14,9 @@ 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 [selectedSidebarSection, setSelectedSidebarSection] =
|
||||
React.useState<SidebarSection>('tables');
|
||||
const [isSidePanelShowed, setIsSidePanelShowed] =
|
||||
@@ -30,6 +33,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
||||
const closeAllDependenciesInSidebar: LayoutContext['closeAllDependenciesInSidebar'] =
|
||||
() => setOpenedDependencyInSidebar('');
|
||||
|
||||
const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
|
||||
() => setOpenedAreaInSidebar('');
|
||||
|
||||
const hideSidePanel: LayoutContext['hideSidePanel'] = () =>
|
||||
setIsSidePanelShowed(false);
|
||||
|
||||
@@ -62,6 +68,14 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setOpenedDependencyInSidebar(dependencyId);
|
||||
};
|
||||
|
||||
const openAreaFromSidebar: LayoutContext['openAreaFromSidebar'] = (
|
||||
areaId
|
||||
) => {
|
||||
showSidePanel();
|
||||
setSelectedSidebarSection('areas');
|
||||
setOpenedAreaInSidebar(areaId);
|
||||
};
|
||||
|
||||
const openSelectSchema: LayoutContext['openSelectSchema'] = () =>
|
||||
setIsSelectSchemaOpen(true);
|
||||
|
||||
@@ -88,6 +102,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
||||
openedDependencyInSidebar,
|
||||
openDependencyFromSidebar,
|
||||
closeAllDependenciesInSidebar,
|
||||
openedAreaInSidebar,
|
||||
openAreaFromSidebar,
|
||||
closeAllAreasInSidebar,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@@ -30,12 +30,6 @@ export interface LocalConfigContext {
|
||||
starUsDialogLastOpen: number;
|
||||
setStarUsDialogLastOpen: (lastOpen: number) => void;
|
||||
|
||||
buckleWaitlistOpened: boolean;
|
||||
setBuckleWaitlistOpened: (githubRepoOpened: boolean) => void;
|
||||
|
||||
buckleDialogLastOpen: number;
|
||||
setBuckleDialogLastOpen: (lastOpen: number) => void;
|
||||
|
||||
showDependenciesOnCanvas: boolean;
|
||||
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
|
||||
|
||||
@@ -53,7 +47,7 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
||||
schemasFilter: {},
|
||||
setSchemasFilter: emptyFn,
|
||||
|
||||
showCardinality: false,
|
||||
showCardinality: true,
|
||||
setShowCardinality: emptyFn,
|
||||
|
||||
hideMultiSchemaNotification: false,
|
||||
@@ -65,12 +59,6 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
||||
starUsDialogLastOpen: 0,
|
||||
setStarUsDialogLastOpen: emptyFn,
|
||||
|
||||
buckleWaitlistOpened: false,
|
||||
setBuckleWaitlistOpened: emptyFn,
|
||||
|
||||
buckleDialogLastOpen: 0,
|
||||
setBuckleDialogLastOpen: emptyFn,
|
||||
|
||||
showDependenciesOnCanvas: false,
|
||||
setShowDependenciesOnCanvas: emptyFn,
|
||||
|
||||
|
@@ -10,8 +10,6 @@ const showCardinalityKey = 'show_cardinality';
|
||||
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
|
||||
const githubRepoOpenedKey = 'github_repo_opened';
|
||||
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
||||
const buckleWaitlistOpenedKey = 'buckle_waitlist_opened';
|
||||
const buckleDialogLastOpenKey = 'buckle_dialog_last_open';
|
||||
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
|
||||
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
|
||||
|
||||
@@ -33,7 +31,7 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
);
|
||||
|
||||
const [showCardinality, setShowCardinality] = React.useState<boolean>(
|
||||
(localStorage.getItem(showCardinalityKey) || 'false') === 'true'
|
||||
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
|
||||
);
|
||||
|
||||
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
|
||||
@@ -51,17 +49,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
|
||||
);
|
||||
|
||||
const [buckleWaitlistOpened, setBuckleWaitlistOpened] =
|
||||
React.useState<boolean>(
|
||||
(localStorage.getItem(buckleWaitlistOpenedKey) || 'false') ===
|
||||
'true'
|
||||
);
|
||||
|
||||
const [buckleDialogLastOpen, setBuckleDialogLastOpen] =
|
||||
React.useState<number>(
|
||||
parseInt(localStorage.getItem(buckleDialogLastOpenKey) || '0')
|
||||
);
|
||||
|
||||
const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
|
||||
React.useState<boolean>(
|
||||
(localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
|
||||
@@ -84,20 +71,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
|
||||
}, [githubRepoOpened]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(
|
||||
buckleDialogLastOpenKey,
|
||||
buckleDialogLastOpen.toString()
|
||||
);
|
||||
}, [buckleDialogLastOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(
|
||||
buckleWaitlistOpenedKey,
|
||||
buckleWaitlistOpened.toString()
|
||||
);
|
||||
}, [buckleWaitlistOpened]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(
|
||||
hideMultiSchemaNotificationKey,
|
||||
@@ -154,10 +127,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setStarUsDialogLastOpen,
|
||||
showDependenciesOnCanvas,
|
||||
setShowDependenciesOnCanvas,
|
||||
setBuckleDialogLastOpen,
|
||||
buckleDialogLastOpen,
|
||||
buckleWaitlistOpened,
|
||||
setBuckleWaitlistOpened,
|
||||
showMiniMapOnCanvas,
|
||||
setShowMiniMapOnCanvas,
|
||||
}}
|
||||
|
@@ -5,6 +5,7 @@ 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';
|
||||
|
||||
export interface StorageContext {
|
||||
// Config operations
|
||||
@@ -17,6 +18,7 @@ export interface StorageContext {
|
||||
includeTables?: boolean;
|
||||
includeRelationships?: boolean;
|
||||
includeDependencies?: boolean;
|
||||
includeAreas?: boolean;
|
||||
}) => Promise<Diagram[]>;
|
||||
getDiagram: (
|
||||
id: string,
|
||||
@@ -24,6 +26,7 @@ export interface StorageContext {
|
||||
includeTables?: boolean;
|
||||
includeRelationships?: boolean;
|
||||
includeDependencies?: boolean;
|
||||
includeAreas?: boolean;
|
||||
}
|
||||
) => Promise<Diagram | undefined>;
|
||||
updateDiagram: (params: {
|
||||
@@ -86,6 +89,20 @@ 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>;
|
||||
}
|
||||
|
||||
export const storageInitialValue: StorageContext = {
|
||||
@@ -119,6 +136,13 @@ export const storageInitialValue: StorageContext = {
|
||||
deleteDependency: emptyFn,
|
||||
listDependencies: emptyFn,
|
||||
deleteDiagramDependencies: emptyFn,
|
||||
|
||||
addArea: emptyFn,
|
||||
getArea: emptyFn,
|
||||
updateArea: emptyFn,
|
||||
deleteArea: emptyFn,
|
||||
listAreas: emptyFn,
|
||||
deleteDiagramAreas: emptyFn,
|
||||
};
|
||||
|
||||
export const storageContext =
|
||||
|
@@ -8,6 +8,7 @@ import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||
import { determineCardinalities } from '@/lib/domain/db-relationship';
|
||||
import type { ChartDBConfig } from '@/lib/domain/config';
|
||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||
import type { Area } from '@/lib/domain/area';
|
||||
|
||||
export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
@@ -29,6 +30,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
DBDependency & { diagramId: string },
|
||||
'id' // primary key "id" (for the typings only)
|
||||
>;
|
||||
areas: EntityTable<
|
||||
Area & { diagramId: string },
|
||||
'id' // primary key "id" (for the typings only)
|
||||
>;
|
||||
config: EntityTable<
|
||||
ChartDBConfig & { id: number },
|
||||
'id' // primary key "id" (for the typings only)
|
||||
@@ -148,6 +153,19 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
})
|
||||
);
|
||||
|
||||
db.version(10).stores({
|
||||
diagrams:
|
||||
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
||||
db_tables:
|
||||
'++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment, isView, isMaterializedView, order',
|
||||
db_relationships:
|
||||
'++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
|
||||
db_dependencies:
|
||||
'++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt',
|
||||
areas: '++id, diagramId, name, x, y, width, height, color',
|
||||
config: '++id, defaultDiagramId',
|
||||
});
|
||||
|
||||
db.on('ready', async () => {
|
||||
const config = await getConfig();
|
||||
|
||||
@@ -209,6 +227,11 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
)
|
||||
);
|
||||
|
||||
const areas = diagram.areas ?? [];
|
||||
promises.push(
|
||||
...areas.map((area) => addArea({ diagramId: diagram.id, area }))
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
};
|
||||
|
||||
@@ -217,10 +240,12 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
includeTables?: boolean;
|
||||
includeRelationships?: boolean;
|
||||
includeDependencies?: boolean;
|
||||
includeAreas?: boolean;
|
||||
} = {
|
||||
includeRelationships: false,
|
||||
includeTables: false,
|
||||
includeDependencies: false,
|
||||
includeAreas: false,
|
||||
}
|
||||
): Promise<Diagram[]> => {
|
||||
let diagrams = await db.diagrams.toArray();
|
||||
@@ -252,6 +277,15 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (options.includeAreas) {
|
||||
diagrams = await Promise.all(
|
||||
diagrams.map(async (diagram) => {
|
||||
diagram.areas = await listAreas(diagram.id);
|
||||
return diagram;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return diagrams;
|
||||
};
|
||||
|
||||
@@ -261,10 +295,12 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
includeTables?: boolean;
|
||||
includeRelationships?: boolean;
|
||||
includeDependencies?: boolean;
|
||||
includeAreas?: boolean;
|
||||
} = {
|
||||
includeRelationships: false,
|
||||
includeTables: false,
|
||||
includeDependencies: false,
|
||||
includeAreas: false,
|
||||
}
|
||||
): Promise<Diagram | undefined> => {
|
||||
const diagram = await db.diagrams.get(id);
|
||||
@@ -285,6 +321,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
diagram.dependencies = await listDependencies(id);
|
||||
}
|
||||
|
||||
if (options.includeAreas) {
|
||||
diagram.areas = await listAreas(id);
|
||||
}
|
||||
|
||||
return diagram;
|
||||
};
|
||||
|
||||
@@ -323,6 +363,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
db.db_tables.where('diagramId').equals(id).delete(),
|
||||
db.db_relationships.where('diagramId').equals(id).delete(),
|
||||
db.db_dependencies.where('diagramId').equals(id).delete(),
|
||||
db.areas.where('diagramId').equals(id).delete(),
|
||||
]);
|
||||
};
|
||||
|
||||
@@ -504,6 +545,41 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
.delete();
|
||||
};
|
||||
|
||||
const addArea: StorageContext['addArea'] = async ({ area, diagramId }) => {
|
||||
await db.areas.add({
|
||||
...area,
|
||||
diagramId,
|
||||
});
|
||||
};
|
||||
|
||||
const getArea: StorageContext['getArea'] = async ({ diagramId, id }) => {
|
||||
return await db.areas.get({ id, diagramId });
|
||||
};
|
||||
|
||||
const updateArea: StorageContext['updateArea'] = async ({
|
||||
id,
|
||||
attributes,
|
||||
}) => {
|
||||
await db.areas.update(id, attributes);
|
||||
};
|
||||
|
||||
const deleteArea: StorageContext['deleteArea'] = async ({
|
||||
diagramId,
|
||||
id,
|
||||
}) => {
|
||||
await db.areas.where({ id, diagramId }).delete();
|
||||
};
|
||||
|
||||
const listAreas: StorageContext['listAreas'] = async (diagramId) => {
|
||||
return await db.areas.where('diagramId').equals(diagramId).toArray();
|
||||
};
|
||||
|
||||
const deleteDiagramAreas: StorageContext['deleteDiagramAreas'] = async (
|
||||
diagramId
|
||||
) => {
|
||||
await db.areas.where('diagramId').equals(diagramId).delete();
|
||||
};
|
||||
|
||||
return (
|
||||
<storageContext.Provider
|
||||
value={{
|
||||
@@ -533,6 +609,12 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
deleteDependency,
|
||||
listDependencies,
|
||||
deleteDiagramDependencies,
|
||||
addArea,
|
||||
getArea,
|
||||
updateArea,
|
||||
deleteArea,
|
||||
listAreas,
|
||||
deleteDiagramAreas,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import type { EffectiveTheme } from './theme-context';
|
||||
import { ThemeContext } from './theme-context';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import {
|
||||
KeyboardShortcutAction,
|
||||
keyboardShortcutsForOS,
|
||||
} from '../keyboard-shortcuts-context/keyboard-shortcuts';
|
||||
|
||||
export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
@@ -29,6 +34,24 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
||||
}
|
||||
}, [effectiveTheme]);
|
||||
|
||||
const handleThemeToggle = useCallback(() => {
|
||||
if (theme === 'system') {
|
||||
setTheme(effectiveTheme === 'dark' ? 'light' : 'dark');
|
||||
} else {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
}
|
||||
}, [theme, effectiveTheme, setTheme]);
|
||||
|
||||
useHotkeys(
|
||||
keyboardShortcutsForOS[KeyboardShortcutAction.TOGGLE_THEME]
|
||||
.keyCombination,
|
||||
handleThemeToggle,
|
||||
{
|
||||
preventDefault: true,
|
||||
},
|
||||
[handleThemeToggle]
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}>
|
||||
{children}
|
||||
|
@@ -1,80 +0,0 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDialog } from '@/hooks/use-dialog';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/dialog/dialog';
|
||||
import { Button } from '@/components/button/button';
|
||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
|
||||
export interface BuckleDialogProps extends BaseDialogProps {}
|
||||
|
||||
export const BuckleDialog: React.FC<BuckleDialogProps> = ({ dialog }) => {
|
||||
const { setBuckleWaitlistOpened } = useLocalConfig();
|
||||
const { effectiveTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialog.open) return;
|
||||
}, [dialog.open]);
|
||||
const { closeBuckleDialog } = useDialog();
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
setBuckleWaitlistOpened(true);
|
||||
window.open('https://waitlist.buckle.dev', '_blank');
|
||||
}, [setBuckleWaitlistOpened]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialog}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
closeBuckleDialog();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="flex flex-col"
|
||||
showClose={false}
|
||||
onInteractOutside={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="hidden" />
|
||||
<DialogDescription className="hidden" />
|
||||
</DialogHeader>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<img
|
||||
src={
|
||||
effectiveTheme === 'light'
|
||||
? '/buckle-animated.gif'
|
||||
: '/buckle.png'
|
||||
}
|
||||
className="h-16"
|
||||
/>
|
||||
<div className="mt-6 text-center text-base">
|
||||
We've been working on something big -{' '}
|
||||
<span className="font-semibold">Ready to explore?</span>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="flex gap-1 md:justify-between">
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">Not now</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={handleConfirm}>
|
||||
Try ChartDB v2.0!
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
@@ -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,63 @@ import {
|
||||
fixMetadataJson,
|
||||
isStringMetadataJson,
|
||||
} from '@/lib/data/import-metadata/utils';
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from '@/components/resizable/resizable';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
import type { OnChange } from '@monaco-editor/react';
|
||||
import { useDebounce } from '@/hooks/use-debounce-v2';
|
||||
import { InstructionsSection } from './instructions-section/instructions-section';
|
||||
import { parseSQLError } from '@/lib/data/sql-import';
|
||||
import type { editor } from 'monaco-editor';
|
||||
|
||||
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 +96,8 @@ export interface ImportDatabaseProps {
|
||||
>;
|
||||
keepDialogAfterImport?: boolean;
|
||||
title: string;
|
||||
importMethod: 'query' | 'ddl';
|
||||
setImportMethod: (method: 'query' | 'ddl') => void;
|
||||
}
|
||||
|
||||
export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
@@ -70,34 +111,52 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
setDatabaseEdition,
|
||||
keepDialogAfterImport,
|
||||
title,
|
||||
importMethod,
|
||||
setImportMethod,
|
||||
}) => {
|
||||
const databaseClients = databaseTypeToClientsMap[databaseType];
|
||||
const { effectiveTheme } = useTheme();
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [databaseClient, setDatabaseClient] = useState<
|
||||
DatabaseClient | undefined
|
||||
>();
|
||||
const { t } = useTranslation();
|
||||
const [importMetadataScripts, setImportMetadataScripts] =
|
||||
useState<ImportMetadataScripts | null>(null);
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { isSm: isDesktop } = useBreakpoint('sm');
|
||||
|
||||
const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
|
||||
const [isCheckingJson, setIsCheckingJson] = useState(false);
|
||||
|
||||
const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadScripts = async () => {
|
||||
const { importMetadataScripts } = await import(
|
||||
'@/lib/data/import-metadata/scripts/scripts'
|
||||
);
|
||||
setImportMetadataScripts(importMetadataScripts);
|
||||
};
|
||||
loadScripts();
|
||||
}, []);
|
||||
setScriptResult('');
|
||||
setErrorMessage('');
|
||||
setShowCheckJsonButton(false);
|
||||
}, [importMethod, setScriptResult]);
|
||||
|
||||
// Check if the ddl is valid
|
||||
useEffect(() => {
|
||||
if (importMethod !== 'ddl') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!scriptResult.trim()) return;
|
||||
|
||||
parseSQLError({
|
||||
sqlContent: scriptResult,
|
||||
sourceDatabaseType: databaseType,
|
||||
}).then((result) => {
|
||||
if (result.success) {
|
||||
setErrorMessage('');
|
||||
} else if (!result.success && result.error) {
|
||||
setErrorMessage(result.error);
|
||||
}
|
||||
});
|
||||
}, [importMethod, scriptResult, databaseType]);
|
||||
|
||||
// Check if the script result is a valid JSON
|
||||
useEffect(() => {
|
||||
if (importMethod !== 'query') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (scriptResult.trim().length === 0) {
|
||||
setErrorMessage('');
|
||||
setShowCheckJsonButton(false);
|
||||
@@ -117,7 +176,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,19 +184,30 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
}
|
||||
}, [errorMessage.length, onImport, scriptResult]);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const inputValue = e.target.value;
|
||||
setScriptResult(inputValue);
|
||||
const formatEditor = useCallback(() => {
|
||||
if (editorRef.current) {
|
||||
setTimeout(() => {
|
||||
editorRef.current
|
||||
?.getAction('editor.action.formatDocument')
|
||||
?.run();
|
||||
}, 50);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleInputChange: OnChange = useCallback(
|
||||
(inputValue) => {
|
||||
setScriptResult(inputValue ?? '');
|
||||
|
||||
// Automatically open SSMS info when input length is exactly 65535
|
||||
if (inputValue.length === 65535) {
|
||||
if ((inputValue ?? '').length === 65535) {
|
||||
setShowSSMSInfoDialog(true);
|
||||
}
|
||||
},
|
||||
[setScriptResult]
|
||||
);
|
||||
|
||||
const debouncedHandleInputChange = useDebounce(handleInputChange, 500);
|
||||
|
||||
const handleCheckJson = useCallback(async () => {
|
||||
setIsCheckingJson(true);
|
||||
|
||||
@@ -146,14 +216,49 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
if (isStringMetadataJson(fixedJson)) {
|
||||
setScriptResult(fixedJson);
|
||||
setErrorMessage('');
|
||||
formatEditor();
|
||||
} else {
|
||||
setScriptResult(fixedJson);
|
||||
setErrorMessage(errorScriptOutputMessage);
|
||||
formatEditor();
|
||||
}
|
||||
|
||||
setShowCheckJsonButton(false);
|
||||
setIsCheckingJson(false);
|
||||
}, [scriptResult, setScriptResult]);
|
||||
}, [scriptResult, setScriptResult, formatEditor]);
|
||||
|
||||
const detectAndSetImportMethod = useCallback(() => {
|
||||
const content = editorRef.current?.getValue();
|
||||
if (content && content.trim()) {
|
||||
const detectedType = detectContentType(content);
|
||||
if (detectedType && detectedType !== importMethod) {
|
||||
setImportMethod(detectedType);
|
||||
}
|
||||
}
|
||||
}, [setImportMethod, importMethod]);
|
||||
|
||||
const [editorDidMount, setEditorDidMount] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorRef.current && editorDidMount) {
|
||||
editorRef.current.onDidPaste(() => {
|
||||
setTimeout(() => {
|
||||
editorRef.current
|
||||
?.getAction('editor.action.formatDocument')
|
||||
?.run();
|
||||
}, 0);
|
||||
setTimeout(detectAndSetImportMethod, 0);
|
||||
});
|
||||
}
|
||||
}, [detectAndSetImportMethod, editorDidMount]);
|
||||
|
||||
const handleEditorDidMount = useCallback(
|
||||
(editor: editor.IStandaloneCodeEditor) => {
|
||||
editorRef.current = editor;
|
||||
setEditorDidMount(true);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const renderHeader = useCallback(() => {
|
||||
return (
|
||||
@@ -164,228 +269,131 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
);
|
||||
}, [title]);
|
||||
|
||||
const renderInstructions = useCallback(
|
||||
() => (
|
||||
<InstructionsSection
|
||||
databaseType={databaseType}
|
||||
importMethod={importMethod}
|
||||
setDatabaseEdition={setDatabaseEdition}
|
||||
setImportMethod={setImportMethod}
|
||||
databaseEdition={databaseEdition}
|
||||
setShowSSMSInfoDialog={setShowSSMSInfoDialog}
|
||||
showSSMSInfoDialog={showSSMSInfoDialog}
|
||||
/>
|
||||
),
|
||||
[
|
||||
databaseType,
|
||||
importMethod,
|
||||
setDatabaseEdition,
|
||||
setImportMethod,
|
||||
databaseEdition,
|
||||
setShowSSMSInfoDialog,
|
||||
showSSMSInfoDialog,
|
||||
]
|
||||
);
|
||||
|
||||
const renderOutputTextArea = useCallback(
|
||||
() => (
|
||||
<div className="flex size-full flex-col gap-1 overflow-hidden rounded-md border p-1">
|
||||
<div className="w-full text-center text-xs text-muted-foreground">
|
||||
{importMethod === 'query'
|
||||
? 'Smart Query Output'
|
||||
: 'SQL Script'}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Editor
|
||||
value={scriptResult}
|
||||
onChange={debouncedHandleInputChange}
|
||||
language={importMethod === 'query' ? 'json' : 'sql'}
|
||||
loading={<Spinner />}
|
||||
onMount={handleEditorDidMount}
|
||||
theme={
|
||||
effectiveTheme === 'dark'
|
||||
? 'dbml-dark'
|
||||
: 'dbml-light'
|
||||
}
|
||||
options={{
|
||||
formatOnPaste: true,
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
glyphMargin: false,
|
||||
lineNumbers: 'on',
|
||||
guides: {
|
||||
indentation: false,
|
||||
},
|
||||
folding: true,
|
||||
lineNumbersMinChars: 3,
|
||||
renderValidationDecorations: 'off',
|
||||
lineDecorationsWidth: 0,
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
hideCursorInOverviewRuler: true,
|
||||
contextmenu: false,
|
||||
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
alwaysConsumeMouseWheel: false,
|
||||
},
|
||||
}}
|
||||
className="size-full min-h-40"
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
{errorMessage ? (
|
||||
<div className="mt-2 flex shrink-0 items-center gap-2">
|
||||
<p className="text-xs text-red-700">{errorMessage}</p>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
),
|
||||
[
|
||||
errorMessage,
|
||||
scriptResult,
|
||||
importMethod,
|
||||
effectiveTheme,
|
||||
debouncedHandleInputChange,
|
||||
handleEditorDidMount,
|
||||
]
|
||||
);
|
||||
|
||||
const renderContent = useCallback(() => {
|
||||
return (
|
||||
<DialogInternalContent>
|
||||
<div className="flex w-full flex-1 flex-col gap-6">
|
||||
{databaseTypeToEditionMap[databaseType].length > 0 ? (
|
||||
<div className="flex flex-col gap-1 md:flex-row">
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
{t(
|
||||
'new_diagram_dialog.import_database.database_edition'
|
||||
)}
|
||||
</p>
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
className="ml-1 flex-wrap gap-2"
|
||||
value={
|
||||
!databaseEdition
|
||||
? 'regular'
|
||||
: databaseEdition
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
setDatabaseEdition(
|
||||
value === 'regular'
|
||||
? undefined
|
||||
: (value as DatabaseEdition)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value="regular"
|
||||
variant="outline"
|
||||
className="h-6 gap-1 p-0 px-2 shadow-none"
|
||||
>
|
||||
<Avatar className="size-4 rounded-none">
|
||||
<AvatarImage
|
||||
src={
|
||||
databaseSecondaryLogoMap[
|
||||
databaseType
|
||||
]
|
||||
}
|
||||
alt="Regular"
|
||||
/>
|
||||
<AvatarFallback>Regular</AvatarFallback>
|
||||
</Avatar>
|
||||
Regular
|
||||
</ToggleGroupItem>
|
||||
{databaseTypeToEditionMap[databaseType].map(
|
||||
(edition) => (
|
||||
<ToggleGroupItem
|
||||
value={edition}
|
||||
key={edition}
|
||||
variant="outline"
|
||||
className="h-6 gap-1 p-0 px-2 shadow-none"
|
||||
>
|
||||
<Avatar className="size-4">
|
||||
<AvatarImage
|
||||
src={
|
||||
databaseEditionToImageMap[
|
||||
edition
|
||||
]
|
||||
}
|
||||
alt={
|
||||
databaseEditionToLabelMap[
|
||||
edition
|
||||
]
|
||||
}
|
||||
/>
|
||||
<AvatarFallback>
|
||||
{
|
||||
databaseEditionToLabelMap[
|
||||
edition
|
||||
]
|
||||
}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{databaseEditionToLabelMap[edition]}
|
||||
</ToggleGroupItem>
|
||||
)
|
||||
)}
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-col gap-1 text-sm text-muted-foreground md:flex-row md:justify-between">
|
||||
<div>
|
||||
1.{' '}
|
||||
{t('new_diagram_dialog.import_database.step_1')}
|
||||
</div>
|
||||
{databaseType === DatabaseType.SQL_SERVER && (
|
||||
<SSMSInfo
|
||||
open={showSSMSInfoDialog}
|
||||
setOpen={setShowSSMSInfoDialog}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{databaseTypeToClientsMap[databaseType].length > 0 ? (
|
||||
<Tabs
|
||||
value={
|
||||
!databaseClient
|
||||
? 'dbclient'
|
||||
: databaseClient
|
||||
}
|
||||
onValueChange={(value) => {
|
||||
setDatabaseClient(
|
||||
value === 'dbclient'
|
||||
? undefined
|
||||
: (value as DatabaseClient)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-1">
|
||||
<TabsList className="h-8 justify-start rounded-none rounded-t-sm ">
|
||||
<TabsTrigger
|
||||
value="dbclient"
|
||||
className="h-6 w-20"
|
||||
>
|
||||
DB Client
|
||||
</TabsTrigger>
|
||||
|
||||
{databaseClients?.map((client) => (
|
||||
<TabsTrigger
|
||||
key={client}
|
||||
value={client}
|
||||
className="h-6 !w-20"
|
||||
>
|
||||
{
|
||||
databaseClientToLabelMap[
|
||||
client
|
||||
]
|
||||
}
|
||||
</TabsTrigger>
|
||||
)) ?? []}
|
||||
</TabsList>
|
||||
</div>
|
||||
<CodeSnippet
|
||||
className="h-40 w-full"
|
||||
loading={!importMetadataScripts}
|
||||
code={
|
||||
importMetadataScripts?.[databaseType]?.(
|
||||
{
|
||||
databaseEdition,
|
||||
databaseClient,
|
||||
}
|
||||
) ?? ''
|
||||
}
|
||||
language={databaseClient ? 'shell' : 'sql'}
|
||||
/>
|
||||
</Tabs>
|
||||
) : (
|
||||
<CodeSnippet
|
||||
className="h-40 w-full flex-auto"
|
||||
loading={!importMetadataScripts}
|
||||
code={
|
||||
importMetadataScripts?.[databaseType]?.({
|
||||
databaseEdition,
|
||||
}) ?? ''
|
||||
}
|
||||
language="sql"
|
||||
/>
|
||||
)}
|
||||
{isDesktop ? (
|
||||
<ResizablePanelGroup
|
||||
direction={isDesktop ? 'horizontal' : 'vertical'}
|
||||
className="min-h-[500px] md:min-h-fit"
|
||||
>
|
||||
<ResizablePanel
|
||||
defaultSize={25}
|
||||
minSize={25}
|
||||
maxSize={99}
|
||||
className="min-h-fit rounded-md bg-gradient-to-b from-slate-50 to-slate-100 p-2 dark:from-slate-900 dark:to-slate-800 md:min-h-fit md:min-w-[350px] md:rounded-l-md md:p-2"
|
||||
>
|
||||
{renderInstructions()}
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel className="min-h-40 py-2 md:px-2 md:py-0">
|
||||
{renderOutputTextArea()}
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{renderInstructions()}
|
||||
{renderOutputTextArea()}
|
||||
</div>
|
||||
<div className="flex h-48 flex-col gap-1">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
2. {t('new_diagram_dialog.import_database.step_2')}
|
||||
</p>
|
||||
<Textarea
|
||||
className="w-full flex-1 rounded-md bg-muted p-2 text-sm"
|
||||
placeholder={t(
|
||||
'new_diagram_dialog.import_database.script_results_placeholder'
|
||||
)}
|
||||
value={scriptResult}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{showCheckJsonButton || errorMessage ? (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
{showCheckJsonButton ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCheckJson}
|
||||
disabled={isCheckingJson}
|
||||
>
|
||||
{isCheckingJson ? (
|
||||
<Spinner size="small" />
|
||||
) : (
|
||||
t(
|
||||
'new_diagram_dialog.import_database.check_script_result'
|
||||
)
|
||||
)}
|
||||
</Button>
|
||||
) : (
|
||||
<p className="text-sm text-red-700">
|
||||
{errorMessage}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogInternalContent>
|
||||
);
|
||||
}, [
|
||||
databaseEdition,
|
||||
databaseType,
|
||||
errorMessage,
|
||||
handleInputChange,
|
||||
scriptResult,
|
||||
setDatabaseEdition,
|
||||
databaseClients,
|
||||
databaseClient,
|
||||
importMetadataScripts,
|
||||
t,
|
||||
showCheckJsonButton,
|
||||
isCheckingJson,
|
||||
handleCheckJson,
|
||||
showSSMSInfoDialog,
|
||||
setShowSSMSInfoDialog,
|
||||
]);
|
||||
}, [renderOutputTextArea, renderInstructions, isDesktop]);
|
||||
|
||||
const renderFooter = useCallback(() => {
|
||||
return (
|
||||
<DialogFooter className="mt-4 flex !justify-between gap-2">
|
||||
<DialogFooter className="flex !justify-between gap-2">
|
||||
<div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
|
||||
{goBack && (
|
||||
<Button
|
||||
@@ -419,7 +427,22 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
</DialogClose>
|
||||
)}
|
||||
|
||||
{keepDialogAfterImport ? (
|
||||
{showCheckJsonButton ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
onClick={handleCheckJson}
|
||||
disabled={isCheckingJson}
|
||||
>
|
||||
{isCheckingJson ? (
|
||||
<Spinner size="small" />
|
||||
) : (
|
||||
t(
|
||||
'new_diagram_dialog.import_database.check_script_result'
|
||||
)
|
||||
)}
|
||||
</Button>
|
||||
) : keepDialogAfterImport ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
@@ -437,7 +460,6 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
type="button"
|
||||
variant="default"
|
||||
disabled={
|
||||
showCheckJsonButton ||
|
||||
scriptResult.trim().length === 0 ||
|
||||
errorMessage.length > 0
|
||||
}
|
||||
@@ -468,6 +490,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
errorMessage.length,
|
||||
scriptResult,
|
||||
showCheckJsonButton,
|
||||
isCheckingJson,
|
||||
handleCheckJson,
|
||||
goBack,
|
||||
t,
|
||||
]);
|
||||
|
@@ -0,0 +1,178 @@
|
||||
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,
|
||||
];
|
||||
|
||||
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,117 @@
|
||||
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.',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
@@ -17,6 +17,7 @@ import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
|
||||
import { ImportDatabase } from '../common/import-database/import-database';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||
import { sqlImportToDiagram } from '@/lib/data/sql-import';
|
||||
|
||||
export interface CreateDiagramDialogProps extends BaseDialogProps {}
|
||||
|
||||
@@ -25,6 +26,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
}) => {
|
||||
const { diagramId } = useChartDB();
|
||||
const { t } = useTranslation();
|
||||
const [importMethod, setImportMethod] = useState<'query' | 'ddl'>('query');
|
||||
const [databaseType, setDatabaseType] = useState<DatabaseType>(
|
||||
DatabaseType.GENERIC
|
||||
);
|
||||
@@ -41,6 +43,11 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
const [diagramNumber, setDiagramNumber] = useState<number>(1);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
setDatabaseEdition(undefined);
|
||||
setImportMethod('query');
|
||||
}, [databaseType]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDiagrams = async () => {
|
||||
const diagrams = await listDiagrams();
|
||||
@@ -54,29 +61,41 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
setDatabaseType(DatabaseType.GENERIC);
|
||||
setDatabaseEdition(undefined);
|
||||
setScriptResult('');
|
||||
setImportMethod('query');
|
||||
}, [dialog.open]);
|
||||
|
||||
const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
|
||||
|
||||
const importNewDiagram = useCallback(async () => {
|
||||
const databaseMetadata: DatabaseMetadata =
|
||||
loadDatabaseMetadata(scriptResult);
|
||||
let diagram: Diagram | undefined;
|
||||
|
||||
const diagram = await loadFromDatabaseMetadata({
|
||||
databaseType,
|
||||
databaseMetadata,
|
||||
diagramNumber,
|
||||
databaseEdition:
|
||||
databaseEdition?.trim().length === 0
|
||||
? undefined
|
||||
: databaseEdition,
|
||||
});
|
||||
if (importMethod === 'ddl') {
|
||||
diagram = await sqlImportToDiagram({
|
||||
sqlContent: scriptResult,
|
||||
sourceDatabaseType: databaseType,
|
||||
targetDatabaseType: databaseType,
|
||||
});
|
||||
} else {
|
||||
const databaseMetadata: DatabaseMetadata =
|
||||
loadDatabaseMetadata(scriptResult);
|
||||
|
||||
diagram = await loadFromDatabaseMetadata({
|
||||
databaseType,
|
||||
databaseMetadata,
|
||||
diagramNumber,
|
||||
databaseEdition:
|
||||
databaseEdition?.trim().length === 0
|
||||
? undefined
|
||||
: databaseEdition,
|
||||
});
|
||||
}
|
||||
|
||||
await addDiagram({ diagram });
|
||||
await updateConfig({ defaultDiagramId: diagram.id });
|
||||
await updateConfig({ config: { defaultDiagramId: diagram.id } });
|
||||
closeCreateDiagramDialog();
|
||||
navigate(`/diagrams/${diagram.id}`);
|
||||
}, [
|
||||
importMethod,
|
||||
databaseType,
|
||||
addDiagram,
|
||||
databaseEdition,
|
||||
@@ -101,7 +120,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
};
|
||||
|
||||
await addDiagram({ diagram });
|
||||
await updateConfig({ defaultDiagramId: diagram.id });
|
||||
await updateConfig({ config: { defaultDiagramId: diagram.id } });
|
||||
closeCreateDiagramDialog();
|
||||
navigate(`/diagrams/${diagram.id}`);
|
||||
setTimeout(
|
||||
@@ -133,7 +152,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="flex max-h-screen w-[90vw] max-w-[90vw] flex-col overflow-y-auto md:overflow-visible lg:max-w-[60vw] xl:lg:max-w-lg xl:min-w-[45vw]"
|
||||
className="flex max-h-dvh w-full flex-col md:max-w-[900px]"
|
||||
showClose={hasExistingDiagram}
|
||||
>
|
||||
{step === CreateDiagramDialogStep.SELECT_DATABASE ? (
|
||||
@@ -159,6 +178,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
}
|
||||
setScriptResult={setScriptResult}
|
||||
title={t('new_diagram_dialog.import_database.title')}
|
||||
importMethod={importMethod}
|
||||
setImportMethod={setImportMethod}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
@@ -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>
|
||||
|
@@ -23,7 +23,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Editor } from '@/components/code-snippet/code-snippet';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { importDBMLToDiagram } from '@/lib/dbml-import';
|
||||
import { importDBMLToDiagram, sanitizeDBML } from '@/lib/dbml-import';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { Parser } from '@dbml/core';
|
||||
import { useCanvas } from '@/hooks/use-canvas';
|
||||
@@ -189,8 +189,9 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
if (!content.trim()) return;
|
||||
|
||||
try {
|
||||
const sanitizedContent = sanitizeDBML(content);
|
||||
const parser = new Parser();
|
||||
parser.parse(content, 'dbml');
|
||||
parser.parse(sanitizedContent, 'dbml');
|
||||
} catch (e) {
|
||||
const parsedError = parseDBMLError(e);
|
||||
if (parsedError) {
|
||||
@@ -241,7 +242,9 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
if (!dbmlContent.trim() || errorMessage) return;
|
||||
|
||||
try {
|
||||
const importedDiagram = await importDBMLToDiagram(dbmlContent);
|
||||
// Sanitize DBML content before importing
|
||||
const sanitizedContent = sanitizeDBML(dbmlContent);
|
||||
const importedDiagram = await importDBMLToDiagram(sanitizedContent);
|
||||
const tableIdsToRemove = tables
|
||||
.filter((table) =>
|
||||
importedDiagram.tables?.some(
|
||||
@@ -330,7 +333,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="flex h-[80vh] max-h-screen flex-col"
|
||||
className="flex h-[80vh] max-h-screen w-full flex-col md:max-w-[900px]"
|
||||
showClose
|
||||
>
|
||||
<DialogHeader>
|
||||
|
@@ -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}`);
|
||||
}
|
||||
},
|
||||
|
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
47
src/hooks/use-debounce-v2.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { debounce as utilsDebounce } from '@/lib/utils';
|
||||
|
||||
interface DebouncedFunction {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(...args: any[]): void;
|
||||
cancel?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook that returns a debounced version of the provided function.
|
||||
* The debounced function will only be called after the specified delay
|
||||
* has passed without the function being called again.
|
||||
*
|
||||
* @param callback The function to debounce
|
||||
* @param delay The delay in milliseconds
|
||||
* @returns A debounced version of the callback
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function useDebounce<T extends (...args: any[]) => any>(
|
||||
callback: T,
|
||||
delay: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
// Use a ref to store the debounced function
|
||||
const debouncedFnRef = useRef<DebouncedFunction>();
|
||||
|
||||
// Update the debounced function when dependencies change
|
||||
useEffect(() => {
|
||||
// Create the debounced function
|
||||
debouncedFnRef.current = utilsDebounce(callback, delay);
|
||||
|
||||
// Clean up when component unmounts or dependencies change
|
||||
return () => {
|
||||
if (debouncedFnRef.current?.cancel) {
|
||||
debouncedFnRef.current.cancel();
|
||||
}
|
||||
};
|
||||
}, [callback, delay]);
|
||||
|
||||
// Create a stable callback that uses the ref
|
||||
const debouncedCallback = useCallback((...args: Parameters<T>) => {
|
||||
debouncedFnRef.current?.(...args);
|
||||
}, []);
|
||||
|
||||
return debouncedCallback;
|
||||
}
|
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;
|
||||
}
|
@@ -42,9 +42,7 @@ export const ar: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'مساعدة',
|
||||
docs_website: 'الوثائق',
|
||||
visit_website: 'ChartDB قم بزيارة',
|
||||
join_discord: 'Discord انضم إلينا على',
|
||||
schedule_a_call: '!تحدث معنا',
|
||||
join_discord: 'انضم إلينا على Discord',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -151,6 +149,8 @@ export const ar: LanguageTranslation = {
|
||||
comments: 'تعليقات',
|
||||
no_comments: 'لا يوجد تعليقات',
|
||||
delete_field: 'حذف الحقل',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'خصائص الفهرس',
|
||||
@@ -210,6 +210,27 @@ 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -236,7 +257,7 @@ export const ar: LanguageTranslation = {
|
||||
title: 'إسترد قاعدة بياناتك',
|
||||
database_edition: ':إصدار قاعدة البيانات',
|
||||
step_1: ':قم بتشغيل هذا البرنامج النصي في قاعدة بياناتك',
|
||||
step_2: ':إلصق نتيجة البرنامج النصي هنا',
|
||||
step_2: ':إلصق نتيجة البرنامج النصي هنا →',
|
||||
script_results_placeholder: '...نتيجة البرنامج النصي هنا',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS تعليمات',
|
||||
@@ -330,6 +351,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: {
|
||||
@@ -362,7 +389,7 @@ export const ar: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'حدث خطأ أثناء التصدير',
|
||||
description:
|
||||
'chartdb.io@gmail.com حدث خطأ ما. هل تحتاج إلى مساعدة؟',
|
||||
'support@chartdb.io حدث خطأ ما. هل تحتاج إلى مساعدة؟',
|
||||
},
|
||||
},
|
||||
import_diagram_dialog: {
|
||||
@@ -373,7 +400,7 @@ export const ar: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'حدث خطأ أثناء الاستيراد',
|
||||
description:
|
||||
'chartdb.io@gmail.com و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
|
||||
'support@chartdb.io و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
|
||||
},
|
||||
},
|
||||
import_dbml_dialog: {
|
||||
@@ -400,6 +427,8 @@ export const ar: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'جدول جديد',
|
||||
new_relationship: 'علاقة جديدة',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
@@ -43,9 +43,7 @@ export const bn: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'সাহায্য',
|
||||
docs_website: 'ডকুমেন্টেশন',
|
||||
visit_website: 'ChartDB ওয়েবসাইটে যান',
|
||||
join_discord: 'আমাদের Discord-এ যোগ দিন',
|
||||
schedule_a_call: 'আমাদের সাথে কথা বলুন!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -152,6 +150,8 @@ export const bn: LanguageTranslation = {
|
||||
comments: 'মন্তব্য',
|
||||
no_comments: 'কোনো মন্তব্য নেই',
|
||||
delete_field: 'ফিল্ড মুছুন',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'ইনডেক্স কর্ম',
|
||||
@@ -211,6 +211,27 @@ 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -237,7 +258,7 @@ export const bn: LanguageTranslation = {
|
||||
title: 'আপনার ডাটাবেস আমদানি করুন',
|
||||
database_edition: 'ডাটাবেস সংস্করণ:',
|
||||
step_1: 'আপনার ডাটাবেসে এই স্ক্রিপ্ট চালান:',
|
||||
step_2: 'স্ক্রিপ্টের ফলাফল এখানে পেস্ট করুন:',
|
||||
step_2: 'স্ক্রিপ্টের ফলাফল এখানে পেস্ট করুন →',
|
||||
script_results_placeholder: 'স্ক্রিপ্টের ফলাফল এখানে...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS নির্দেশনা',
|
||||
@@ -331,6 +352,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: {
|
||||
@@ -365,7 +392,7 @@ export const bn: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'চিত্র রপ্তানিতে ত্রুটি',
|
||||
description:
|
||||
'কিছু ভুল হয়েছে। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
|
||||
'কিছু ভুল হয়েছে। সাহায্যের প্রয়োজন? support@chartdb.io-এ যোগাযোগ করুন।',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -377,7 +404,7 @@ export const bn: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'চিত্র আমদানিতে ত্রুটি',
|
||||
description:
|
||||
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
|
||||
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? support@chartdb.io-এ যোগাযোগ করুন।',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -404,6 +431,8 @@ export const bn: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'নতুন টেবিল',
|
||||
new_relationship: 'নতুন সম্পর্ক',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
@@ -43,9 +43,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',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -153,6 +151,8 @@ export const de: LanguageTranslation = {
|
||||
comments: 'Kommentare',
|
||||
no_comments: 'Keine Kommentare',
|
||||
delete_field: 'Feld löschen',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Indexattribute',
|
||||
@@ -213,6 +213,27 @@ 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -239,7 +260,7 @@ export const de: LanguageTranslation = {
|
||||
title: 'Datenbank importieren',
|
||||
database_edition: 'Datenbank Edition:',
|
||||
step_1: 'Führen Sie dieses Skript in Ihrer Datenbank aus:',
|
||||
step_2: 'Fügen Sie das Skriptergebnis hier ein:',
|
||||
step_2: 'Fügen Sie das Skriptergebnis hier ein →',
|
||||
script_results_placeholder: 'Skriptergebnisse hier...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS Anweisungen',
|
||||
@@ -334,6 +355,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: {
|
||||
@@ -368,7 +395,7 @@ export const de: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error exporting diagram',
|
||||
description:
|
||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
||||
'Something went wrong. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -380,7 +407,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
|
||||
@@ -407,6 +434,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: {
|
||||
|
@@ -41,9 +41,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!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -145,6 +143,7 @@ export const en = {
|
||||
field_actions: {
|
||||
title: 'Field Attributes',
|
||||
unique: 'Unique',
|
||||
character_length: 'Max Length',
|
||||
comments: 'Comments',
|
||||
no_comments: 'No comments',
|
||||
delete_field: 'Delete Field',
|
||||
@@ -207,6 +206,26 @@ 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -233,7 +252,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',
|
||||
@@ -327,6 +346,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: {
|
||||
@@ -360,7 +384,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',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -372,7 +396,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',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -399,6 +423,7 @@ export const en = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'New Table',
|
||||
new_relationship: 'New Relationship',
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
@@ -42,9 +42,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!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -142,6 +140,8 @@ export const es: LanguageTranslation = {
|
||||
comments: 'Comentarios',
|
||||
no_comments: 'Sin comentarios',
|
||||
delete_field: 'Eliminar Campo',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Atributos del Índice',
|
||||
@@ -201,6 +201,27 @@ 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -227,7 +248,7 @@ export const es: LanguageTranslation = {
|
||||
title: 'Importa tu Base de Datos',
|
||||
database_edition: 'Edición de Base de Datos:',
|
||||
step_1: 'Ejecuta este script en tu base de datos:',
|
||||
step_2: 'Pega el resultado del script aquí:',
|
||||
step_2: 'Pega el resultado del script aquí →',
|
||||
script_results_placeholder: 'Resultados del script aquí...',
|
||||
ssms_instructions: {
|
||||
button_text: 'Instrucciones SSMS',
|
||||
@@ -323,6 +344,12 @@ export const es: LanguageTranslation = {
|
||||
scale_4x: '4x',
|
||||
cancel: 'Cancelar',
|
||||
export: 'Exportar',
|
||||
// TODO: Translate
|
||||
advanced_options: 'Advanced Options',
|
||||
pattern: 'Include background pattern',
|
||||
pattern_description: 'Add subtle grid pattern to background.',
|
||||
transparent: 'Transparent background',
|
||||
transparent_description: 'Remove background color from image.',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
@@ -366,7 +393,7 @@ export const es: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error exporting diagram',
|
||||
description:
|
||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
||||
'Something went wrong. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -378,7 +405,7 @@ export const es: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error importing diagram',
|
||||
description:
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -405,6 +432,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: {
|
||||
|
@@ -41,9 +41,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 !',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -140,6 +138,8 @@ export const fr: LanguageTranslation = {
|
||||
comments: 'Commentaires',
|
||||
no_comments: 'Pas de commentaires',
|
||||
delete_field: 'Supprimer le Champ',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: "Attributs de l'Index",
|
||||
@@ -199,6 +199,27 @@ 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -225,7 +246,7 @@ export const fr: LanguageTranslation = {
|
||||
title: 'Importer votre Base de Données',
|
||||
database_edition: 'Édition de la Base de Données :',
|
||||
step_1: 'Exécutez ce script dans votre base de données :',
|
||||
step_2: 'Collez le résultat du script ici :',
|
||||
step_2: 'Collez le résultat du script ici →',
|
||||
script_results_placeholder: 'Résultats du script ici...',
|
||||
ssms_instructions: {
|
||||
button_text: 'Instructions SSMS',
|
||||
@@ -286,6 +307,12 @@ export const fr: LanguageTranslation = {
|
||||
scale_4x: '4x',
|
||||
cancel: 'Annuler',
|
||||
export: 'Exporter',
|
||||
// TODO: Translate
|
||||
advanced_options: 'Advanced Options',
|
||||
pattern: 'Include background pattern',
|
||||
pattern_description: 'Add subtle grid pattern to background.',
|
||||
transparent: 'Transparent background',
|
||||
transparent_description: 'Remove background color from image.',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
@@ -363,7 +390,7 @@ export const fr: LanguageTranslation = {
|
||||
error: {
|
||||
title: "Erreur lors de l'exportation du diagramme",
|
||||
description:
|
||||
"Une erreur s'est produite. Besoin d'aide ? chartdb.io@gmail.com",
|
||||
"Une erreur s'est produite. Besoin d'aide ? support@chartdb.io",
|
||||
},
|
||||
},
|
||||
import_diagram_dialog: {
|
||||
@@ -374,7 +401,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: {
|
||||
@@ -402,6 +429,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: {
|
||||
|
@@ -43,9 +43,7 @@ export const gu: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'મદદ',
|
||||
docs_website: 'દસ્તાવેજીકરણ',
|
||||
visit_website: 'ChartDB વેબસાઇટ પર જાઓ',
|
||||
join_discord: 'અમારા Discordમાં જોડાઓ',
|
||||
schedule_a_call: 'અમારી સાથે વાત કરો!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -153,6 +151,8 @@ export const gu: LanguageTranslation = {
|
||||
comments: 'ટિપ્પણીઓ',
|
||||
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
|
||||
delete_field: 'ફીલ્ડ કાઢી નાખો',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'ઇન્ડેક્સ લક્ષણો',
|
||||
@@ -212,6 +212,27 @@ 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -237,7 +258,7 @@ export const gu: LanguageTranslation = {
|
||||
title: 'તમારું ડેટાબેસ આયાત કરો',
|
||||
database_edition: 'ડેટાબેસ આવૃત્તિ:',
|
||||
step_1: 'તમારા ડેટાબેસમાં આ સ્ક્રિપ્ટ ચલાવો:',
|
||||
step_2: 'સ્ક્રિપ્ટનો પરિણામ અહીં પેસ્ટ કરો:',
|
||||
step_2: 'સ્ક્રિપ્ટનો પરિણામ અહીં પેસ્ટ કરો →',
|
||||
script_results_placeholder: 'સ્ક્રિપ્ટના પરિણામ અહીં...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS સૂચનાઓ',
|
||||
@@ -331,6 +352,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: {
|
||||
@@ -365,7 +392,7 @@ export const gu: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'ડાયાગ્રામ નિકાસમાં ભૂલ',
|
||||
description:
|
||||
'કશુક તો ખોટું થયું. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
|
||||
'કશુક તો ખોટું થયું. મદદ જોઈએ? support@chartdb.io પર સંપર્ક કરો.',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -377,7 +404,7 @@ export const gu: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'ડાયાગ્રામ આયાતમાં ભૂલ',
|
||||
description:
|
||||
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
|
||||
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? support@chartdb.io પર સંપર્ક કરો.',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -404,6 +431,8 @@ export const gu: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'નવું ટેબલ',
|
||||
new_relationship: 'નવો સંબંધ',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
@@ -42,9 +42,7 @@ export const hi: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'मदद',
|
||||
docs_website: 'દસ્તાવેજીકરણ',
|
||||
visit_website: 'ChartDB वेबसाइट पर जाएँ',
|
||||
join_discord: 'हमसे Discord पर जुड़ें',
|
||||
schedule_a_call: 'हमसे बात करें!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -152,6 +150,8 @@ export const hi: LanguageTranslation = {
|
||||
comments: 'टिप्पणियाँ',
|
||||
no_comments: 'कोई टिप्पणी नहीं',
|
||||
delete_field: 'फ़ील्ड हटाएँ',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'सूचकांक विशेषताएँ',
|
||||
@@ -212,6 +212,27 @@ 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -238,7 +259,7 @@ export const hi: LanguageTranslation = {
|
||||
title: 'अपना डेटाबेस आयात करें',
|
||||
database_edition: 'डेटाबेस संस्करण:',
|
||||
step_1: 'अपने डेटाबेस में यह स्क्रिप्ट चलाएँ:',
|
||||
step_2: 'यहाँ स्क्रिप्ट का परिणाम पेस्ट करें:',
|
||||
step_2: 'यहाँ स्क्रिप्ट का परिणाम पेस्ट करें →',
|
||||
script_results_placeholder: 'स्क्रिप्ट के परिणाम यहाँ...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS निर्देश',
|
||||
@@ -334,6 +355,12 @@ export const hi: LanguageTranslation = {
|
||||
scale_4x: '4x',
|
||||
cancel: 'रद्द करें',
|
||||
export: 'निर्यात करें',
|
||||
// TODO: Translate
|
||||
advanced_options: 'Advanced Options',
|
||||
pattern: 'Include background pattern',
|
||||
pattern_description: 'Add subtle grid pattern to background.',
|
||||
transparent: 'Transparent background',
|
||||
transparent_description: 'Remove background color from image.',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
@@ -368,7 +395,7 @@ export const hi: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error exporting diagram',
|
||||
description:
|
||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
||||
'Something went wrong. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -380,7 +407,7 @@ export const hi: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error importing diagram',
|
||||
description:
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -407,6 +434,8 @@ export const hi: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'नई तालिका',
|
||||
new_relationship: 'नया संबंध',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
@@ -41,10 +41,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!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -151,6 +149,8 @@ export const id_ID: LanguageTranslation = {
|
||||
comments: 'Komentar',
|
||||
no_comments: 'Tidak ada komentar',
|
||||
delete_field: 'Hapus Kolom',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Atribut Indeks',
|
||||
@@ -210,6 +210,27 @@ 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -236,7 +257,7 @@ export const id_ID: LanguageTranslation = {
|
||||
title: 'Impor Database Anda',
|
||||
database_edition: 'Edisi Database:',
|
||||
step_1: 'Jalankan skrip ini di database Anda:',
|
||||
step_2: 'Tempel hasil skrip di sini:',
|
||||
step_2: 'Tempel hasil skrip di sini →',
|
||||
script_results_placeholder: 'Hasil skrip di sini...',
|
||||
ssms_instructions: {
|
||||
button_text: 'Instruksi SSMS',
|
||||
@@ -329,6 +350,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: {
|
||||
@@ -363,7 +390,7 @@ export const id_ID: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error ekspor diagram',
|
||||
description:
|
||||
'Sesuatu yang salah. Butuh bantuan? chartdb.io@gmail.com',
|
||||
'Sesuatu yang salah. Butuh bantuan? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -375,7 +402,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
|
||||
@@ -403,6 +430,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: {
|
||||
|
@@ -44,9 +44,7 @@ export const ja: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'ヘルプ',
|
||||
docs_website: 'ドキュメント',
|
||||
visit_website: 'ChartDBにアクセス',
|
||||
join_discord: 'Discordに参加',
|
||||
schedule_a_call: '話しかけてください!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -155,6 +153,8 @@ export const ja: LanguageTranslation = {
|
||||
comments: 'コメント',
|
||||
no_comments: 'コメントがありません',
|
||||
delete_field: 'フィールドを削除',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'インデックス属性',
|
||||
@@ -216,6 +216,27 @@ 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -242,7 +263,7 @@ export const ja: LanguageTranslation = {
|
||||
title: 'データベースをインポート',
|
||||
database_edition: 'データベースエディション:',
|
||||
step_1: 'このスクリプトをデータベースで実行してください:',
|
||||
step_2: 'ここにスクリプトの結果を貼り付けてください:',
|
||||
step_2: 'ここにスクリプトの結果を貼り付けてください →',
|
||||
script_results_placeholder: 'ここにスクリプトの結果...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMSの手順',
|
||||
@@ -338,6 +359,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: {
|
||||
@@ -372,7 +399,7 @@ export const ja: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error exporting diagram',
|
||||
description:
|
||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
||||
'Something went wrong. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -384,7 +411,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
|
||||
@@ -411,6 +438,8 @@ export const ja: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: '新しいテーブル',
|
||||
new_relationship: '新しいリレーションシップ',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
@@ -42,9 +42,7 @@ export const ko_KR: LanguageTranslation = {
|
||||
help: {
|
||||
help: '도움말',
|
||||
docs_website: '선적 서류 비치',
|
||||
visit_website: 'ChartDB 사이트 방문',
|
||||
join_discord: 'Discord 가입',
|
||||
schedule_a_call: 'Talk with us!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -151,6 +149,8 @@ export const ko_KR: LanguageTranslation = {
|
||||
comments: '주석',
|
||||
no_comments: '주석 없음',
|
||||
delete_field: '필드 삭제',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: '인덱스 속성',
|
||||
@@ -210,6 +210,27 @@ 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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -236,7 +257,7 @@ export const ko_KR: LanguageTranslation = {
|
||||
title: '당신의 데이터베이스를 가져오세요',
|
||||
database_edition: '데이터베이스 세부 종류:',
|
||||
step_1: '데이터베이스에서 아래의 SQL을 실행해주세요:',
|
||||
step_2: '이곳에 결과를 붙여넣어주세요:',
|
||||
step_2: '이곳에 결과를 붙여넣어주세요 →',
|
||||
script_results_placeholder: '이곳에 스크립트 결과를 입력...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS을 사용하시는 경우',
|
||||
@@ -329,6 +350,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: {
|
||||
@@ -362,7 +389,7 @@ export const ko_KR: LanguageTranslation = {
|
||||
error: {
|
||||
title: '다이어그램 내보내기 오류',
|
||||
description:
|
||||
'무언가 문제가 발생하였습니다. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
|
||||
'무언가 문제가 발생하였습니다. 도움이 필요하신 경우 support@chartdb.io으로 연락해주세요.',
|
||||
},
|
||||
},
|
||||
import_diagram_dialog: {
|
||||
@@ -373,7 +400,7 @@ export const ko_KR: LanguageTranslation = {
|
||||
error: {
|
||||
title: '다이어그램 가져오기 오류',
|
||||
description:
|
||||
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
|
||||
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 support@chartdb.io으로 연락해주세요.',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -400,6 +427,8 @@ export const ko_KR: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: '새 테이블',
|
||||
new_relationship: '새 연관관계',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
@@ -43,9 +43,7 @@ export const mr: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'मदत',
|
||||
docs_website: 'दस्तऐवजीकरण',
|
||||
visit_website: 'ChartDB ला भेट द्या',
|
||||
join_discord: 'आमच्या डिस्कॉर्डमध्ये सामील व्हा',
|
||||
schedule_a_call: 'आमच्याशी बोला!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -154,6 +152,8 @@ export const mr: LanguageTranslation = {
|
||||
comments: 'टिप्पण्या',
|
||||
no_comments: 'कोणत्याही टिप्पणी नाहीत',
|
||||
delete_field: 'फील्ड हटवा',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'इंडेक्स गुणधर्म',
|
||||
@@ -215,6 +215,27 @@ export const mr: LanguageTranslation = {
|
||||
description: 'सुरू करण्यासाठी एक दृश्य तयार करा',
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: Translate
|
||||
areas_section: {
|
||||
areas: 'Areas',
|
||||
add_area: 'Add Area',
|
||||
filter: 'Filter',
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No areas found matching your filter.',
|
||||
|
||||
area: {
|
||||
area_actions: {
|
||||
title: 'Area Actions',
|
||||
edit_name: 'Edit Name',
|
||||
delete_area: 'Delete Area',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'No areas',
|
||||
description: 'Create an area to get started',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -241,7 +262,7 @@ export const mr: LanguageTranslation = {
|
||||
title: 'तुमचा डेटाबेस आयात करा',
|
||||
database_edition: 'डेटाबेस संस्करण:',
|
||||
step_1: 'तुमच्या डेटाबेसमध्ये हा स्क्रिप्ट चालवा:',
|
||||
step_2: 'स्क्रिप्टचा परिणाम येथे पेस्ट करा:',
|
||||
step_2: 'स्क्रिप्टचा परिणाम येथे पेस्ट करा →',
|
||||
script_results_placeholder: 'स्क्रिप्ट परिणाम येथे...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS सूचना',
|
||||
@@ -337,6 +358,12 @@ export const mr: LanguageTranslation = {
|
||||
scale_4x: '4x',
|
||||
cancel: 'रद्द करा',
|
||||
export: 'निर्यात करा',
|
||||
// TODO: Translate
|
||||
advanced_options: 'Advanced Options',
|
||||
pattern: 'Include background pattern',
|
||||
pattern_description: 'Add subtle grid pattern to background.',
|
||||
transparent: 'Transparent background',
|
||||
transparent_description: 'Remove background color from image.',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
@@ -372,7 +399,7 @@ export const mr: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error exporting diagram',
|
||||
description:
|
||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
||||
'Something went wrong. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -385,7 +412,7 @@ export const mr: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error importing diagram',
|
||||
description:
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -413,6 +440,8 @@ export const mr: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'नवीन टेबल',
|
||||
new_relationship: 'नवीन रिलेशनशिप',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
@@ -43,9 +43,7 @@ export const ne: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'मद्दत',
|
||||
docs_website: 'कागजात',
|
||||
visit_website: 'वेबसाइटमा जानुहोस्',
|
||||
join_discord: 'डिस्कोर्डमा सामिल हुनुहोस्',
|
||||
schedule_a_call: 'कल अनुसूची गर्नुहोस्',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -152,6 +150,8 @@ export const ne: LanguageTranslation = {
|
||||
comments: 'टिप्पणीहरू',
|
||||
no_comments: 'कुनै टिप्पणीहरू छैनन्',
|
||||
delete_field: 'क्षेत्र हटाउनुहोस्',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'सूचक विशेषताहरू',
|
||||
@@ -212,6 +212,27 @@ export const ne: LanguageTranslation = {
|
||||
'डिपेन्डेन्सीहरू देखाउनका लागि एक व्यू बनाउनुहोस्',
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: Translate
|
||||
areas_section: {
|
||||
areas: 'Areas',
|
||||
add_area: 'Add Area',
|
||||
filter: 'Filter',
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No areas found matching your filter.',
|
||||
|
||||
area: {
|
||||
area_actions: {
|
||||
title: 'Area Actions',
|
||||
edit_name: 'Edit Name',
|
||||
delete_area: 'Delete Area',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'No areas',
|
||||
description: 'Create an area to get started',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -239,7 +260,7 @@ export const ne: LanguageTranslation = {
|
||||
title: 'तपाईंको डाटाबेस आयात गर्नुहोस्',
|
||||
database_edition: 'डाटाबेस संस्करण:',
|
||||
step_1: 'तपाईंको डाटाबेसमा यो स्क्रिप्ट चलाउनुहोस्:',
|
||||
step_2: 'यो स्क्रिप्ट परिणाम यहाँ पेस्ट गर्नुहोस्:',
|
||||
step_2: 'यो स्क्रिप्ट परिणाम यहाँ पेस्ट गर्नुहोस् →',
|
||||
script_results_placeholder: 'स्क्रिप्ट परिणाम यहाँ...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS निर्देशन',
|
||||
@@ -334,6 +355,12 @@ export const ne: LanguageTranslation = {
|
||||
scale_4x: '४x',
|
||||
cancel: 'रद्द गर्नुहोस्',
|
||||
export: 'निर्यात गर्नुहोस्',
|
||||
// TODO: Translate
|
||||
advanced_options: 'Advanced Options',
|
||||
pattern: 'Include background pattern',
|
||||
pattern_description: 'Add subtle grid pattern to background.',
|
||||
transparent: 'Transparent background',
|
||||
transparent_description: 'Remove background color from image.',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
@@ -367,7 +394,7 @@ export const ne: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error exporting diagram',
|
||||
description:
|
||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
||||
'Something went wrong. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -379,7 +406,7 @@ export const ne: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'डायाग्राम आयात गर्दा समस्या आयो',
|
||||
description:
|
||||
'डायाग्राम JSON अमान्य छ। कृपया JSON जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्। मद्दत चाहिन्छ? chartdb.io@gmail.com मा सम्पर्क गर्नुहोस्',
|
||||
'डायाग्राम JSON अमान्य छ। कृपया JSON जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्। मद्दत चाहिन्छ? support@chartdb.io मा सम्पर्क गर्नुहोस्',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -407,6 +434,8 @@ export const ne: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'नयाँ तालिका',
|
||||
new_relationship: 'नयाँ सम्बन्ध',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
@@ -43,9 +43,7 @@ export const pt_BR: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'Ajuda',
|
||||
docs_website: 'Documentação',
|
||||
visit_website: 'Visitar ChartDB',
|
||||
join_discord: 'Junte-se a nós no Discord',
|
||||
schedule_a_call: 'Fale Conosco!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -152,6 +150,8 @@ export const pt_BR: LanguageTranslation = {
|
||||
comments: 'Comentários',
|
||||
no_comments: 'Sem comentários',
|
||||
delete_field: 'Excluir Campo',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Atributos do Índice',
|
||||
@@ -211,6 +211,27 @@ export const pt_BR: LanguageTranslation = {
|
||||
description: 'Crie uma visualização para começar',
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: Translate
|
||||
areas_section: {
|
||||
areas: 'Areas',
|
||||
add_area: 'Add Area',
|
||||
filter: 'Filter',
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No areas found matching your filter.',
|
||||
|
||||
area: {
|
||||
area_actions: {
|
||||
title: 'Area Actions',
|
||||
edit_name: 'Edit Name',
|
||||
delete_area: 'Delete Area',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'No areas',
|
||||
description: 'Create an area to get started',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -237,7 +258,7 @@ export const pt_BR: LanguageTranslation = {
|
||||
title: 'Importe seu Banco de Dados',
|
||||
database_edition: 'Edição do Banco de Dados:',
|
||||
step_1: 'Execute este script no seu banco de dados:',
|
||||
step_2: 'Cole o resultado do script aqui:',
|
||||
step_2: 'Cole o resultado do script aqui →',
|
||||
script_results_placeholder: 'Resultados do script aqui...',
|
||||
ssms_instructions: {
|
||||
button_text: 'Instruções do SSMS',
|
||||
@@ -332,6 +353,12 @@ export const pt_BR: LanguageTranslation = {
|
||||
scale_4x: '4x',
|
||||
cancel: 'Cancelar',
|
||||
export: 'Exportar',
|
||||
// TODO: Translate
|
||||
advanced_options: 'Advanced Options',
|
||||
pattern: 'Include background pattern',
|
||||
pattern_description: 'Add subtle grid pattern to background.',
|
||||
transparent: 'Transparent background',
|
||||
transparent_description: 'Remove background color from image.',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
@@ -366,7 +393,7 @@ export const pt_BR: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error exporting diagram',
|
||||
description:
|
||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
||||
'Something went wrong. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -378,7 +405,7 @@ export const pt_BR: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error importing diagram',
|
||||
description:
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -405,6 +432,8 @@ export const pt_BR: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'Nova Tabela',
|
||||
new_relationship: 'Novo Relacionamento',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
@@ -24,28 +24,24 @@ export const ru: LanguageTranslation = {
|
||||
view: 'Вид',
|
||||
show_sidebar: 'Показать боковую панель',
|
||||
hide_sidebar: 'Скрыть боковую панель',
|
||||
hide_cardinality: 'Скрыть множественность связи',
|
||||
show_cardinality: 'Показать множественность связи',
|
||||
hide_cardinality: 'Скрыть виды связи',
|
||||
show_cardinality: 'Показать виды связи',
|
||||
zoom_on_scroll: 'Увеличение при прокрутке',
|
||||
theme: 'Тема',
|
||||
show_dependencies: 'Показать зависимости',
|
||||
hide_dependencies: 'Скрыть зависимости',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
show_minimap: 'Показать мини-карту',
|
||||
hide_minimap: 'Скрыть мини-карту',
|
||||
},
|
||||
// TODO: Translate
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
backup: 'Бэкап',
|
||||
export_diagram: 'Экспорт диаграммы',
|
||||
restore_diagram: 'Восстановить диаграмму',
|
||||
},
|
||||
help: {
|
||||
help: 'Помощь',
|
||||
docs_website: 'Документация',
|
||||
visit_website: 'Перейти на сайт ChartDB',
|
||||
join_discord: 'Присоединиться к сообществу в Discord',
|
||||
schedule_a_call: 'Поговорите с нами!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -125,17 +121,17 @@ export const ru: LanguageTranslation = {
|
||||
add_table: 'Добавить таблицу',
|
||||
filter: 'Фильтр',
|
||||
collapse: 'Свернуть все',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
clear: 'Очистить фильтр',
|
||||
|
||||
no_results:
|
||||
'Таблицы не найдены, соответствующие вашему фильтру.',
|
||||
show_list: 'Переключиться на список таблиц',
|
||||
show_dbml: 'Переключиться на редактор DBML',
|
||||
|
||||
table: {
|
||||
fields: 'Поля',
|
||||
nullable: 'Может содержать NULL?',
|
||||
primary_key: 'Первичный ключ,',
|
||||
nullable: 'Может быть NULL?',
|
||||
primary_key: 'Первичный ключ',
|
||||
indexes: 'Индексы',
|
||||
comments: 'Комментарии',
|
||||
no_comments: 'Нет комментария',
|
||||
@@ -151,6 +147,7 @@ export const ru: LanguageTranslation = {
|
||||
comments: 'Комментарии',
|
||||
no_comments: 'Нет комментария',
|
||||
delete_field: 'Удалить поле',
|
||||
character_length: 'Макс. длина',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Атрибуты индекса',
|
||||
@@ -163,7 +160,7 @@ export const ru: LanguageTranslation = {
|
||||
change_schema: 'Изменить схему',
|
||||
add_field: 'Добавить поле',
|
||||
add_index: 'Добавить индекс',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
duplicate_table: 'Создать копию',
|
||||
delete_table: 'Удалить таблицу',
|
||||
},
|
||||
},
|
||||
@@ -180,7 +177,7 @@ export const ru: LanguageTranslation = {
|
||||
relationship: {
|
||||
primary: 'Основная таблица',
|
||||
foreign: 'Справочная таблица',
|
||||
cardinality: 'Тип множественности связи',
|
||||
cardinality: 'Тип множественной связи',
|
||||
delete_relationship: 'Удалить',
|
||||
relationship_actions: {
|
||||
title: 'Действия',
|
||||
@@ -210,6 +207,28 @@ export const ru: LanguageTranslation = {
|
||||
description: 'Создайте представление, чтобы начать',
|
||||
},
|
||||
},
|
||||
|
||||
areas_section: {
|
||||
areas: 'Области',
|
||||
add_area: 'Добавить область',
|
||||
filter: 'Фильтр',
|
||||
clear: 'Очистить фильтр',
|
||||
|
||||
no_results:
|
||||
'Области не найдены, соответствующие вашему фильтру.',
|
||||
|
||||
area: {
|
||||
area_actions: {
|
||||
title: 'Действия',
|
||||
edit_name: 'Изменить название',
|
||||
delete_area: 'Удалить область',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'Нет областей',
|
||||
description: 'Создайте область, чтобы начать',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -236,7 +255,7 @@ export const ru: LanguageTranslation = {
|
||||
title: 'Импортируйте свою базу данных',
|
||||
database_edition: 'Версия базы данных:',
|
||||
step_1: 'Запустите этот скрипт в своей базе данных:',
|
||||
step_2: 'Вставьте вывод скрипта сюда:',
|
||||
step_2: 'Вставьте вывод скрипта сюда →',
|
||||
script_results_placeholder: 'Вывод скрипта здесь...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS Инструкции',
|
||||
@@ -331,6 +350,12 @@ export const ru: LanguageTranslation = {
|
||||
scale_4x: '4x',
|
||||
cancel: 'Отменить',
|
||||
export: 'Экспортировать',
|
||||
// TODO: Translate
|
||||
advanced_options: 'Advanced Options',
|
||||
pattern: 'Include background pattern',
|
||||
pattern_description: 'Add subtle grid pattern to background.',
|
||||
transparent: 'Transparent background',
|
||||
transparent_description: 'Remove background color from image.',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
@@ -364,7 +389,7 @@ export const ru: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Ошибка экспортирования диаграммы',
|
||||
description:
|
||||
'Что-то пошло не так. Если вам нужна помощь, напишите нам: chartdb.io@gmail.com',
|
||||
'Что-то пошло не так. Если вам нужна помощь, напишите нам: support@chartdb.io',
|
||||
},
|
||||
},
|
||||
import_diagram_dialog: {
|
||||
@@ -375,21 +400,22 @@ export const ru: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Ошибка при импорте диаграммы',
|
||||
description:
|
||||
'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: chartdb.io@gmail.com',
|
||||
'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
example_title: 'Импорт DBML',
|
||||
title: 'Импортировать DBML',
|
||||
description: 'Импортировать схему базы данных из DBML формата.',
|
||||
import: 'Импортировать',
|
||||
cancel: 'Отмена',
|
||||
skip_and_empty: 'Продолжить с пустой диаграммой',
|
||||
show_example: 'Использовать эту схему',
|
||||
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
title: 'Ошибка',
|
||||
description:
|
||||
'Ошибка парсинга DBML. Пожалуйста проверьте синтаксис.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
@@ -402,13 +428,14 @@ export const ru: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'Создать таблицу',
|
||||
new_relationship: 'Создать отношение',
|
||||
new_area: 'Новая область',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
edit_table: 'Изменить таблицу',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
duplicate_table: 'Создать копию',
|
||||
delete_table: 'Удалить таблицу',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
add_relationship: 'Добавить связь',
|
||||
},
|
||||
|
||||
copy_to_clipboard: 'Скопировать в буфер обмена',
|
||||
|
@@ -43,9 +43,7 @@ export const te: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'సహాయం',
|
||||
docs_website: 'డాక్యుమెంటేషన్',
|
||||
visit_website: 'ChartDB సందర్శించండి',
|
||||
join_discord: 'డిస్కార్డ్లో మా నుంచి చేరండి',
|
||||
schedule_a_call: 'మాతో మాట్లాడండి!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -152,6 +150,8 @@ export const te: LanguageTranslation = {
|
||||
comments: 'వ్యాఖ్యలు',
|
||||
no_comments: 'వ్యాఖ్యలు లేవు',
|
||||
delete_field: 'ఫీల్డ్ తొలగించు',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'ఇండెక్స్ గుణాలు',
|
||||
@@ -212,6 +212,27 @@ export const te: LanguageTranslation = {
|
||||
description: 'ప్రారంభించడానికి ఒక వీక్షణ సృష్టించండి',
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: Translate
|
||||
areas_section: {
|
||||
areas: 'Areas',
|
||||
add_area: 'Add Area',
|
||||
filter: 'Filter',
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No areas found matching your filter.',
|
||||
|
||||
area: {
|
||||
area_actions: {
|
||||
title: 'Area Actions',
|
||||
edit_name: 'Edit Name',
|
||||
delete_area: 'Delete Area',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'No areas',
|
||||
description: 'Create an area to get started',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -238,7 +259,7 @@ export const te: LanguageTranslation = {
|
||||
title: 'మీ డేటాబేస్ను దిగుమతి చేసుకోండి',
|
||||
database_edition: 'డేటాబేస్ ఎడిషన్:',
|
||||
step_1: 'ఈ స్క్రిప్ట్ను మీ డేటాబేస్లో అమలు చేయండి:',
|
||||
step_2: 'స్క్రిప్ట్ ఫలితాన్ని ఇక్కడ పేస్ట్ చేయండి:',
|
||||
step_2: 'స్క్రిప్ట్ ఫలితాన్ని ఇక్కడ పేస్ట్ చేయండి →',
|
||||
script_results_placeholder: 'స్క్రిప్ట్ ఫలితాలు ఇక్కడ...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS సూచనల్ని చూపించు',
|
||||
@@ -333,6 +354,12 @@ export const te: LanguageTranslation = {
|
||||
scale_4x: '4x',
|
||||
cancel: 'రద్దు',
|
||||
export: 'ఎగుమతి',
|
||||
// TODO: Translate
|
||||
advanced_options: 'Advanced Options',
|
||||
pattern: 'Include background pattern',
|
||||
pattern_description: 'Add subtle grid pattern to background.',
|
||||
transparent: 'Transparent background',
|
||||
transparent_description: 'Remove background color from image.',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
@@ -368,7 +395,7 @@ export const te: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error exporting diagram',
|
||||
description:
|
||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
||||
'Something went wrong. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -381,7 +408,7 @@ export const te: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error importing diagram',
|
||||
description:
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -409,6 +436,8 @@ export const te: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'కొత్త పట్టిక',
|
||||
new_relationship: 'కొత్త సంబంధం',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
@@ -43,9 +43,7 @@ export const tr: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'Yardım',
|
||||
docs_website: 'Belgeleme',
|
||||
visit_website: "ChartDB'yi Ziyaret Et",
|
||||
join_discord: "Discord'a Katıl",
|
||||
schedule_a_call: 'Bize Ulaş!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -151,6 +149,8 @@ export const tr: LanguageTranslation = {
|
||||
comments: 'Yorumlar',
|
||||
no_comments: 'Yorum yok',
|
||||
delete_field: 'Alanı Sil',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'İndeks Özellikleri',
|
||||
@@ -211,6 +211,27 @@ export const tr: LanguageTranslation = {
|
||||
description: 'Başlamak için bir görünüm oluşturun',
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: Translate
|
||||
areas_section: {
|
||||
areas: 'Areas',
|
||||
add_area: 'Add Area',
|
||||
filter: 'Filter',
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No areas found matching your filter.',
|
||||
|
||||
area: {
|
||||
area_actions: {
|
||||
title: 'Area Actions',
|
||||
edit_name: 'Edit Name',
|
||||
delete_area: 'Delete Area',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'No areas',
|
||||
description: 'Create an area to get started',
|
||||
},
|
||||
},
|
||||
},
|
||||
toolbar: {
|
||||
zoom_in: 'Yakınlaştır',
|
||||
@@ -234,7 +255,7 @@ export const tr: LanguageTranslation = {
|
||||
title: 'Veritabanını İçe Aktar',
|
||||
database_edition: 'Veritabanı Sürümü:',
|
||||
step_1: 'Bu komut dosyasını veritabanınızda çalıştırın:',
|
||||
step_2: 'Komut dosyası sonucunu buraya yapıştırın:',
|
||||
step_2: 'Komut dosyası sonucunu buraya yapıştırın →',
|
||||
script_results_placeholder: 'Komut dosyası sonuçları burada...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS Talimatları',
|
||||
@@ -325,6 +346,12 @@ export const tr: LanguageTranslation = {
|
||||
scale_4x: '4x',
|
||||
cancel: 'İptal',
|
||||
export: 'Dışa Aktar',
|
||||
// TODO: Translate
|
||||
advanced_options: 'Advanced Options',
|
||||
pattern: 'Include background pattern',
|
||||
pattern_description: 'Add subtle grid pattern to background.',
|
||||
transparent: 'Transparent background',
|
||||
transparent_description: 'Remove background color from image.',
|
||||
},
|
||||
new_table_schema_dialog: {
|
||||
title: 'Şema Seç',
|
||||
@@ -356,7 +383,7 @@ export const tr: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error exporting diagram',
|
||||
description:
|
||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
||||
'Something went wrong. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -368,7 +395,7 @@ export const tr: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error importing diagram',
|
||||
description:
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -394,6 +421,8 @@ export const tr: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'Yeni Tablo',
|
||||
new_relationship: 'Yeni İlişki',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
table_node_context_menu: {
|
||||
edit_table: 'Tabloyu Düzenle',
|
||||
|
@@ -41,9 +41,7 @@ export const uk: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'Довідка',
|
||||
docs_website: 'Документація',
|
||||
visit_website: 'Сайт ChartDB',
|
||||
join_discord: 'Приєднуйтесь до нас в Діскорд',
|
||||
schedule_a_call: 'Забронювати зустріч!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -150,6 +148,8 @@ export const uk: LanguageTranslation = {
|
||||
comments: 'Коментарі',
|
||||
no_comments: 'Немає коментарів',
|
||||
delete_field: 'Видалити поле',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Атрибути індексу',
|
||||
@@ -209,6 +209,27 @@ export const uk: LanguageTranslation = {
|
||||
description: 'Створіть подання, щоб почати',
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: Translate
|
||||
areas_section: {
|
||||
areas: 'Areas',
|
||||
add_area: 'Add Area',
|
||||
filter: 'Filter',
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No areas found matching your filter.',
|
||||
|
||||
area: {
|
||||
area_actions: {
|
||||
title: 'Area Actions',
|
||||
edit_name: 'Edit Name',
|
||||
delete_area: 'Delete Area',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'No areas',
|
||||
description: 'Create an area to get started',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -235,7 +256,7 @@ export const uk: LanguageTranslation = {
|
||||
title: 'Імпортуйте вашу базу даних',
|
||||
database_edition: 'Варіант бази даних:',
|
||||
step_1: 'Запустіть цей сценарій у своїй базі даних:',
|
||||
step_2: 'Вставте сюди результат сценарію:',
|
||||
step_2: 'Вставте сюди результат сценарію →',
|
||||
script_results_placeholder: 'Результати сценарію має бути тут…',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS Інструкції',
|
||||
@@ -330,6 +351,12 @@ export const uk: LanguageTranslation = {
|
||||
scale_4x: '4x',
|
||||
cancel: 'Скасувати',
|
||||
export: 'Експортувати',
|
||||
// TODO: Translate
|
||||
advanced_options: 'Advanced Options',
|
||||
pattern: 'Include background pattern',
|
||||
pattern_description: 'Add subtle grid pattern to background.',
|
||||
transparent: 'Transparent background',
|
||||
transparent_description: 'Remove background color from image.',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
@@ -362,7 +389,7 @@ export const uk: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Помилка експорут діаграми',
|
||||
description:
|
||||
'Щось пішло не так. Потрібна допомога? chartdb.io@gmail.com',
|
||||
'Щось пішло не так. Потрібна допомога? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
import_diagram_dialog: {
|
||||
@@ -373,7 +400,7 @@ export const uk: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Помилка імпорту діаграми',
|
||||
description:
|
||||
'JSON діаграми є неправильним. Будь ласка, перевірте JSON і спробуйте ще раз. Потрібна допомога? chartdb.io@gmail.com',
|
||||
'JSON діаграми є неправильним. Будь ласка, перевірте JSON і спробуйте ще раз. Потрібна допомога? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -400,6 +427,8 @@ export const uk: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'Нова таблиця',
|
||||
new_relationship: 'Новий звʼязок',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
@@ -42,9 +42,7 @@ export const vi: LanguageTranslation = {
|
||||
help: {
|
||||
help: 'Trợ giúp',
|
||||
docs_website: 'Tài liệu',
|
||||
visit_website: 'Truy cập ChartDB',
|
||||
join_discord: 'Tham gia Discord',
|
||||
schedule_a_call: 'Trò chuyện cùng chúng tôi!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -151,6 +149,8 @@ export const vi: LanguageTranslation = {
|
||||
comments: 'Bình luận',
|
||||
no_comments: 'Không có bình luận',
|
||||
delete_field: 'Xóa trường',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Thuộc tính chỉ mục',
|
||||
@@ -210,6 +210,27 @@ export const vi: LanguageTranslation = {
|
||||
description: 'Tạo bảng xem phụ thuộc để bắt đầu',
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: Translate
|
||||
areas_section: {
|
||||
areas: 'Areas',
|
||||
add_area: 'Add Area',
|
||||
filter: 'Filter',
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No areas found matching your filter.',
|
||||
|
||||
area: {
|
||||
area_actions: {
|
||||
title: 'Area Actions',
|
||||
edit_name: 'Edit Name',
|
||||
delete_area: 'Delete Area',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'No areas',
|
||||
description: 'Create an area to get started',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -236,7 +257,7 @@ export const vi: LanguageTranslation = {
|
||||
title: 'Nhập cơ sở dữ liệu của bạn',
|
||||
database_edition: 'Loại:',
|
||||
step_1: 'Chạy lệnh này trong cơ sở dữ liệu của bạn:',
|
||||
step_2: 'Dán kết quả vào đây:',
|
||||
step_2: 'Dán kết quả vào đây →',
|
||||
script_results_placeholder: 'Kết quả...',
|
||||
ssms_instructions: {
|
||||
button_text: 'Hướng dẫn SSMS',
|
||||
@@ -329,6 +350,12 @@ export const vi: LanguageTranslation = {
|
||||
scale_4x: '4x',
|
||||
cancel: 'Hủy',
|
||||
export: 'Xuất',
|
||||
// TODO: Translate
|
||||
advanced_options: 'Advanced Options',
|
||||
pattern: 'Include background pattern',
|
||||
pattern_description: 'Add subtle grid pattern to background.',
|
||||
transparent: 'Transparent background',
|
||||
transparent_description: 'Remove background color from image.',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
@@ -362,7 +389,7 @@ export const vi: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Lỗi khi xuất sơ đồ',
|
||||
description:
|
||||
'Có gì đó không ổn. Cần trợ giúp? chartdb.io@gmail.com',
|
||||
'Có gì đó không ổn. Cần trợ giúp? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -374,7 +401,7 @@ export const vi: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Lỗi khi nhập sơ đồ',
|
||||
description:
|
||||
'Sơ đồ ở dạng JSON không hợp lệ. Vui lòng kiểm tra JSON và thử lại. Bạn cần trợ giúp? chartdb.io@gmail.com',
|
||||
'Sơ đồ ở dạng JSON không hợp lệ. Vui lòng kiểm tra JSON và thử lại. Bạn cần trợ giúp? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -401,6 +428,8 @@ export const vi: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: 'Tạo bảng mới',
|
||||
new_relationship: 'Tạo quan hệ mới',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
@@ -42,9 +42,7 @@ export const zh_CN: LanguageTranslation = {
|
||||
help: {
|
||||
help: '帮助',
|
||||
docs_website: '文档',
|
||||
visit_website: '访问 ChartDB',
|
||||
join_discord: '在 Discord 上加入我们',
|
||||
schedule_a_call: '和我们交流!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -148,6 +146,8 @@ export const zh_CN: LanguageTranslation = {
|
||||
comments: '注释',
|
||||
no_comments: '空',
|
||||
delete_field: '删除字段',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: '索引属性',
|
||||
@@ -207,6 +207,27 @@ export const zh_CN: LanguageTranslation = {
|
||||
description: '创建视图以开始',
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: Translate
|
||||
areas_section: {
|
||||
areas: 'Areas',
|
||||
add_area: 'Add Area',
|
||||
filter: 'Filter',
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No areas found matching your filter.',
|
||||
|
||||
area: {
|
||||
area_actions: {
|
||||
title: 'Area Actions',
|
||||
edit_name: 'Edit Name',
|
||||
delete_area: 'Delete Area',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'No areas',
|
||||
description: 'Create an area to get started',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -232,7 +253,7 @@ export const zh_CN: LanguageTranslation = {
|
||||
title: '导入您的数据库',
|
||||
database_edition: '数据库类型:',
|
||||
step_1: '在您的数据库中执行以下脚本:',
|
||||
step_2: '将结果粘贴于此:',
|
||||
step_2: '将结果粘贴于此 →',
|
||||
script_results_placeholder: '结果...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS 说明',
|
||||
@@ -326,6 +347,12 @@ export const zh_CN: LanguageTranslation = {
|
||||
scale_4x: '4x',
|
||||
cancel: '取消',
|
||||
export: '导出',
|
||||
// TODO: Translate
|
||||
advanced_options: 'Advanced Options',
|
||||
pattern: 'Include background pattern',
|
||||
pattern_description: 'Add subtle grid pattern to background.',
|
||||
transparent: 'Transparent background',
|
||||
transparent_description: 'Remove background color from image.',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
@@ -358,7 +385,7 @@ export const zh_CN: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error exporting diagram',
|
||||
description:
|
||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
||||
'Something went wrong. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -370,7 +397,7 @@ export const zh_CN: LanguageTranslation = {
|
||||
error: {
|
||||
title: '导入关系图时出错',
|
||||
description:
|
||||
'关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 chartdb.io@gmail.com',
|
||||
'关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -397,6 +424,8 @@ export const zh_CN: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: '新建表',
|
||||
new_relationship: '新建关系',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
@@ -42,9 +42,7 @@ export const zh_TW: LanguageTranslation = {
|
||||
help: {
|
||||
help: '幫助',
|
||||
docs_website: '文件',
|
||||
visit_website: '訪問 ChartDB 網站',
|
||||
join_discord: '加入 Discord',
|
||||
schedule_a_call: '與我們聯絡!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -148,6 +146,8 @@ export const zh_TW: LanguageTranslation = {
|
||||
comments: '註解',
|
||||
no_comments: '無註解',
|
||||
delete_field: '刪除欄位',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: '索引屬性',
|
||||
@@ -207,6 +207,27 @@ export const zh_TW: LanguageTranslation = {
|
||||
description: '請建立檢視以開始',
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: Translate
|
||||
areas_section: {
|
||||
areas: 'Areas',
|
||||
add_area: 'Add Area',
|
||||
filter: 'Filter',
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No areas found matching your filter.',
|
||||
|
||||
area: {
|
||||
area_actions: {
|
||||
title: 'Area Actions',
|
||||
edit_name: 'Edit Name',
|
||||
delete_area: 'Delete Area',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'No areas',
|
||||
description: 'Create an area to get started',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
@@ -232,7 +253,7 @@ export const zh_TW: LanguageTranslation = {
|
||||
title: '匯入資料庫',
|
||||
database_edition: '資料庫版本:',
|
||||
step_1: '請在資料庫中執行以下腳本:',
|
||||
step_2: '將腳本結果貼到此處:',
|
||||
step_2: '將腳本結果貼到此處 →',
|
||||
script_results_placeholder: '在此處貼上腳本結果...',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS 操作步驟',
|
||||
@@ -325,6 +346,12 @@ export const zh_TW: LanguageTranslation = {
|
||||
scale_4x: '4x',
|
||||
cancel: '取消',
|
||||
export: '匯出',
|
||||
// TODO: Translate
|
||||
advanced_options: 'Advanced Options',
|
||||
pattern: 'Include background pattern',
|
||||
pattern_description: 'Add subtle grid pattern to background.',
|
||||
transparent: 'Transparent background',
|
||||
transparent_description: 'Remove background color from image.',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
@@ -357,7 +384,7 @@ export const zh_TW: LanguageTranslation = {
|
||||
error: {
|
||||
title: 'Error exporting diagram',
|
||||
description:
|
||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
||||
'Something went wrong. Need help? support@chartdb.io',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -369,7 +396,7 @@ export const zh_TW: LanguageTranslation = {
|
||||
error: {
|
||||
title: '匯入圖表時發生錯誤',
|
||||
description:
|
||||
'圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 chartdb.io@gmail.com',
|
||||
'圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 support@chartdb.io',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
@@ -396,6 +423,8 @@ export const zh_TW: LanguageTranslation = {
|
||||
canvas_context_menu: {
|
||||
new_table: '新建表格',
|
||||
new_relationship: '新建關聯',
|
||||
// TODO: Translate
|
||||
new_area: 'New Area',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import type { Area } from './domain/area';
|
||||
import type { DBDependency } from './domain/db-dependency';
|
||||
import type { DBField } from './domain/db-field';
|
||||
import type { DBIndex } from './domain/db-index';
|
||||
@@ -43,6 +44,10 @@ const generateIdsMapFromDiagram = (
|
||||
idsMap.set(dependency.id, generateId());
|
||||
});
|
||||
|
||||
diagram.areas?.forEach((area) => {
|
||||
idsMap.set(area.id, generateId());
|
||||
});
|
||||
|
||||
return idsMap;
|
||||
};
|
||||
|
||||
@@ -193,12 +198,28 @@ export const cloneDiagram = (
|
||||
(dependency): dependency is DBDependency => dependency !== null
|
||||
) ?? [];
|
||||
|
||||
const areas: Area[] =
|
||||
diagram.areas
|
||||
?.map((area) => {
|
||||
const id = getNewId(area.id);
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...area,
|
||||
id,
|
||||
} satisfies Area;
|
||||
})
|
||||
.filter((area): area is Area => area !== null) ?? [];
|
||||
|
||||
return {
|
||||
...diagram,
|
||||
id: diagramId,
|
||||
dependencies,
|
||||
relationships,
|
||||
tables,
|
||||
areas,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const clickhouseDataTypes: readonly DataType[] = [
|
||||
export const clickhouseDataTypes: readonly DataTypeData[] = [
|
||||
// Numeric Types
|
||||
{ name: 'uint8', id: 'uint8' },
|
||||
{ name: 'uint16', id: 'uint16' },
|
||||
@@ -48,25 +48,41 @@ export const clickhouseDataTypes: readonly DataType[] = [
|
||||
{ name: 'mediumblob', id: 'mediumblob' },
|
||||
{ name: 'tinyblob', id: 'tinyblob' },
|
||||
{ name: 'blob', id: 'blob' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'char large object', id: 'char_large_object' },
|
||||
{ name: 'char varying', id: 'char_varying' },
|
||||
{ name: 'char varying', id: 'char_varying', hasCharMaxLength: true },
|
||||
{ name: 'character large object', id: 'character_large_object' },
|
||||
{ name: 'character varying', id: 'character_varying' },
|
||||
{
|
||||
name: 'character varying',
|
||||
id: 'character_varying',
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{ name: 'nchar large object', id: 'nchar_large_object' },
|
||||
{ name: 'nchar varying', id: 'nchar_varying' },
|
||||
{ name: 'nchar varying', id: 'nchar_varying', hasCharMaxLength: true },
|
||||
{
|
||||
name: 'national character large object',
|
||||
id: 'national_character_large_object',
|
||||
},
|
||||
{ name: 'national character varying', id: 'national_character_varying' },
|
||||
{ name: 'national char varying', id: 'national_char_varying' },
|
||||
{ name: 'national character', id: 'national_character' },
|
||||
{ name: 'national char', id: 'national_char' },
|
||||
{
|
||||
name: 'national character varying',
|
||||
id: 'national_character_varying',
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{
|
||||
name: 'national char varying',
|
||||
id: 'national_char_varying',
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{
|
||||
name: 'national character',
|
||||
id: 'national_character',
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{ name: 'national char', id: 'national_char', hasCharMaxLength: true },
|
||||
{ name: 'binary large object', id: 'binary_large_object' },
|
||||
{ name: 'binary varying', id: 'binary_varying' },
|
||||
{ name: 'fixedstring', id: 'fixedstring' },
|
||||
{ name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true },
|
||||
{ name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true },
|
||||
{ name: 'string', id: 'string' },
|
||||
|
||||
// Date Types
|
||||
|
@@ -13,12 +13,17 @@ export interface DataType {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DataTypeData extends DataType {
|
||||
hasCharMaxLength?: boolean;
|
||||
usageLevel?: 1 | 2; // Level 1 is most common, Level 2 is second most common
|
||||
}
|
||||
|
||||
export const dataTypeSchema: z.ZodType<DataType> = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export const dataTypeMap: Record<DatabaseType, readonly DataType[]> = {
|
||||
export const dataTypeMap: Record<DatabaseType, readonly DataTypeData[]> = {
|
||||
[DatabaseType.GENERIC]: genericDataTypes,
|
||||
[DatabaseType.POSTGRESQL]: postgresDataTypes,
|
||||
[DatabaseType.MYSQL]: mysqlDataTypes,
|
||||
@@ -29,6 +34,45 @@ export const dataTypeMap: Record<DatabaseType, readonly DataType[]> = {
|
||||
[DatabaseType.COCKROACHDB]: postgresDataTypes,
|
||||
} as const;
|
||||
|
||||
export const sortDataTypes = (dataTypes: DataTypeData[]): DataTypeData[] => {
|
||||
const types = [...dataTypes];
|
||||
return types.sort((a, b) => {
|
||||
// First sort by usage level (lower numbers first)
|
||||
if ((a.usageLevel || 3) < (b.usageLevel || 3)) return -1;
|
||||
if ((a.usageLevel || 3) > (b.usageLevel || 3)) return 1;
|
||||
// Then sort alphabetically by name
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
};
|
||||
|
||||
export const sortedDataTypeMap: Record<DatabaseType, readonly DataTypeData[]> =
|
||||
{
|
||||
[DatabaseType.GENERIC]: sortDataTypes([
|
||||
...dataTypeMap[DatabaseType.GENERIC],
|
||||
]),
|
||||
[DatabaseType.POSTGRESQL]: sortDataTypes([
|
||||
...dataTypeMap[DatabaseType.POSTGRESQL],
|
||||
]),
|
||||
[DatabaseType.MYSQL]: sortDataTypes([
|
||||
...dataTypeMap[DatabaseType.MYSQL],
|
||||
]),
|
||||
[DatabaseType.SQL_SERVER]: sortDataTypes([
|
||||
...dataTypeMap[DatabaseType.SQL_SERVER],
|
||||
]),
|
||||
[DatabaseType.MARIADB]: sortDataTypes([
|
||||
...dataTypeMap[DatabaseType.MARIADB],
|
||||
]),
|
||||
[DatabaseType.SQLITE]: sortDataTypes([
|
||||
...dataTypeMap[DatabaseType.SQLITE],
|
||||
]),
|
||||
[DatabaseType.CLICKHOUSE]: sortDataTypes([
|
||||
...dataTypeMap[DatabaseType.CLICKHOUSE],
|
||||
]),
|
||||
[DatabaseType.COCKROACHDB]: sortDataTypes([
|
||||
...dataTypeMap[DatabaseType.COCKROACHDB],
|
||||
]),
|
||||
} as const;
|
||||
|
||||
const compatibleTypes: Record<DatabaseType, Record<string, string[]>> = {
|
||||
[DatabaseType.POSTGRESQL]: {
|
||||
serial: ['integer'],
|
||||
@@ -64,3 +108,21 @@ export function areFieldTypesCompatible(
|
||||
}
|
||||
|
||||
export const dataTypes = Object.values(dataTypeMap).flat();
|
||||
|
||||
export const dataTypeDataToDataType = (
|
||||
dataTypeData: DataTypeData
|
||||
): DataType => ({
|
||||
id: dataTypeData.id,
|
||||
name: dataTypeData.name,
|
||||
});
|
||||
|
||||
export const findDataTypeDataById = (
|
||||
id: string,
|
||||
databaseType?: DatabaseType
|
||||
): DataTypeData | undefined => {
|
||||
const dataTypesOptions = databaseType
|
||||
? dataTypeMap[databaseType]
|
||||
: dataTypes;
|
||||
|
||||
return dataTypesOptions.find((dataType) => dataType.id === id);
|
||||
};
|
||||
|
@@ -1,27 +1,32 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const genericDataTypes: readonly DataType[] = [
|
||||
export const genericDataTypes: readonly DataTypeData[] = [
|
||||
// Level 1 - Most commonly used types
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
||||
{ name: 'int', id: 'int', usageLevel: 1 },
|
||||
{ name: 'text', id: 'text', usageLevel: 1 },
|
||||
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
||||
{ name: 'date', id: 'date', usageLevel: 1 },
|
||||
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
||||
|
||||
// Level 2 - Second most common types
|
||||
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
|
||||
{ name: 'datetime', id: 'datetime', usageLevel: 2 },
|
||||
{ name: 'json', id: 'json', usageLevel: 2 },
|
||||
{ name: 'uuid', id: 'uuid', usageLevel: 2 },
|
||||
|
||||
// Less common types
|
||||
{ name: 'bigint', id: 'bigint' },
|
||||
{ name: 'binary', id: 'binary' },
|
||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||
{ name: 'blob', id: 'blob' },
|
||||
{ name: 'boolean', id: 'boolean' },
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'date', id: 'date' },
|
||||
{ name: 'datetime', id: 'datetime' },
|
||||
{ name: 'decimal', id: 'decimal' },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'double', id: 'double' },
|
||||
{ name: 'enum', id: 'enum' },
|
||||
{ name: 'float', id: 'float' },
|
||||
{ name: 'int', id: 'int' },
|
||||
{ name: 'json', id: 'json' },
|
||||
{ name: 'numeric', id: 'numeric' },
|
||||
{ name: 'real', id: 'real' },
|
||||
{ name: 'set', id: 'set' },
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
{ name: 'text', id: 'text' },
|
||||
{ name: 'time', id: 'time' },
|
||||
{ name: 'timestamp', id: 'timestamp' },
|
||||
{ name: 'uuid', id: 'uuid' },
|
||||
{ name: 'varbinary', id: 'varbinary' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||
] as const;
|
||||
|
@@ -1,44 +1,44 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const mariadbDataTypes: readonly DataType[] = [
|
||||
// Numeric Types
|
||||
export const mariadbDataTypes: readonly DataTypeData[] = [
|
||||
// Level 1 - Most commonly used types
|
||||
{ name: 'int', id: 'int', usageLevel: 1 },
|
||||
{ name: 'bigint', id: 'bigint', usageLevel: 1 },
|
||||
{ name: 'decimal', id: 'decimal', usageLevel: 1 },
|
||||
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
||||
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
|
||||
{ name: 'date', id: 'date', usageLevel: 1 },
|
||||
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
||||
{ name: 'text', id: 'text', usageLevel: 1 },
|
||||
|
||||
// Level 2 - Second most common types
|
||||
{ name: 'json', id: 'json', usageLevel: 2 },
|
||||
{ name: 'uuid', id: 'uuid', usageLevel: 2 },
|
||||
|
||||
// Less common types
|
||||
{ name: 'tinyint', id: 'tinyint' },
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
{ name: 'mediumint', id: 'mediumint' },
|
||||
{ name: 'int', id: 'int' },
|
||||
{ name: 'bigint', id: 'bigint' },
|
||||
{ name: 'decimal', id: 'decimal' },
|
||||
{ name: 'numeric', id: 'numeric' },
|
||||
{ name: 'float', id: 'float' },
|
||||
{ name: 'double', id: 'double' },
|
||||
{ name: 'bit', id: 'bit' },
|
||||
{ name: 'bool', id: 'bool' },
|
||||
{ name: 'boolean', id: 'boolean' },
|
||||
|
||||
// Date and Time Types
|
||||
{ name: 'date', id: 'date' },
|
||||
{ name: 'datetime', id: 'datetime' },
|
||||
{ name: 'timestamp', id: 'timestamp' },
|
||||
{ name: 'time', id: 'time' },
|
||||
{ name: 'year', id: 'year' },
|
||||
|
||||
// String Types
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'binary', id: 'binary' },
|
||||
{ name: 'varbinary', id: 'varbinary' },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||
{ name: 'tinyblob', id: 'tinyblob' },
|
||||
{ name: 'blob', id: 'blob' },
|
||||
{ name: 'mediumblob', id: 'mediumblob' },
|
||||
{ name: 'longblob', id: 'longblob' },
|
||||
{ name: 'tinytext', id: 'tinytext' },
|
||||
{ name: 'text', id: 'text' },
|
||||
{ name: 'mediumtext', id: 'mediumtext' },
|
||||
{ name: 'longtext', id: 'longtext' },
|
||||
{ name: 'enum', id: 'enum' },
|
||||
{ name: 'set', id: 'set' },
|
||||
|
||||
// Spatial Types
|
||||
{ name: 'geometry', id: 'geometry' },
|
||||
{ name: 'point', id: 'point' },
|
||||
{ name: 'linestring', id: 'linestring' },
|
||||
@@ -47,8 +47,4 @@ export const mariadbDataTypes: readonly DataType[] = [
|
||||
{ name: 'multilinestring', id: 'multilinestring' },
|
||||
{ name: 'multipolygon', id: 'multipolygon' },
|
||||
{ name: 'geometrycollection', id: 'geometrycollection' },
|
||||
|
||||
// JSON Type
|
||||
{ name: 'json', id: 'json' },
|
||||
{ name: 'uuid', id: 'uuid' },
|
||||
] as const;
|
||||
|
@@ -1,44 +1,41 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const mysqlDataTypes: readonly DataType[] = [
|
||||
// Numeric Types
|
||||
export const mysqlDataTypes: readonly DataTypeData[] = [
|
||||
// Level 1 - Most commonly used types
|
||||
{ name: 'int', id: 'int', usageLevel: 1 },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
||||
{ name: 'text', id: 'text', usageLevel: 1 },
|
||||
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
||||
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
||||
{ name: 'date', id: 'date', usageLevel: 1 },
|
||||
|
||||
// Level 2 - Second most common types
|
||||
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
|
||||
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
|
||||
{ name: 'datetime', id: 'datetime', usageLevel: 2 },
|
||||
{ name: 'json', id: 'json', usageLevel: 2 },
|
||||
|
||||
// Less common types
|
||||
{ name: 'tinyint', id: 'tinyint' },
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
{ name: 'mediumint', id: 'mediumint' },
|
||||
{ name: 'int', id: 'int' },
|
||||
{ name: 'bigint', id: 'bigint' },
|
||||
{ name: 'decimal', id: 'decimal' },
|
||||
{ name: 'numeric', id: 'numeric' },
|
||||
{ name: 'float', id: 'float' },
|
||||
{ name: 'double', id: 'double' },
|
||||
{ name: 'bit', id: 'bit' },
|
||||
{ name: 'bool', id: 'bool' },
|
||||
{ name: 'boolean', id: 'boolean' },
|
||||
|
||||
// Date and Time Types
|
||||
{ name: 'date', id: 'date' },
|
||||
{ name: 'datetime', id: 'datetime' },
|
||||
{ name: 'timestamp', id: 'timestamp' },
|
||||
{ name: 'time', id: 'time' },
|
||||
{ name: 'year', id: 'year' },
|
||||
|
||||
// String Types
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'tinytext', id: 'tinytext' },
|
||||
{ name: 'mediumtext', id: 'mediumtext' },
|
||||
{ name: 'longtext', id: 'longtext' },
|
||||
{ name: 'binary', id: 'binary' },
|
||||
{ name: 'varbinary', id: 'varbinary' },
|
||||
{ name: 'tinyblob', id: 'tinyblob' },
|
||||
{ name: 'blob', id: 'blob' },
|
||||
{ name: 'mediumblob', id: 'mediumblob' },
|
||||
{ name: 'longblob', id: 'longblob' },
|
||||
{ name: 'tinytext', id: 'tinytext' },
|
||||
{ name: 'text', id: 'text' },
|
||||
{ name: 'mediumtext', id: 'mediumtext' },
|
||||
{ name: 'longtext', id: 'longtext' },
|
||||
{ name: 'enum', id: 'enum' },
|
||||
{ name: 'set', id: 'set' },
|
||||
|
||||
// Spatial Types
|
||||
{ name: 'time', id: 'time' },
|
||||
{ name: 'year', id: 'year' },
|
||||
{ name: 'geometry', id: 'geometry' },
|
||||
{ name: 'point', id: 'point' },
|
||||
{ name: 'linestring', id: 'linestring' },
|
||||
@@ -47,7 +44,4 @@ export const mysqlDataTypes: readonly DataType[] = [
|
||||
{ name: 'multilinestring', id: 'multilinestring' },
|
||||
{ name: 'multipolygon', id: 'multipolygon' },
|
||||
{ name: 'geometrycollection', id: 'geometrycollection' },
|
||||
|
||||
// JSON Type
|
||||
{ name: 'json', id: 'json' },
|
||||
] as const;
|
||||
|
@@ -1,45 +1,48 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const postgresDataTypes: readonly DataType[] = [
|
||||
// Numeric Types
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
{ name: 'integer', id: 'integer' },
|
||||
{ name: 'bigint', id: 'bigint' },
|
||||
{ name: 'decimal', id: 'decimal' },
|
||||
export const postgresDataTypes: readonly DataTypeData[] = [
|
||||
// Level 1 - Most commonly used types
|
||||
{ name: 'integer', id: 'integer', usageLevel: 1 },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
||||
{ name: 'text', id: 'text', usageLevel: 1 },
|
||||
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
||||
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
||||
{ name: 'date', id: 'date', usageLevel: 1 },
|
||||
|
||||
// Level 2 - Second most common types
|
||||
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
|
||||
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
|
||||
{ name: 'serial', id: 'serial', usageLevel: 2 },
|
||||
{ name: 'json', id: 'json', usageLevel: 2 },
|
||||
{ name: 'jsonb', id: 'jsonb', usageLevel: 2 },
|
||||
{ name: 'uuid', id: 'uuid', usageLevel: 2 },
|
||||
{
|
||||
name: 'timestamp with time zone',
|
||||
id: 'timestamp_with_time_zone',
|
||||
usageLevel: 2,
|
||||
},
|
||||
|
||||
// Less common types
|
||||
{ name: 'numeric', id: 'numeric' },
|
||||
{ name: 'real', id: 'real' },
|
||||
{ name: 'double precision', id: 'double_precision' },
|
||||
{ name: 'smallserial', id: 'smallserial' },
|
||||
{ name: 'serial', id: 'serial' },
|
||||
{ name: 'bigserial', id: 'bigserial' },
|
||||
{ name: 'money', id: 'money' },
|
||||
|
||||
// Character Types
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'character varying', id: 'character_varying' },
|
||||
{ name: 'text', id: 'text' },
|
||||
|
||||
// Binary Data Types
|
||||
{ name: 'bytea', id: 'bytea' },
|
||||
|
||||
// Date/Time Types
|
||||
{ name: 'date', id: 'date' },
|
||||
{ name: 'timestamp', id: 'timestamp' },
|
||||
{ name: 'timestamp with time zone', id: 'timestamp_with_time_zone' },
|
||||
{ name: 'timestamp without time zone', id: 'timestamp_without_time_zone' },
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{
|
||||
name: 'character varying',
|
||||
id: 'character_varying',
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{ name: 'time', id: 'time' },
|
||||
{ name: 'timestamp without time zone', id: 'timestamp_without_time_zone' },
|
||||
{ name: 'time with time zone', id: 'time_with_time_zone' },
|
||||
{ name: 'time without time zone', id: 'time_without_time_zone' },
|
||||
{ name: 'interval', id: 'interval' },
|
||||
|
||||
// Boolean Type
|
||||
{ name: 'boolean', id: 'boolean' },
|
||||
|
||||
// Enumerated Types
|
||||
{ name: 'bytea', id: 'bytea' },
|
||||
{ name: 'enum', id: 'enum' },
|
||||
|
||||
// Geometric Types
|
||||
{ name: 'point', id: 'point' },
|
||||
{ name: 'line', id: 'line' },
|
||||
{ name: 'lseg', id: 'lseg' },
|
||||
@@ -47,43 +50,22 @@ export const postgresDataTypes: readonly DataType[] = [
|
||||
{ name: 'path', id: 'path' },
|
||||
{ name: 'polygon', id: 'polygon' },
|
||||
{ name: 'circle', id: 'circle' },
|
||||
|
||||
// Network Address Types
|
||||
{ name: 'cidr', id: 'cidr' },
|
||||
{ name: 'inet', id: 'inet' },
|
||||
{ name: 'macaddr', id: 'macaddr' },
|
||||
{ name: 'macaddr8', id: 'macaddr8' },
|
||||
|
||||
// Bit String Types
|
||||
{ name: 'bit', id: 'bit' },
|
||||
{ name: 'bit varying', id: 'bit_varying' },
|
||||
|
||||
// Text Search Types
|
||||
{ name: 'tsvector', id: 'tsvector' },
|
||||
{ name: 'tsquery', id: 'tsquery' },
|
||||
|
||||
// UUID Type
|
||||
{ name: 'uuid', id: 'uuid' },
|
||||
|
||||
// XML Type
|
||||
{ name: 'xml', id: 'xml' },
|
||||
|
||||
// JSON Types
|
||||
{ name: 'json', id: 'json' },
|
||||
{ name: 'jsonb', id: 'jsonb' },
|
||||
|
||||
// Array Types
|
||||
{ name: 'array', id: 'array' },
|
||||
|
||||
// Range Types
|
||||
{ name: 'int4range', id: 'int4range' },
|
||||
{ name: 'int8range', id: 'int8range' },
|
||||
{ name: 'numrange', id: 'numrange' },
|
||||
{ name: 'tsrange', id: 'tsrange' },
|
||||
{ name: 'tstzrange', id: 'tstzrange' },
|
||||
{ name: 'daterange', id: 'daterange' },
|
||||
|
||||
// Object Identifier Types
|
||||
{ name: 'oid', id: 'oid' },
|
||||
{ name: 'regproc', id: 'regproc' },
|
||||
{ name: 'regprocedure', id: 'regprocedure' },
|
||||
@@ -95,7 +77,5 @@ export const postgresDataTypes: readonly DataType[] = [
|
||||
{ name: 'regnamespace', id: 'regnamespace' },
|
||||
{ name: 'regconfig', id: 'regconfig' },
|
||||
{ name: 'regdictionary', id: 'regdictionary' },
|
||||
|
||||
// User Defined types
|
||||
{ name: 'user-defined', id: 'user-defined' },
|
||||
] as const;
|
||||
|
@@ -1,56 +1,44 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const sqlServerDataTypes: readonly DataType[] = [
|
||||
// Exact Numerics
|
||||
{ name: 'bigint', id: 'bigint' },
|
||||
{ name: 'bit', id: 'bit' },
|
||||
{ name: 'decimal', id: 'decimal' },
|
||||
{ name: 'int', id: 'int' },
|
||||
{ name: 'money', id: 'money' },
|
||||
export const sqlServerDataTypes: readonly DataTypeData[] = [
|
||||
// Level 1 - Most commonly used types
|
||||
{ name: 'int', id: 'int', usageLevel: 1 },
|
||||
{ name: 'bit', id: 'bit', usageLevel: 1 },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
||||
{ name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true, usageLevel: 1 },
|
||||
{ name: 'text', id: 'text', usageLevel: 1 },
|
||||
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
|
||||
{ name: 'date', id: 'date', usageLevel: 1 },
|
||||
|
||||
// Level 2 - Second most common types
|
||||
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
|
||||
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
|
||||
{ name: 'datetime2', id: 'datetime2', usageLevel: 2 },
|
||||
{ name: 'uniqueidentifier', id: 'uniqueidentifier', usageLevel: 2 },
|
||||
{ name: 'json', id: 'json', usageLevel: 2 },
|
||||
|
||||
// Less common types
|
||||
{ name: 'numeric', id: 'numeric' },
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
{ name: 'smallmoney', id: 'smallmoney' },
|
||||
{ name: 'tinyint', id: 'tinyint' },
|
||||
|
||||
// Approximate Numerics
|
||||
{ name: 'money', id: 'money' },
|
||||
{ name: 'float', id: 'float' },
|
||||
{ name: 'real', id: 'real' },
|
||||
|
||||
// Date and Time
|
||||
{ name: 'date', id: 'date' },
|
||||
{ name: 'datetime2', id: 'datetime2' },
|
||||
{ name: 'datetime', id: 'datetime' },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'nchar', id: 'nchar', hasCharMaxLength: true },
|
||||
{ name: 'ntext', id: 'ntext' },
|
||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||
{ name: 'image', id: 'image' },
|
||||
{ name: 'datetimeoffset', id: 'datetimeoffset' },
|
||||
{ name: 'smalldatetime', id: 'smalldatetime' },
|
||||
{ name: 'time', id: 'time' },
|
||||
|
||||
// Character Strings
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'text', id: 'text' },
|
||||
|
||||
// Unicode Character Strings
|
||||
{ name: 'nchar', id: 'nchar' },
|
||||
{ name: 'nvarchar', id: 'nvarchar' },
|
||||
{ name: 'ntext', id: 'ntext' },
|
||||
|
||||
// Binary Strings
|
||||
{ name: 'binary', id: 'binary' },
|
||||
{ name: 'varbinary', id: 'varbinary' },
|
||||
{ name: 'image', id: 'image' },
|
||||
|
||||
// Other Data Types
|
||||
{ name: 'timestamp', id: 'timestamp' },
|
||||
{ name: 'xml', id: 'xml' },
|
||||
{ name: 'cursor', id: 'cursor' },
|
||||
{ name: 'hierarchyid', id: 'hierarchyid' },
|
||||
{ name: 'sql_variant', id: 'sql_variant' },
|
||||
{ name: 'timestamp', id: 'timestamp' },
|
||||
{ name: 'uniqueidentifier', id: 'uniqueidentifier' },
|
||||
{ name: 'xml', id: 'xml' },
|
||||
|
||||
// Spatial Data Types
|
||||
{ name: 'geometry', id: 'geometry' },
|
||||
{ name: 'geography', id: 'geography' },
|
||||
|
||||
// JSON
|
||||
{ name: 'json', id: 'json' },
|
||||
] as const;
|
||||
|
@@ -1,27 +1,34 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const sqliteDataTypes: readonly DataType[] = [
|
||||
// Numeric Types
|
||||
{ name: 'integer', id: 'integer' },
|
||||
{ name: 'real', id: 'real' },
|
||||
{ name: 'numeric', id: 'numeric' },
|
||||
export const sqliteDataTypes: readonly DataTypeData[] = [
|
||||
// Level 1 - Most commonly used types - SQLite's 5 storage classes
|
||||
{ name: 'integer', id: 'integer', usageLevel: 1 },
|
||||
{ name: 'text', id: 'text', usageLevel: 1 },
|
||||
{ name: 'real', id: 'real', usageLevel: 1 },
|
||||
{ name: 'blob', id: 'blob', usageLevel: 1 },
|
||||
{ name: 'null', id: 'null', usageLevel: 1 },
|
||||
|
||||
// Text Type
|
||||
{ name: 'text', id: 'text' },
|
||||
// SQLite type aliases and common types
|
||||
{ name: 'int', id: 'int', usageLevel: 1 },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
|
||||
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
|
||||
{ name: 'date', id: 'date', usageLevel: 1 },
|
||||
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
|
||||
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
|
||||
|
||||
// Blob Type
|
||||
{ name: 'blob', id: 'blob' },
|
||||
// Level 2 - Second most common types
|
||||
{ name: 'numeric', id: 'numeric', usageLevel: 2 },
|
||||
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
|
||||
{ name: 'float', id: 'float', usageLevel: 2 },
|
||||
{ name: 'double', id: 'double', usageLevel: 2 },
|
||||
{ name: 'json', id: 'json', usageLevel: 2 },
|
||||
|
||||
// Blob Type
|
||||
{ name: 'json', id: 'json' },
|
||||
|
||||
// Date/Time Types (SQLite uses TEXT, REAL, or INTEGER types for dates and times)
|
||||
{ name: 'date', id: 'date' },
|
||||
{ name: 'datetime', id: 'datetime' },
|
||||
|
||||
{ name: 'int', id: 'int' },
|
||||
{ name: 'float', id: 'float' },
|
||||
{ name: 'boolean', id: 'boolean' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'decimal', id: 'decimal' },
|
||||
// Less common types (all map to SQLite storage classes)
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'binary', id: 'binary' },
|
||||
{ name: 'varbinary', id: 'varbinary' },
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
{ name: 'bigint', id: 'bigint' },
|
||||
{ name: 'bool', id: 'bool' },
|
||||
{ name: 'time', id: 'time' },
|
||||
] as const;
|
||||
|
447
src/lib/data/export-metadata/export-per-type/mysql.ts
Normal file
@@ -0,0 +1,447 @@
|
||||
import {
|
||||
exportFieldComment,
|
||||
isFunction,
|
||||
isKeyword,
|
||||
strHasQuotes,
|
||||
} from './common';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||
|
||||
function parseMySQLDefault(field: DBField): string {
|
||||
if (!field.default) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const defaultValue = field.default.trim();
|
||||
|
||||
// Handle specific MySQL default values
|
||||
if (
|
||||
defaultValue.toLowerCase() === 'now()' ||
|
||||
defaultValue.toLowerCase() === 'current_timestamp'
|
||||
) {
|
||||
return 'CURRENT_TIMESTAMP';
|
||||
}
|
||||
|
||||
// Handle MySQL auto-increment, which is handled via AUTO_INCREMENT
|
||||
if (
|
||||
defaultValue.toLowerCase().includes('identity') ||
|
||||
defaultValue.toLowerCase().includes('autoincrement') ||
|
||||
defaultValue.includes('nextval')
|
||||
) {
|
||||
return ''; // MySQL handles this with AUTO_INCREMENT
|
||||
}
|
||||
|
||||
// If it's a function call, convert to MySQL equivalents
|
||||
if (isFunction(defaultValue)) {
|
||||
// Map common PostgreSQL/MSSQL functions to MySQL equivalents
|
||||
if (
|
||||
defaultValue.toLowerCase().includes('newid()') ||
|
||||
defaultValue.toLowerCase().includes('uuid()')
|
||||
) {
|
||||
return 'UUID()';
|
||||
}
|
||||
|
||||
// For functions we can't translate, return as is (MySQL might not support them)
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// If it's a keyword, keep it as is
|
||||
if (isKeyword(defaultValue)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// If it already has quotes, keep it as is
|
||||
if (strHasQuotes(defaultValue)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// If it's a number, keep it as is
|
||||
if (/^-?\d+(\.\d+)?$/.test(defaultValue)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// For other cases, add quotes
|
||||
return `'${defaultValue.replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
// Map problematic types to MySQL compatible types
|
||||
function mapMySQLType(typeName: string): string {
|
||||
typeName = typeName.toLowerCase();
|
||||
|
||||
// Map common types to MySQL type system
|
||||
switch (typeName) {
|
||||
case 'int':
|
||||
case 'integer':
|
||||
return 'INT';
|
||||
|
||||
case 'smallint':
|
||||
return 'SMALLINT';
|
||||
|
||||
case 'bigint':
|
||||
return 'BIGINT';
|
||||
|
||||
case 'decimal':
|
||||
case 'numeric':
|
||||
return 'DECIMAL';
|
||||
|
||||
case 'float':
|
||||
return 'FLOAT';
|
||||
|
||||
case 'double':
|
||||
case 'real':
|
||||
return 'DOUBLE';
|
||||
|
||||
case 'char':
|
||||
case 'character':
|
||||
return 'CHAR';
|
||||
|
||||
case 'varchar':
|
||||
case 'character varying':
|
||||
case 'nvarchar':
|
||||
return 'VARCHAR';
|
||||
|
||||
case 'text':
|
||||
case 'ntext':
|
||||
return 'TEXT';
|
||||
|
||||
case 'longtext':
|
||||
return 'LONGTEXT';
|
||||
|
||||
case 'mediumtext':
|
||||
return 'MEDIUMTEXT';
|
||||
|
||||
case 'tinytext':
|
||||
return 'TINYTEXT';
|
||||
|
||||
case 'date':
|
||||
return 'DATE';
|
||||
|
||||
case 'datetime':
|
||||
case 'timestamp':
|
||||
case 'datetime2':
|
||||
return 'DATETIME';
|
||||
|
||||
case 'time':
|
||||
return 'TIME';
|
||||
|
||||
case 'blob':
|
||||
case 'binary':
|
||||
return 'BLOB';
|
||||
|
||||
case 'varbinary':
|
||||
return 'VARBINARY';
|
||||
|
||||
case 'bit':
|
||||
return 'BIT';
|
||||
|
||||
case 'boolean':
|
||||
case 'bool':
|
||||
return 'TINYINT(1)'; // MySQL uses TINYINT(1) for boolean
|
||||
|
||||
case 'enum':
|
||||
return 'VARCHAR(50)'; // Convert ENUM to VARCHAR instead of assuming values
|
||||
|
||||
case 'json':
|
||||
case 'jsonb':
|
||||
return 'JSON'; // MySQL has JSON type since 5.7.8
|
||||
|
||||
case 'uuid':
|
||||
return 'CHAR(36)'; // MySQL doesn't have a UUID type, use CHAR(36)
|
||||
|
||||
case 'geometry':
|
||||
case 'geography':
|
||||
return 'GEOMETRY'; // If MySQL has spatial extensions
|
||||
|
||||
case 'array':
|
||||
case 'user-defined':
|
||||
return 'JSON'; // Use JSON for complex types like arrays or user-defined
|
||||
}
|
||||
|
||||
// If type has array notation (ends with []), treat as JSON
|
||||
if (typeName.endsWith('[]')) {
|
||||
return 'JSON';
|
||||
}
|
||||
|
||||
// For any other types, default to original type
|
||||
return typeName;
|
||||
}
|
||||
|
||||
export function exportMySQL(diagram: Diagram): string {
|
||||
if (!diagram.tables || !diagram.relationships) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const tables = diagram.tables;
|
||||
const relationships = diagram.relationships;
|
||||
|
||||
// Start SQL script
|
||||
let sqlScript = '-- MySQL database export\n\n';
|
||||
|
||||
// MySQL doesn't really use transactions for DDL statements but we'll add it for consistency
|
||||
sqlScript += 'START TRANSACTION;\n\n';
|
||||
|
||||
// Create databases (schemas) if they don't exist
|
||||
const schemas = new Set<string>();
|
||||
tables.forEach((table) => {
|
||||
if (table.schema) {
|
||||
schemas.add(table.schema);
|
||||
}
|
||||
});
|
||||
|
||||
schemas.forEach((schema) => {
|
||||
sqlScript += `CREATE DATABASE IF NOT EXISTS \`${schema}\`;\n`;
|
||||
});
|
||||
|
||||
if (schemas.size > 0) {
|
||||
sqlScript += '\n';
|
||||
}
|
||||
|
||||
// Generate table creation SQL
|
||||
sqlScript += tables
|
||||
.map((table: DBTable) => {
|
||||
// Skip views
|
||||
if (table.isView) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Use schema prefix if available
|
||||
const tableName = table.schema
|
||||
? `\`${table.schema}\`.\`${table.name}\``
|
||||
: `\`${table.name}\``;
|
||||
|
||||
// Get primary key fields
|
||||
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
|
||||
|
||||
return `${
|
||||
table.comments ? `-- ${table.comments}\n` : ''
|
||||
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
|
||||
.map((field: DBField) => {
|
||||
const fieldName = `\`${field.name}\``;
|
||||
|
||||
// Handle type name - map to MySQL compatible types
|
||||
const typeName = mapMySQLType(field.type.name);
|
||||
|
||||
// Handle MySQL specific type formatting
|
||||
let typeWithSize = typeName;
|
||||
if (field.characterMaximumLength) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'varchar' ||
|
||||
typeName.toLowerCase() === 'char' ||
|
||||
typeName.toLowerCase() === 'varbinary'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
||||
}
|
||||
} else if (field.precision && field.scale) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'decimal' ||
|
||||
typeName.toLowerCase() === 'numeric'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
|
||||
}
|
||||
} else if (field.precision) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'decimal' ||
|
||||
typeName.toLowerCase() === 'numeric'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.precision})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Set a default size for VARCHAR columns if not specified
|
||||
if (
|
||||
typeName.toLowerCase() === 'varchar' &&
|
||||
!field.characterMaximumLength
|
||||
) {
|
||||
typeWithSize = `${typeName}(255)`;
|
||||
}
|
||||
|
||||
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||
|
||||
// Handle auto_increment - MySQL uses AUTO_INCREMENT keyword
|
||||
let autoIncrement = '';
|
||||
if (
|
||||
field.primaryKey &&
|
||||
(field.default?.toLowerCase().includes('identity') ||
|
||||
field.default
|
||||
?.toLowerCase()
|
||||
.includes('autoincrement') ||
|
||||
field.default?.includes('nextval'))
|
||||
) {
|
||||
autoIncrement = ' AUTO_INCREMENT';
|
||||
}
|
||||
|
||||
// Only add UNIQUE constraint if the field is not part of the primary key
|
||||
const unique =
|
||||
!field.primaryKey && field.unique ? ' UNIQUE' : '';
|
||||
|
||||
// Handle default value
|
||||
const defaultValue =
|
||||
field.default &&
|
||||
!field.default.toLowerCase().includes('identity') &&
|
||||
!field.default
|
||||
.toLowerCase()
|
||||
.includes('autoincrement') &&
|
||||
!field.default.includes('nextval')
|
||||
? ` DEFAULT ${parseMySQLDefault(field)}`
|
||||
: '';
|
||||
|
||||
// MySQL supports inline comments
|
||||
const comment = field.comments
|
||||
? ` COMMENT '${field.comments.replace(/'/g, "''")}'`
|
||||
: '';
|
||||
|
||||
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${autoIncrement}${unique}${defaultValue}${comment}`;
|
||||
})
|
||||
.join(',\n')}${
|
||||
// Add PRIMARY KEY as table constraint
|
||||
primaryKeyFields.length > 0
|
||||
? `,\n PRIMARY KEY (${primaryKeyFields
|
||||
.map((f) => `\`${f.name}\``)
|
||||
.join(', ')})`
|
||||
: ''
|
||||
}\n)${
|
||||
// MySQL supports table comments
|
||||
table.comments
|
||||
? ` COMMENT='${table.comments.replace(/'/g, "''")}'`
|
||||
: ''
|
||||
};\n\n${
|
||||
// Add indexes - MySQL creates them separately from the table definition
|
||||
table.indexes
|
||||
.map((index) => {
|
||||
// Get the list of fields for this index
|
||||
const indexFields = index.fieldIds
|
||||
.map((fieldId) => {
|
||||
const field = table.fields.find(
|
||||
(f) => f.id === fieldId
|
||||
);
|
||||
return field ? field : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
// Skip if this index exactly matches the primary key fields
|
||||
if (
|
||||
primaryKeyFields.length === indexFields.length &&
|
||||
primaryKeyFields.every((pk) =>
|
||||
indexFields.some(
|
||||
(field) => field && field.id === pk.id
|
||||
)
|
||||
)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Create a unique index name by combining table name, field names, and a unique/non-unique indicator
|
||||
const fieldNamesForIndex = indexFields
|
||||
.map((field) => field?.name || '')
|
||||
.join('_');
|
||||
const uniqueIndicator = index.unique ? '_unique' : '';
|
||||
const indexName = `\`idx_${table.name}_${fieldNamesForIndex}${uniqueIndicator}\``;
|
||||
|
||||
// Get the properly quoted field names
|
||||
const indexFieldNames = indexFields
|
||||
.map((field) => (field ? `\`${field.name}\`` : ''))
|
||||
.filter(Boolean);
|
||||
|
||||
// Check for text/blob fields that need special handling
|
||||
const hasTextOrBlob = indexFields.some((field) => {
|
||||
const typeName =
|
||||
field?.type.name.toLowerCase() || '';
|
||||
return (
|
||||
typeName === 'text' ||
|
||||
typeName === 'mediumtext' ||
|
||||
typeName === 'longtext' ||
|
||||
typeName === 'blob'
|
||||
);
|
||||
});
|
||||
|
||||
// If there are TEXT/BLOB fields, need to add prefix length
|
||||
const indexFieldsWithPrefix = hasTextOrBlob
|
||||
? indexFieldNames.map((name) => {
|
||||
const field = indexFields.find(
|
||||
(f) => `\`${f?.name}\`` === name
|
||||
);
|
||||
if (!field) return name;
|
||||
|
||||
const typeName =
|
||||
field.type.name.toLowerCase();
|
||||
if (
|
||||
typeName === 'text' ||
|
||||
typeName === 'mediumtext' ||
|
||||
typeName === 'longtext' ||
|
||||
typeName === 'blob'
|
||||
) {
|
||||
// Add a prefix length for TEXT/BLOB fields (required in MySQL)
|
||||
return `${name}(255)`;
|
||||
}
|
||||
return name;
|
||||
})
|
||||
: indexFieldNames;
|
||||
|
||||
return indexFieldNames.length > 0
|
||||
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFieldsWithPrefix.join(', ')});\n`
|
||||
: '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
}`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings (views)
|
||||
.join('\n');
|
||||
|
||||
// Generate foreign keys
|
||||
if (relationships.length > 0) {
|
||||
sqlScript += '\n-- Foreign key constraints\n\n';
|
||||
|
||||
sqlScript += relationships
|
||||
.map((r: DBRelationship) => {
|
||||
const sourceTable = tables.find(
|
||||
(t) => t.id === r.sourceTableId
|
||||
);
|
||||
const targetTable = tables.find(
|
||||
(t) => t.id === r.targetTableId
|
||||
);
|
||||
|
||||
if (
|
||||
!sourceTable ||
|
||||
!targetTable ||
|
||||
sourceTable.isView ||
|
||||
targetTable.isView
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const sourceField = sourceTable.fields.find(
|
||||
(f) => f.id === r.sourceFieldId
|
||||
);
|
||||
const targetField = targetTable.fields.find(
|
||||
(f) => f.id === r.targetFieldId
|
||||
);
|
||||
|
||||
if (!sourceField || !targetField) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const sourceTableName = sourceTable.schema
|
||||
? `\`${sourceTable.schema}\`.\`${sourceTable.name}\``
|
||||
: `\`${sourceTable.name}\``;
|
||||
const targetTableName = targetTable.schema
|
||||
? `\`${targetTable.schema}\`.\`${targetTable.name}\``
|
||||
: `\`${targetTable.name}\``;
|
||||
|
||||
// Create a descriptive constraint name
|
||||
const constraintName = `\`fk_${sourceTable.name}_${sourceField.name}\``;
|
||||
|
||||
// MySQL supports ON DELETE and ON UPDATE actions
|
||||
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY(\`${sourceField.name}\`) REFERENCES ${targetTableName}(\`${targetField.name}\`)\nON UPDATE CASCADE ON DELETE RESTRICT;\n`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
sqlScript += '\nCOMMIT;\n';
|
||||
|
||||
return sqlScript;
|
||||
}
|
379
src/lib/data/export-metadata/export-per-type/postgresql.ts
Normal file
@@ -0,0 +1,379 @@
|
||||
import {
|
||||
exportFieldComment,
|
||||
isFunction,
|
||||
isKeyword,
|
||||
strHasQuotes,
|
||||
} from './common';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||
|
||||
function parsePostgresDefault(field: DBField): string {
|
||||
if (!field.default || typeof field.default !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
const defaultValue = field.default.trim();
|
||||
|
||||
// Handle specific PostgreSQL default values
|
||||
if (defaultValue.toLowerCase() === 'now()') {
|
||||
return 'CURRENT_TIMESTAMP';
|
||||
}
|
||||
|
||||
// Handle PostgreSQL functions for JSON/JSONB types
|
||||
if (
|
||||
(field.type.name.toLowerCase() === 'json' ||
|
||||
field.type.name.toLowerCase() === 'jsonb') &&
|
||||
(defaultValue.includes('json_build_object') ||
|
||||
defaultValue.includes('jsonb_build_object') ||
|
||||
defaultValue.includes('json_build_array') ||
|
||||
defaultValue.includes('jsonb_build_array') ||
|
||||
defaultValue.includes('to_json') ||
|
||||
defaultValue.includes('to_jsonb'))
|
||||
) {
|
||||
// Remove any enclosing quotes and return the function call as is
|
||||
return defaultValue.replace(/^'(.*)'$/, '$1').replace(/''/, "'");
|
||||
}
|
||||
|
||||
// Handle nextval sequences for PostgreSQL
|
||||
if (defaultValue.includes('nextval')) {
|
||||
return defaultValue; // Keep it as is for PostgreSQL
|
||||
}
|
||||
|
||||
// If it's a function call, keep it as is
|
||||
if (isFunction(defaultValue)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// If it's a keyword, keep it as is
|
||||
if (isKeyword(defaultValue)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// If it already has quotes, keep it as is
|
||||
if (strHasQuotes(defaultValue)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// If it's a number, keep it as is
|
||||
if (/^-?\d+(\.\d+)?$/.test(defaultValue)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// For other cases, add quotes
|
||||
return `'${defaultValue.replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
// Map problematic types to PostgreSQL compatible types
|
||||
function mapPostgresType(typeName: string, fieldName: string): string {
|
||||
typeName = typeName.toLowerCase();
|
||||
fieldName = fieldName.toLowerCase();
|
||||
|
||||
// Handle known problematic types
|
||||
if (typeName === 'user-defined') {
|
||||
return 'jsonb'; // Default fallback for user-defined types
|
||||
}
|
||||
|
||||
// Handle generic "array" type (when not specified as array of what)
|
||||
if (typeName === 'array') {
|
||||
return 'text[]'; // Default to text array
|
||||
}
|
||||
|
||||
// Handle array type notation
|
||||
if (typeName.endsWith('[]')) {
|
||||
const baseType = mapPostgresType(typeName.slice(0, -2), fieldName);
|
||||
return `${baseType}[]`;
|
||||
}
|
||||
|
||||
// Default case: return the type as is
|
||||
return typeName;
|
||||
}
|
||||
|
||||
export function exportPostgreSQL(diagram: Diagram): string {
|
||||
if (!diagram.tables || !diagram.relationships) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const tables = diagram.tables;
|
||||
const relationships = diagram.relationships;
|
||||
|
||||
// Create CREATE SCHEMA statements for all schemas
|
||||
let sqlScript = '';
|
||||
const schemas = new Set<string>();
|
||||
|
||||
tables.forEach((table) => {
|
||||
if (table.schema) {
|
||||
schemas.add(table.schema);
|
||||
}
|
||||
});
|
||||
|
||||
// Add schema creation statements
|
||||
schemas.forEach((schema) => {
|
||||
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
|
||||
});
|
||||
sqlScript += '\n';
|
||||
|
||||
// Add sequence creation statements
|
||||
const sequences = new Set<string>();
|
||||
|
||||
tables.forEach((table) => {
|
||||
table.fields.forEach((field) => {
|
||||
if (field.default) {
|
||||
// Match nextval('schema.sequence_name') or nextval('sequence_name')
|
||||
const match = field.default.match(
|
||||
/nextval\('([^']+)'(?:::[^)]+)?\)/
|
||||
);
|
||||
if (match) {
|
||||
sequences.add(match[1]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
sequences.forEach((sequence) => {
|
||||
sqlScript += `CREATE SEQUENCE IF NOT EXISTS ${sequence};\n`;
|
||||
});
|
||||
sqlScript += '\n';
|
||||
|
||||
// Generate table creation SQL
|
||||
sqlScript += tables
|
||||
.map((table: DBTable) => {
|
||||
// Skip views
|
||||
if (table.isView) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const tableName = table.schema
|
||||
? `"${table.schema}"."${table.name}"`
|
||||
: `"${table.name}"`;
|
||||
|
||||
// Get primary key fields
|
||||
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
|
||||
|
||||
return `${
|
||||
table.comments ? `-- ${table.comments}\n` : ''
|
||||
}CREATE TABLE ${tableName} (\n${table.fields
|
||||
.map((field: DBField) => {
|
||||
const fieldName = `"${field.name}"`;
|
||||
|
||||
// Handle type name - map problematic types to PostgreSQL compatible types
|
||||
const typeName = mapPostgresType(
|
||||
field.type.name,
|
||||
field.name
|
||||
);
|
||||
|
||||
// Handle PostgreSQL specific type formatting
|
||||
let typeWithSize = typeName;
|
||||
let serialType = null;
|
||||
|
||||
if (field.increment && !field.nullable) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'integer' ||
|
||||
typeName.toLowerCase() === 'int'
|
||||
) {
|
||||
serialType = 'SERIAL';
|
||||
} else if (typeName.toLowerCase() === 'bigint') {
|
||||
serialType = 'BIGSERIAL';
|
||||
} else if (typeName.toLowerCase() === 'smallint') {
|
||||
serialType = 'SMALLSERIAL';
|
||||
}
|
||||
}
|
||||
|
||||
if (field.characterMaximumLength) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'varchar' ||
|
||||
typeName.toLowerCase() === 'character varying' ||
|
||||
typeName.toLowerCase() === 'char' ||
|
||||
typeName.toLowerCase() === 'character'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
||||
}
|
||||
} else if (field.precision && field.scale) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'decimal' ||
|
||||
typeName.toLowerCase() === 'numeric'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
|
||||
}
|
||||
} else if (field.precision) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'decimal' ||
|
||||
typeName.toLowerCase() === 'numeric'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.precision})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle array types (check if the type name ends with '[]')
|
||||
if (typeName.endsWith('[]')) {
|
||||
typeWithSize = typeWithSize.replace('[]', '') + '[]';
|
||||
}
|
||||
|
||||
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||
|
||||
// Handle identity generation
|
||||
let identity = '';
|
||||
if (field.default && field.default.includes('nextval')) {
|
||||
// PostgreSQL already handles this with DEFAULT nextval()
|
||||
} else if (
|
||||
field.default &&
|
||||
field.default.toLowerCase().includes('identity')
|
||||
) {
|
||||
identity = ' GENERATED BY DEFAULT AS IDENTITY';
|
||||
}
|
||||
|
||||
// Only add UNIQUE constraint if the field is not part of the primary key
|
||||
// This avoids redundant uniqueness constraints
|
||||
const unique =
|
||||
!field.primaryKey && field.unique ? ' UNIQUE' : '';
|
||||
|
||||
// Handle default value using PostgreSQL specific parser
|
||||
const defaultValue =
|
||||
field.default &&
|
||||
!field.default.toLowerCase().includes('identity')
|
||||
? ` DEFAULT ${parsePostgresDefault(field)}`
|
||||
: '';
|
||||
|
||||
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
|
||||
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${serialType || typeWithSize}${serialType ? '' : notNull}${identity}${unique}${defaultValue}`;
|
||||
})
|
||||
.join(',\n')}${
|
||||
primaryKeyFields.length > 0
|
||||
? `,\n PRIMARY KEY (${primaryKeyFields
|
||||
.map((f) => `"${f.name}"`)
|
||||
.join(', ')})`
|
||||
: ''
|
||||
}\n);\n\n${
|
||||
// Add table comments
|
||||
table.comments
|
||||
? `COMMENT ON TABLE ${tableName} IS '${table.comments.replace(/'/g, "''")}';\n\n`
|
||||
: ''
|
||||
}${
|
||||
// Add column comments
|
||||
table.fields
|
||||
.filter((f) => f.comments)
|
||||
.map(
|
||||
(f) =>
|
||||
`COMMENT ON COLUMN ${tableName}."${f.name}" IS '${f.comments?.replace(/'/g, "''")}';\n`
|
||||
)
|
||||
.join('')
|
||||
}\n${
|
||||
// Add indexes only for non-primary key fields or composite indexes
|
||||
// This avoids duplicate indexes on primary key columns
|
||||
table.indexes
|
||||
.map((index) => {
|
||||
// Get the list of fields for this index
|
||||
const indexFields = index.fieldIds
|
||||
.map((fieldId) => {
|
||||
const field = table.fields.find(
|
||||
(f) => f.id === fieldId
|
||||
);
|
||||
return field ? field : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
// Skip if this index exactly matches the primary key fields
|
||||
// This prevents creating redundant indexes
|
||||
if (
|
||||
primaryKeyFields.length === indexFields.length &&
|
||||
primaryKeyFields.every((pk) =>
|
||||
indexFields.some(
|
||||
(field) => field && field.id === pk.id
|
||||
)
|
||||
)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Create unique index name using table name and index name
|
||||
// This ensures index names are unique across the database
|
||||
const safeTableName = table.name.replace(
|
||||
/[^a-zA-Z0-9_]/g,
|
||||
'_'
|
||||
);
|
||||
const safeIndexName = index.name.replace(
|
||||
/[^a-zA-Z0-9_]/g,
|
||||
'_'
|
||||
);
|
||||
|
||||
// Limit index name length to avoid PostgreSQL's 63-character identifier limit
|
||||
let combinedName = `${safeTableName}_${safeIndexName}`;
|
||||
if (combinedName.length > 60) {
|
||||
// If too long, use just the index name or a truncated version
|
||||
combinedName =
|
||||
safeIndexName.length > 60
|
||||
? safeIndexName.substring(0, 60)
|
||||
: safeIndexName;
|
||||
}
|
||||
|
||||
const indexName = `"${combinedName}"`;
|
||||
|
||||
// Get the properly quoted field names
|
||||
const indexFieldNames = indexFields
|
||||
.map((field) => (field ? `"${field.name}"` : ''))
|
||||
.filter(Boolean);
|
||||
|
||||
return indexFieldNames.length > 0
|
||||
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFieldNames.join(', ')});\n\n`
|
||||
: '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('')
|
||||
}`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings (views)
|
||||
.join('\n');
|
||||
|
||||
// Generate foreign keys
|
||||
sqlScript += `\n${relationships
|
||||
.map((r: DBRelationship) => {
|
||||
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
|
||||
const targetTable = tables.find((t) => t.id === r.targetTableId);
|
||||
|
||||
if (
|
||||
!sourceTable ||
|
||||
!targetTable ||
|
||||
sourceTable.isView ||
|
||||
targetTable.isView
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const sourceField = sourceTable.fields.find(
|
||||
(f) => f.id === r.sourceFieldId
|
||||
);
|
||||
const targetField = targetTable.fields.find(
|
||||
(f) => f.id === r.targetFieldId
|
||||
);
|
||||
|
||||
if (!sourceField || !targetField) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const sourceTableName = sourceTable.schema
|
||||
? `"${sourceTable.schema}"."${sourceTable.name}"`
|
||||
: `"${sourceTable.name}"`;
|
||||
const targetTableName = targetTable.schema
|
||||
? `"${targetTable.schema}"."${targetTable.name}"`
|
||||
: `"${targetTable.name}"`;
|
||||
|
||||
// Create a unique constraint name by combining table and field names
|
||||
// Ensure it stays within PostgreSQL's 63-character limit for identifiers
|
||||
// and doesn't get truncated in a way that breaks SQL syntax
|
||||
const baseName = `fk_${sourceTable.name}_${sourceField.name}_${targetTable.name}_${targetField.name}`;
|
||||
// Limit to 60 chars (63 minus quotes) to ensure the whole identifier stays within limits
|
||||
const safeConstraintName =
|
||||
baseName.length > 60
|
||||
? baseName.substring(0, 60).replace(/[^a-zA-Z0-9_]/g, '_')
|
||||
: baseName.replace(/[^a-zA-Z0-9_]/g, '_');
|
||||
|
||||
const constraintName = `"${safeConstraintName}"`;
|
||||
|
||||
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT ${constraintName} FOREIGN KEY("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}");\n`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings
|
||||
.join('\n')}`;
|
||||
|
||||
return sqlScript;
|
||||
}
|
358
src/lib/data/export-metadata/export-per-type/sqlite.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
import {
|
||||
exportFieldComment,
|
||||
isFunction,
|
||||
isKeyword,
|
||||
strHasQuotes,
|
||||
} from './common';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||
|
||||
function parseSQLiteDefault(field: DBField): string {
|
||||
if (!field.default) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const defaultValue = field.default.trim();
|
||||
|
||||
// Handle specific SQLite default values
|
||||
if (
|
||||
defaultValue.toLowerCase() === 'now()' ||
|
||||
defaultValue.toLowerCase() === 'current_timestamp'
|
||||
) {
|
||||
return 'CURRENT_TIMESTAMP';
|
||||
}
|
||||
|
||||
// Handle SQLite auto-increment
|
||||
if (
|
||||
defaultValue.toLowerCase().includes('identity') ||
|
||||
defaultValue.toLowerCase().includes('autoincrement') ||
|
||||
defaultValue.includes('nextval')
|
||||
) {
|
||||
return ''; // SQLite handles this differently with INTEGER PRIMARY KEY AUTOINCREMENT
|
||||
}
|
||||
|
||||
// If it's a function call, convert to SQLite equivalents
|
||||
if (isFunction(defaultValue)) {
|
||||
// Map common PostgreSQL/MSSQL functions to SQLite equivalents
|
||||
if (
|
||||
defaultValue.toLowerCase().includes('newid()') ||
|
||||
defaultValue.toLowerCase().includes('uuid()')
|
||||
) {
|
||||
return 'lower(hex(randomblob(16)))';
|
||||
}
|
||||
|
||||
// For functions we can't translate, return as is (SQLite might not support them)
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// If it's a keyword, keep it as is
|
||||
if (isKeyword(defaultValue)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// If it already has quotes, keep it as is
|
||||
if (strHasQuotes(defaultValue)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// If it's a number, keep it as is
|
||||
if (/^-?\d+(\.\d+)?$/.test(defaultValue)) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// For other cases, add quotes
|
||||
return `'${defaultValue.replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
// Map problematic types to SQLite compatible types
|
||||
function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
|
||||
typeName = typeName.toLowerCase();
|
||||
|
||||
// Special handling for primary key integer columns (autoincrement requires INTEGER PRIMARY KEY)
|
||||
if (isPrimaryKey && (typeName === 'integer' || typeName === 'int')) {
|
||||
return 'INTEGER'; // Must be uppercase for SQLite to recognize it for AUTOINCREMENT
|
||||
}
|
||||
|
||||
// Map common types to SQLite's simplified type system
|
||||
switch (typeName) {
|
||||
case 'int':
|
||||
case 'smallint':
|
||||
case 'tinyint':
|
||||
case 'mediumint':
|
||||
case 'bigint':
|
||||
return 'INTEGER';
|
||||
|
||||
case 'decimal':
|
||||
case 'numeric':
|
||||
case 'float':
|
||||
case 'double':
|
||||
case 'real':
|
||||
return 'REAL';
|
||||
|
||||
case 'char':
|
||||
case 'nchar':
|
||||
case 'varchar':
|
||||
case 'nvarchar':
|
||||
case 'text':
|
||||
case 'ntext':
|
||||
case 'character varying':
|
||||
case 'character':
|
||||
return 'TEXT';
|
||||
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
case 'timestamp':
|
||||
case 'datetime2':
|
||||
return 'TEXT'; // SQLite doesn't have dedicated date types
|
||||
|
||||
case 'blob':
|
||||
case 'binary':
|
||||
case 'varbinary':
|
||||
case 'image':
|
||||
return 'BLOB';
|
||||
|
||||
case 'bit':
|
||||
case 'boolean':
|
||||
return 'INTEGER'; // SQLite doesn't have a boolean type, use INTEGER
|
||||
|
||||
case 'user-defined':
|
||||
case 'json':
|
||||
case 'jsonb':
|
||||
return 'TEXT'; // Store as JSON text
|
||||
|
||||
case 'array':
|
||||
return 'TEXT'; // Store as serialized array text
|
||||
|
||||
case 'geometry':
|
||||
case 'geography':
|
||||
return 'BLOB'; // Store spatial data as BLOB in SQLite
|
||||
}
|
||||
|
||||
// If type has array notation (ends with []), treat as TEXT
|
||||
if (typeName.endsWith('[]')) {
|
||||
return 'TEXT';
|
||||
}
|
||||
|
||||
// For any other types, default to TEXT
|
||||
return typeName;
|
||||
}
|
||||
|
||||
export function exportSQLite(diagram: Diagram): string {
|
||||
if (!diagram.tables || !diagram.relationships) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const tables = diagram.tables;
|
||||
const relationships = diagram.relationships;
|
||||
|
||||
// Start SQL script - SQLite doesn't use schemas, so we skip schema creation
|
||||
let sqlScript = '-- SQLite database export\n\n';
|
||||
|
||||
// Begin transaction for faster import
|
||||
sqlScript += 'BEGIN TRANSACTION;\n\n';
|
||||
|
||||
// SQLite doesn't have sequences, so we skip sequence creation
|
||||
|
||||
// SQLite system tables that should be skipped
|
||||
const sqliteSystemTables = [
|
||||
'sqlite_sequence',
|
||||
'sqlite_stat1',
|
||||
'sqlite_stat2',
|
||||
'sqlite_stat3',
|
||||
'sqlite_stat4',
|
||||
'sqlite_master',
|
||||
];
|
||||
|
||||
// Generate table creation SQL
|
||||
sqlScript += tables
|
||||
.map((table: DBTable) => {
|
||||
// Skip views
|
||||
if (table.isView) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Skip SQLite system tables
|
||||
if (sqliteSystemTables.includes(table.name.toLowerCase())) {
|
||||
return `-- Skipping SQLite system table: "${table.name}"\n`;
|
||||
}
|
||||
|
||||
// SQLite doesn't use schema prefixes, so we use just the table name
|
||||
// Include the schema in a comment if it exists
|
||||
const schemaComment = table.schema
|
||||
? `-- Original schema: ${table.schema}\n`
|
||||
: '';
|
||||
const tableName = `"${table.name}"`;
|
||||
|
||||
// Get primary key fields
|
||||
const primaryKeyFields = table.fields.filter((f) => f.primaryKey);
|
||||
|
||||
// Check if this is a single-column INTEGER PRIMARY KEY (for AUTOINCREMENT)
|
||||
const singleIntegerPrimaryKey =
|
||||
primaryKeyFields.length === 1 &&
|
||||
(primaryKeyFields[0].type.name.toLowerCase() === 'integer' ||
|
||||
primaryKeyFields[0].type.name.toLowerCase() === 'int');
|
||||
|
||||
return `${schemaComment}${
|
||||
table.comments ? `-- ${table.comments}\n` : ''
|
||||
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
|
||||
.map((field: DBField) => {
|
||||
const fieldName = `"${field.name}"`;
|
||||
|
||||
// Handle type name - map to SQLite compatible types
|
||||
const typeName = mapSQLiteType(
|
||||
field.type.name,
|
||||
field.primaryKey
|
||||
);
|
||||
|
||||
// SQLite ignores length specifiers, so we don't add them
|
||||
// We'll keep this simple without size info
|
||||
const typeWithoutSize = typeName;
|
||||
|
||||
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||
|
||||
// Handle autoincrement - only works with INTEGER PRIMARY KEY
|
||||
let autoIncrement = '';
|
||||
if (
|
||||
field.primaryKey &&
|
||||
singleIntegerPrimaryKey &&
|
||||
(field.default?.toLowerCase().includes('identity') ||
|
||||
field.default
|
||||
?.toLowerCase()
|
||||
.includes('autoincrement') ||
|
||||
field.default?.includes('nextval'))
|
||||
) {
|
||||
autoIncrement = ' AUTOINCREMENT';
|
||||
}
|
||||
|
||||
// Only add UNIQUE constraint if the field is not part of the primary key
|
||||
const unique =
|
||||
!field.primaryKey && field.unique ? ' UNIQUE' : '';
|
||||
|
||||
// Handle default value - Special handling for datetime() function
|
||||
let defaultValue = '';
|
||||
if (
|
||||
field.default &&
|
||||
!field.default.toLowerCase().includes('identity') &&
|
||||
!field.default
|
||||
.toLowerCase()
|
||||
.includes('autoincrement') &&
|
||||
!field.default.includes('nextval')
|
||||
) {
|
||||
// Special handling for quoted functions like 'datetime(\'\'now\'\')' - remove extra quotes
|
||||
if (field.default.includes("datetime(''now'')")) {
|
||||
defaultValue = ' DEFAULT CURRENT_TIMESTAMP';
|
||||
} else {
|
||||
defaultValue = ` DEFAULT ${parseSQLiteDefault(field)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Add PRIMARY KEY inline only for single INTEGER primary key
|
||||
const primaryKey =
|
||||
field.primaryKey && singleIntegerPrimaryKey
|
||||
? ' PRIMARY KEY' + autoIncrement
|
||||
: '';
|
||||
|
||||
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithoutSize}${primaryKey}${notNull}${unique}${defaultValue}`;
|
||||
})
|
||||
.join(',\n')}${
|
||||
// Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys
|
||||
primaryKeyFields.length > 0 && !singleIntegerPrimaryKey
|
||||
? `,\n PRIMARY KEY (${primaryKeyFields
|
||||
.map((f) => `"${f.name}"`)
|
||||
.join(', ')})`
|
||||
: ''
|
||||
}\n);\n\n${
|
||||
// Add indexes - SQLite doesn't support indexes in CREATE TABLE
|
||||
table.indexes
|
||||
.map((index) => {
|
||||
// Skip indexes that exactly match the primary key
|
||||
const indexFields = index.fieldIds
|
||||
.map((fieldId) => {
|
||||
const field = table.fields.find(
|
||||
(f) => f.id === fieldId
|
||||
);
|
||||
return field ? field : null;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
// Get the properly quoted field names
|
||||
const indexFieldNames = indexFields
|
||||
.map((field) => (field ? `"${field.name}"` : ''))
|
||||
.filter(Boolean);
|
||||
|
||||
// Skip if this index exactly matches the primary key fields
|
||||
if (
|
||||
primaryKeyFields.length === indexFields.length &&
|
||||
primaryKeyFields.every((pk) =>
|
||||
indexFields.some(
|
||||
(field) => field && field.id === pk.id
|
||||
)
|
||||
)
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Create safe index name
|
||||
const safeIndexName = `${table.name}_${index.name}`
|
||||
.replace(/[^a-zA-Z0-9_]/g, '_')
|
||||
.substring(0, 60);
|
||||
|
||||
return indexFieldNames.length > 0
|
||||
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX IF NOT EXISTS "${safeIndexName}"\nON ${tableName} (${indexFieldNames.join(', ')});\n`
|
||||
: '';
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
}`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings (views)
|
||||
.join('\n');
|
||||
|
||||
// Generate table constraints and triggers for foreign keys
|
||||
// SQLite handles foreign keys differently - we'll add them with CREATE TABLE statements
|
||||
// But we'll also provide individual ALTER TABLE statements as comments for reference
|
||||
|
||||
if (relationships.length > 0) {
|
||||
sqlScript += '\n-- Foreign key constraints\n';
|
||||
sqlScript +=
|
||||
'-- Note: SQLite requires foreign_keys pragma to be enabled:\n';
|
||||
sqlScript += '-- PRAGMA foreign_keys = ON;\n\n';
|
||||
|
||||
relationships.forEach((r: DBRelationship) => {
|
||||
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
|
||||
const targetTable = tables.find((t) => t.id === r.targetTableId);
|
||||
|
||||
if (
|
||||
!sourceTable ||
|
||||
!targetTable ||
|
||||
sourceTable.isView ||
|
||||
targetTable.isView ||
|
||||
sqliteSystemTables.includes(sourceTable.name.toLowerCase()) ||
|
||||
sqliteSystemTables.includes(targetTable.name.toLowerCase())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceField = sourceTable.fields.find(
|
||||
(f) => f.id === r.sourceFieldId
|
||||
);
|
||||
const targetField = targetTable.fields.find(
|
||||
(f) => f.id === r.targetFieldId
|
||||
);
|
||||
|
||||
if (!sourceField || !targetField) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create commented out version of what would be ALTER TABLE statement
|
||||
sqlScript += `-- ALTER TABLE "${sourceTable.name}" ADD CONSTRAINT "fk_${sourceTable.name}_${sourceField.name}" FOREIGN KEY("${sourceField.name}") REFERENCES "${targetTable.name}"("${targetField.name}");\n`;
|
||||
});
|
||||
}
|
||||
|
||||
// Commit transaction
|
||||
sqlScript += '\nCOMMIT;\n';
|
||||
|
||||
return sqlScript;
|
||||
}
|
@@ -5,16 +5,62 @@ import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { DataType } from '../data-types/data-types';
|
||||
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
|
||||
import { exportMSSQL } from './export-per-type/mssql';
|
||||
import { exportPostgreSQL } from './export-per-type/postgresql';
|
||||
import { exportSQLite } from './export-per-type/sqlite';
|
||||
import { exportMySQL } from './export-per-type/mysql';
|
||||
|
||||
export const exportBaseSQL = (diagram: Diagram): string => {
|
||||
// Function to simplify verbose data type names
|
||||
const simplifyDataType = (typeName: string): string => {
|
||||
const typeMap: Record<string, string> = {
|
||||
'character varying': 'varchar',
|
||||
'char varying': 'varchar',
|
||||
integer: 'int',
|
||||
int4: 'int',
|
||||
int8: 'bigint',
|
||||
serial4: 'serial',
|
||||
serial8: 'bigserial',
|
||||
float8: 'double precision',
|
||||
float4: 'real',
|
||||
bool: 'boolean',
|
||||
character: 'char',
|
||||
'timestamp without time zone': 'timestamp',
|
||||
'timestamp with time zone': 'timestamptz',
|
||||
'time without time zone': 'time',
|
||||
'time with time zone': 'timetz',
|
||||
};
|
||||
|
||||
return typeMap[typeName.toLowerCase()] || typeName;
|
||||
};
|
||||
|
||||
export const exportBaseSQL = ({
|
||||
diagram,
|
||||
targetDatabaseType,
|
||||
isDBMLFlow = false,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
targetDatabaseType: DatabaseType;
|
||||
isDBMLFlow?: boolean;
|
||||
}): string => {
|
||||
const { tables, relationships } = diagram;
|
||||
|
||||
if (!tables || tables.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (diagram.databaseType === DatabaseType.SQL_SERVER) {
|
||||
return exportMSSQL(diagram);
|
||||
if (!isDBMLFlow && diagram.databaseType === targetDatabaseType) {
|
||||
switch (diagram.databaseType) {
|
||||
case DatabaseType.SQL_SERVER:
|
||||
return exportMSSQL(diagram);
|
||||
case DatabaseType.POSTGRESQL:
|
||||
return exportPostgreSQL(diagram);
|
||||
case DatabaseType.SQLITE:
|
||||
return exportSQLite(diagram);
|
||||
case DatabaseType.MYSQL:
|
||||
case DatabaseType.MARIADB:
|
||||
return exportMySQL(diagram);
|
||||
default:
|
||||
return exportPostgreSQL(diagram);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out the tables that are views
|
||||
@@ -70,7 +116,13 @@ export const exportBaseSQL = (diagram: Diagram): string => {
|
||||
sqlScript += `CREATE TABLE ${tableName} (\n`;
|
||||
|
||||
table.fields.forEach((field, index) => {
|
||||
let typeName = field.type.name;
|
||||
let typeName = simplifyDataType(field.type.name);
|
||||
|
||||
// Handle ENUM type
|
||||
if (typeName.toLowerCase() === 'enum') {
|
||||
// Map enum to TEXT for broader compatibility, especially with DBML importer
|
||||
typeName = 'text';
|
||||
}
|
||||
|
||||
// Temp fix for 'array' to be text[]
|
||||
if (typeName.toLowerCase() === 'array') {
|
||||
@@ -88,6 +140,7 @@ export const exportBaseSQL = (diagram: Diagram): string => {
|
||||
if (field.characterMaximumLength) {
|
||||
sqlScript += `(${field.characterMaximumLength})`;
|
||||
} else if (field.type.name.toLowerCase().includes('varchar')) {
|
||||
// Keep varchar sizing, but don't apply to TEXT (previously enum)
|
||||
sqlScript += `(500)`;
|
||||
}
|
||||
|
||||
@@ -115,8 +168,22 @@ export const exportBaseSQL = (diagram: Diagram): string => {
|
||||
|
||||
// Remove the type cast part after :: if it exists
|
||||
if (fieldDefault.includes('::')) {
|
||||
const endedWithParentheses = fieldDefault.endsWith(')');
|
||||
fieldDefault = fieldDefault.split('::')[0];
|
||||
|
||||
if (
|
||||
(fieldDefault.startsWith('(') &&
|
||||
!fieldDefault.endsWith(')')) ||
|
||||
endedWithParentheses
|
||||
) {
|
||||
fieldDefault += ')';
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldDefault === `('now')`) {
|
||||
fieldDefault = `now()`;
|
||||
}
|
||||
|
||||
sqlScript += ` DEFAULT ${fieldDefault}`;
|
||||
}
|
||||
|
||||
@@ -230,8 +297,12 @@ export const exportSQL = async (
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
): Promise<string> => {
|
||||
const sqlScript = exportBaseSQL(diagram);
|
||||
if (databaseType === DatabaseType.SQL_SERVER) {
|
||||
const sqlScript = exportBaseSQL({
|
||||
diagram,
|
||||
targetDatabaseType: databaseType,
|
||||
});
|
||||
|
||||
if (databaseType === diagram.databaseType) {
|
||||
return sqlScript;
|
||||
}
|
||||
|
||||
@@ -395,7 +466,7 @@ const generateSQLPrompt = (databaseType: DatabaseType, sqlScript: string) => {
|
||||
- **Sequence Creation**: Use \`CREATE SEQUENCE IF NOT EXISTS\` for sequence creation.
|
||||
- **Table and Index Creation**: Use \`CREATE TABLE IF NOT EXISTS\` and \`CREATE INDEX IF NOT EXISTS\` to avoid errors if the object already exists.
|
||||
- **Serial and Identity Columns**: For auto-increment columns, use \`SERIAL\` or \`GENERATED BY DEFAULT AS IDENTITY\`.
|
||||
- **Conditional Statements**: Utilize PostgreSQL’s support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
|
||||
- **Conditional Statements**: Utilize PostgreSQL's support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
|
||||
`,
|
||||
mysql: `
|
||||
- **Table Creation**: Use \`CREATE TABLE IF NOT EXISTS\` for creating tables. While creating the table structure, ensure that all foreign key columns use the correct data types as determined in the foreign key review.
|
||||
@@ -415,7 +486,7 @@ const generateSQLPrompt = (databaseType: DatabaseType, sqlScript: string) => {
|
||||
sql_server: `
|
||||
- **Sequence Creation**: Use \`CREATE SEQUENCE\` without \`IF NOT EXISTS\`, and employ conditional logic (\`IF NOT EXISTS\`) to check for sequence existence before creation.
|
||||
- **Identity Columns**: Always prefer using the \`IDENTITY\` keyword (e.g., \`INT IDENTITY(1,1)\`) for auto-incrementing primary key columns when possible.
|
||||
- **Conditional Logic**: Use a conditional block like \`IF NOT EXISTS (SELECT * FROM sys.objects WHERE ...)\` since SQL Server doesn’t support \`IF NOT EXISTS\` directly in \`CREATE\` statements.
|
||||
- **Conditional Logic**: Use a conditional block like \`IF NOT EXISTS (SELECT * FROM sys.objects WHERE ...)\` since SQL Server doesn't support \`IF NOT EXISTS\` directly in \`CREATE\` statements.
|
||||
- **Avoid Unsupported Syntax**: Ensure the script does not include unsupported statements like \`CREATE TABLE IF NOT EXISTS\`.
|
||||
|
||||
**Reminder**: Ensure all column names that conflict with reserved keywords or data types (e.g., key, primary, column, table), escape the column name by enclosing it.
|
||||
@@ -449,7 +520,7 @@ const generateSQLPrompt = (databaseType: DatabaseType, sqlScript: string) => {
|
||||
- **Sequence Creation**: Use \`CREATE SEQUENCE IF NOT EXISTS\` for sequence creation.
|
||||
- **Table and Index Creation**: Use \`CREATE TABLE IF NOT EXISTS\` and \`CREATE INDEX IF NOT EXISTS\` to avoid errors if the object already exists.
|
||||
- **Serial and Identity Columns**: For auto-increment columns, use \`SERIAL\` or \`GENERATED BY DEFAULT AS IDENTITY\`.
|
||||
- **Conditional Statements**: Utilize PostgreSQL’s support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
|
||||
- **Conditional Statements**: Utilize PostgreSQL's support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
|
||||
`,
|
||||
};
|
||||
|
||||
|
@@ -1,10 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export interface ColumnInfo {
|
||||
schema: string;
|
||||
table: string;
|
||||
name: string;
|
||||
type: string;
|
||||
ordinal_position: number;
|
||||
nullable: boolean;
|
||||
nullable: boolean | number;
|
||||
character_maximum_length?: string | null; // The maximum length of the column (if applicable), nullable
|
||||
precision?: {
|
||||
precision: number | null; // The precision for numeric types
|
||||
@@ -14,3 +16,23 @@ export interface ColumnInfo {
|
||||
collation?: string | null;
|
||||
comment?: string | null;
|
||||
}
|
||||
|
||||
export const ColumnInfoSchema: z.ZodType<ColumnInfo> = z.object({
|
||||
schema: z.string(),
|
||||
table: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string(),
|
||||
ordinal_position: z.number(),
|
||||
nullable: z.union([z.boolean(), z.number()]),
|
||||
character_maximum_length: z.string().nullable().optional(),
|
||||
precision: z
|
||||
.object({
|
||||
precision: z.number().nullable(),
|
||||
scale: z.number().nullable(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
default: z.string().nullable().optional(),
|
||||
collation: z.string().nullable().optional(),
|
||||
comment: z.string().nullable().optional(),
|
||||
});
|
||||
|
@@ -1,9 +1,11 @@
|
||||
import type { ForeignKeyInfo } from './foreign-key-info';
|
||||
import type { PrimaryKeyInfo } from './primary-key-info';
|
||||
import type { ColumnInfo } from './column-info';
|
||||
import type { IndexInfo } from './index-info';
|
||||
import type { TableInfo } from './table-info';
|
||||
import type { ViewInfo } from './view-info';
|
||||
import { z } from 'zod';
|
||||
import { ForeignKeyInfoSchema, type ForeignKeyInfo } from './foreign-key-info';
|
||||
import { PrimaryKeyInfoSchema, type PrimaryKeyInfo } from './primary-key-info';
|
||||
import { ColumnInfoSchema, type ColumnInfo } from './column-info';
|
||||
import { IndexInfoSchema, type IndexInfo } from './index-info';
|
||||
import { TableInfoSchema, type TableInfo } from './table-info';
|
||||
import { ViewInfoSchema, type ViewInfo } from './view-info';
|
||||
|
||||
export interface DatabaseMetadata {
|
||||
fk_info: ForeignKeyInfo[];
|
||||
pk_info: PrimaryKeyInfo[];
|
||||
@@ -15,16 +17,26 @@ export interface DatabaseMetadata {
|
||||
version: string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isDatabaseMetadata = (obj: any): boolean => {
|
||||
return (
|
||||
Array.isArray(obj.fk_info) &&
|
||||
Array.isArray(obj.pk_info) &&
|
||||
Array.isArray(obj.columns) &&
|
||||
Array.isArray(obj.indexes) &&
|
||||
Array.isArray(obj.tables) &&
|
||||
Array.isArray(obj.views)
|
||||
);
|
||||
export const DatabaseMetadataSchema: z.ZodType<DatabaseMetadata> = z.object({
|
||||
fk_info: z.array(ForeignKeyInfoSchema),
|
||||
pk_info: z.array(PrimaryKeyInfoSchema),
|
||||
columns: z.array(ColumnInfoSchema),
|
||||
indexes: z.array(IndexInfoSchema),
|
||||
tables: z.array(TableInfoSchema),
|
||||
views: z.array(ViewInfoSchema),
|
||||
database_name: z.string(),
|
||||
version: z.string(),
|
||||
});
|
||||
|
||||
export const isDatabaseMetadata = (obj: unknown): boolean => {
|
||||
const parsedObject = DatabaseMetadataSchema.safeParse(obj);
|
||||
|
||||
if (!parsedObject.success) {
|
||||
console.error(parsedObject.error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const loadDatabaseMetadata = (jsonString: string): DatabaseMetadata => {
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export interface ForeignKeyInfo {
|
||||
schema: string;
|
||||
table: string;
|
||||
@@ -8,3 +10,14 @@ export interface ForeignKeyInfo {
|
||||
reference_column: string;
|
||||
fk_def: string;
|
||||
}
|
||||
|
||||
export const ForeignKeyInfoSchema: z.ZodType<ForeignKeyInfo> = z.object({
|
||||
schema: z.string(),
|
||||
table: z.string(),
|
||||
column: z.string(),
|
||||
foreign_key_name: z.string(),
|
||||
reference_schema: z.string().optional(),
|
||||
reference_table: z.string(),
|
||||
reference_column: z.string(),
|
||||
fk_def: z.string(),
|
||||
});
|
||||
|