Compare commits
	
		
			138 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 4a52bf02e6 | ||
|  | 08b627cb8c | ||
|  | 73f542adad | ||
|  | 0d11b0c55a | ||
|  | 5b9d2bd1e3 | ||
|  | cf1e141837 | ||
|  | 3894a22174 | ||
|  | cad155e655 | ||
|  | 4477b1ca1f | ||
|  | cd443466c7 | ||
|  | 18012ddab1 | ||
|  | beb015194f | ||
|  | c3904d9fdd | ||
|  | aee5779983 | ||
|  | 765a1c4354 | ||
|  | 86840a8822 | ||
|  | 487fb2d5c1 | ||
|  | 54d5e96a6d | ||
|  | 481ad3c844 | ||
|  | 0ce85cf76b | ||
|  | 5849e4586c | ||
|  | 34c0a7163f | ||
|  | 89e3ceab00 | ||
|  | 5a5e64abef | ||
|  | 2368e0d263 | ||
|  | 547149da44 | ||
|  | a1144bbf76 | ||
|  | 6b8d637b75 | ||
|  | fd47eb7f4b | ||
|  | 7db86dcf8c | ||
|  | e75323c16e | ||
|  | 97d01d7201 | ||
|  | 90b42a4bb7 | ||
|  | fbf2fe919c | ||
|  | d3ddf7c51e | ||
|  | 5759241573 | ||
|  | 3747abbc3b | ||
|  | 226e6cf1ce | ||
|  | 1778abb683 | ||
|  | 90a20dd1b0 | ||
|  | 21c9129e14 | ||
|  | 19d2d0bddd | ||
|  | 83c43332d4 | ||
|  | 3a1b8d1db1 | ||
|  | 46426e27b4 | ||
|  | 9402822fa3 | ||
|  | 651fe361fc | ||
|  | aee1713aec | ||
|  | ecfa14829b | ||
|  | 92e3ec785c | ||
|  | 8102f19f79 | ||
|  | 840a00ebcd | ||
|  | 181f96d250 | ||
|  | ce2389f135 | ||
|  | f15dc77f33 | ||
|  | caa81c24a6 | ||
|  | e3cb62788c | ||
|  | fc46cbb893 | ||
|  | d94a71e9e1 | ||
|  | cf81253535 | ||
|  | 25c4b42538 | ||
|  | f7a6e0cb5e | ||
|  | 85275e5dd6 | ||
|  | 4e5b467ce5 | ||
|  | 874aa5ab75 | ||
|  | 0940d72d5d | ||
|  | 0d1739d70f | ||
|  | 60fe0843ac | ||
|  | 794f226209 | ||
|  | 2fbf3476b8 | ||
|  | 897ac60a82 | ||
|  | 18f228ca1d | ||
|  | 14de30b7aa | ||
|  | 3faa39e787 | ||
|  | 63b5ba0bb9 | ||
|  | 44eac7daff | ||
|  | 502472b083 | ||
|  | 52d2ea596c | ||
|  | bd67ccfbcf | ||
|  | 62beb68fa1 | ||
|  | 09b1275475 | ||
|  | 5dd7fe75d1 | ||
|  | 2939320a15 | ||
|  | a643852837 | ||
|  | 467ff697c9 | ||
|  | d6919f3033 | ||
|  | 56382a9fdc | ||
|  | e06eb2a48e | ||
|  | 543b716c77 | ||
|  | b55d631146 | ||
|  | ef118929ad | ||
|  | 68f48190c9 | ||
|  | bba265ad43 | ||
|  | cbc4e85a14 | ||
|  | 26a0a5b550 | ||
|  | b935b7f251 | ||
|  | a1c0cf102a | ||
|  | ab89bad6d5 | ||
|  | deb218423f | ||
|  | 48342471ac | ||
|  | 47bb87a88f | ||
|  | a96c2e1078 | ||
|  | 26d95eed25 | ||
|  | be65328f24 | ||
|  | 85fd14fa02 | ||
|  | 9c485b3b01 | ||
|  | e993f1549c | ||
|  | 0db67ea42a | ||
|  | b9e621bd68 | ||
|  | 93d59f8887 | ||
|  | 190e4f4ffa | ||
|  | dc404c9d7e | ||
|  | dd4324d64f | ||
|  | 1878083056 | ||
|  | 7b6271962a | ||
|  | 2edc8dfde8 | ||
|  | 004d530880 | ||
|  | fd2cc9fcfc | ||
|  | 4c93326bb6 | ||
|  | ef3d7a8b67 | ||
|  | 3b3be086b1 | ||
|  | b424518212 | ||
|  | 99a8201398 | ||
|  | eb9b41e4f6 | ||
|  | fef6d3f499 | ||
|  | 14f11c27a7 | ||
|  | 2118bce0f0 | ||
|  | 88be6c1fd4 | ||
|  | 0dcc9b9568 | ||
|  | ff3269ec05 | ||
|  | 659dc2e3e7 | ||
|  | c36cd33180 | ||
|  | 58231c9139 | ||
|  | 1643e7bdeb | ||
|  | 42d4cbac8c | ||
|  | 7452ca6965 | ||
|  | 27aede7794 | ||
|  | e9e2736cb2 | 
							
								
								
									
										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 }} |           registry: ${{ env.REGISTRY }} | ||||||
|           username: ${{ github.actor }} |           username: ${{ github.actor }} | ||||||
|           password: ${{ secrets.GITHUB_TOKEN }} |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|      |  | ||||||
|       - name: Install dependencies |       - name: Install dependencies | ||||||
|         run: npm ci |         run: npm ci | ||||||
|  |  | ||||||
| @@ -42,6 +42,12 @@ jobs: | |||||||
|       - name: Build project |       - name: Build project | ||||||
|         run: npm run build |         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 |       - name: Extract metadata (tags, labels) for Docker | ||||||
|         id: meta |         id: meta | ||||||
|         uses: docker/metadata-action@v4 |         uses: docker/metadata-action@v4 | ||||||
| @@ -50,10 +56,11 @@ jobs: | |||||||
|           tags: | |           tags: | | ||||||
|             type=semver,pattern={{version}} |             type=semver,pattern={{version}} | ||||||
|  |  | ||||||
|       - name: Build and push Docker image |       - name: Build and push multi-arch Docker image | ||||||
|         uses: docker/build-push-action@v6 |         uses: docker/build-push-action@v6 | ||||||
|         with: |         with: | ||||||
|           context: . |           context: . | ||||||
|           push: true |           push: true | ||||||
|  |           platforms: linux/amd64,linux/arm64 | ||||||
|           tags: ${{ steps.meta.outputs.tags }} |           tags: ${{ steps.meta.outputs.tags }} | ||||||
|           labels: ${{ steps.meta.outputs.labels }} |           labels: ${{ steps.meta.outputs.labels }} | ||||||
|   | |||||||
							
								
								
									
										194
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						| @@ -1,5 +1,199 @@ | |||||||
| # Changelog | # Changelog | ||||||
|  |  | ||||||
|  | ## [1.13.0](https://github.com/chartdb/chartdb/compare/v1.12.0...v1.13.0) (2025-05-28) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Features | ||||||
|  |  | ||||||
|  | * **custom-types:** add enums and composite types for Postgres ([#714](https://github.com/chartdb/chartdb/issues/714)) ([c3904d9](https://github.com/chartdb/chartdb/commit/c3904d9fdd63ef5b76a44e73582d592f2c418687)) | ||||||
|  | * **export-sql:** add custom types to export sql script ([#720](https://github.com/chartdb/chartdb/issues/720)) ([cad155e](https://github.com/chartdb/chartdb/commit/cad155e6550f171b8faecbfdff27032798ecea43)) | ||||||
|  | * **oracle:** support oracle in ChartDB ([#709](https://github.com/chartdb/chartdb/issues/709)) ([765a1c4](https://github.com/chartdb/chartdb/commit/765a1c43547a29bd3428c942c7afb56f63aaf046)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Bug Fixes | ||||||
|  |  | ||||||
|  | * **canvas:** prevent canvas blink and lag on field edit ([#723](https://github.com/chartdb/chartdb/issues/723)) ([cd44346](https://github.com/chartdb/chartdb/commit/cd443466c7952f1cdc3739645c12130b9231e3a1)) | ||||||
|  | * **canvas:** prevent canvas blink and lag on primary field edit ([#725](https://github.com/chartdb/chartdb/issues/725)) ([4477b1c](https://github.com/chartdb/chartdb/commit/4477b1ca1fe6b282b604739a23e31181acd4d7bc)) | ||||||
|  | * **custom_types:** fix custom types on storage provider ([#721](https://github.com/chartdb/chartdb/issues/721)) ([beb0151](https://github.com/chartdb/chartdb/commit/beb015194f917c0ba644458410162d2b7599918c)) | ||||||
|  | * **custom_types:** fix custom types on storage provider ([#722](https://github.com/chartdb/chartdb/issues/722)) ([18012dd](https://github.com/chartdb/chartdb/commit/18012ddab1718bcce3432aea626adf6fc9be25d9)) | ||||||
|  | * **custom-types:** fetch directly via the smart-query the custom types ([#729](https://github.com/chartdb/chartdb/issues/729)) ([cf1e141](https://github.com/chartdb/chartdb/commit/cf1e141837eda77d717ad87489ce9946b688e226)) | ||||||
|  | * **dbml-editor:** export comments with schema if existsed ([#728](https://github.com/chartdb/chartdb/issues/728)) ([73f542a](https://github.com/chartdb/chartdb/commit/73f542adad2d66a1e84fc656a0c34d9b1f39f33c)) | ||||||
|  | * **dbml-editor:** fix export dbml - to show enums ([#724](https://github.com/chartdb/chartdb/issues/724)) ([3894a22](https://github.com/chartdb/chartdb/commit/3894a221745d32c13160bedcb1bcf53d89897698)) | ||||||
|  | * **import-database:** remove the default fetch from import database ([#718](https://github.com/chartdb/chartdb/issues/718)) ([0d11b0c](https://github.com/chartdb/chartdb/commit/0d11b0c55a94a12a764785cfdcf2ba10437241d6)) | ||||||
|  | * **menu:** add oracle to import menu ([#713](https://github.com/chartdb/chartdb/issues/713)) ([aee5779](https://github.com/chartdb/chartdb/commit/aee577998342eb4a2b05b3e03181992a435712d8)) | ||||||
|  | * **relationship:** fix creating of relationships ([#732](https://github.com/chartdb/chartdb/issues/732)) ([08b627c](https://github.com/chartdb/chartdb/commit/08b627cb8ca8fdf08d8ed2ff7e89104887deffb7)) | ||||||
|  |  | ||||||
|  | ## [1.12.0](https://github.com/chartdb/chartdb/compare/v1.11.0...v1.12.0) (2025-05-20) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Features | ||||||
|  |  | ||||||
|  | * **areas:** implement area to enable logical diagram arrangement ([#661](https://github.com/chartdb/chartdb/issues/661)) ([92e3ec7](https://github.com/chartdb/chartdb/commit/92e3ec785c91f7f19881c6d9d0692257af4651bc)) | ||||||
|  | * **examples:** update examples to have areas ([#677](https://github.com/chartdb/chartdb/issues/677)) ([21c9129](https://github.com/chartdb/chartdb/commit/21c9129e14670c744950cd43a5cbdd4b7d47c639)) | ||||||
|  | * **image-export:** add transparent and pattern export image toggles ([#671](https://github.com/chartdb/chartdb/issues/671)) ([6b8d637](https://github.com/chartdb/chartdb/commit/6b8d637b757b94630ecd7521b4a2c99634afae69)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Bug Fixes | ||||||
|  |  | ||||||
|  | * add sorting based on how common the datatype on side-panel ([#651](https://github.com/chartdb/chartdb/issues/651)) ([3a1b8d1](https://github.com/chartdb/chartdb/commit/3a1b8d1db13d8dd7cb6cbe5ef8c5a60faccfeae5)) | ||||||
|  | * **canvas:** disable edit area name on read only ([#666](https://github.com/chartdb/chartdb/issues/666)) ([9402822](https://github.com/chartdb/chartdb/commit/9402822fa31f8cd94fe7971277839ee5425e29bf)) | ||||||
|  | * **canvas:** read only mode ([#665](https://github.com/chartdb/chartdb/issues/665)) ([651fe36](https://github.com/chartdb/chartdb/commit/651fe361fce61fe0577d2593f268131e9ca359d0)) | ||||||
|  | * **clone:** add areas to clone diagram ([#664](https://github.com/chartdb/chartdb/issues/664)) ([aee1713](https://github.com/chartdb/chartdb/commit/aee1713aecdd5e54228a16cbc3c4fc184661c56b)) | ||||||
|  | * **dbml-editor:** add inline refs mode + fix issues with DBML syntax ([#687](https://github.com/chartdb/chartdb/issues/687)) ([fbf2fe9](https://github.com/chartdb/chartdb/commit/fbf2fe919c2168c715f8231c0246753b19635f14)) | ||||||
|  | * **dbml-editor:** remove invalid fields before showing DBML + warning ([#683](https://github.com/chartdb/chartdb/issues/683)) ([5759241](https://github.com/chartdb/chartdb/commit/5759241573db204183c92599588d59f4aadaeafb)) | ||||||
|  | * **ddl-import:** fix datatypes when importing via ddl ([#696](https://github.com/chartdb/chartdb/issues/696)) ([a1144bb](https://github.com/chartdb/chartdb/commit/a1144bbf761a0daedd546b5d9b92300be59e0157)) | ||||||
|  | * **ddl:** inline fks ddl script ([#701](https://github.com/chartdb/chartdb/issues/701)) ([5849e45](https://github.com/chartdb/chartdb/commit/5849e4586c7c2a7cd86bd064df8916b130fc6234)) | ||||||
|  | * **dependencies:** hide icon when diagram has no dependencies ([#684](https://github.com/chartdb/chartdb/issues/684)) ([547149d](https://github.com/chartdb/chartdb/commit/547149da44db6d3d1e36d619d475fe52ff83a472)) | ||||||
|  | * **examples:** add loader ([#678](https://github.com/chartdb/chartdb/issues/678)) ([90a20dd](https://github.com/chartdb/chartdb/commit/90a20dd1b0277c4aee848fae5ed7a8347c5ba77d)) | ||||||
|  | * **examples:** fix clone examples ([#679](https://github.com/chartdb/chartdb/issues/679)) ([1778abb](https://github.com/chartdb/chartdb/commit/1778abb683d575af244edcd9a11f8d03f903f719)) | ||||||
|  | * **expanded-table:** persist expanded state across renders ([#707](https://github.com/chartdb/chartdb/issues/707)) ([54d5e96](https://github.com/chartdb/chartdb/commit/54d5e96a6db1e3abd52229a89ac503ff31885386)) | ||||||
|  | * **export image:** Fix usage of advanced options accordion ([#703](https://github.com/chartdb/chartdb/issues/703)) ([0ce85cf](https://github.com/chartdb/chartdb/commit/0ce85cf76b733f441f661608278c0db3122c5074)) | ||||||
|  | * **import-database:** auto detect when user try to import ddl script ([#698](https://github.com/chartdb/chartdb/issues/698)) ([5a5e64a](https://github.com/chartdb/chartdb/commit/5a5e64abef510cff28b3d8972520d0b9df29b024)) | ||||||
|  | * **import-database:** remove view_definition when importing via query ([#702](https://github.com/chartdb/chartdb/issues/702)) ([481ad3c](https://github.com/chartdb/chartdb/commit/481ad3c8449f469bf2b4418e4cdcc5b5608dfd36)) | ||||||
|  | * **import-json:** for broken json imports ([#697](https://github.com/chartdb/chartdb/issues/697)) ([2368e0d](https://github.com/chartdb/chartdb/commit/2368e0d2639021c4a11a8e5131d6af44fb6a47db)) | ||||||
|  | * **import-json:** simplify import script for fixing invalid JSON ([#681](https://github.com/chartdb/chartdb/issues/681)) ([226e6cf](https://github.com/chartdb/chartdb/commit/226e6cf1ce4d2edcfbee6a4de7ab0bc0cfeb17fe)) | ||||||
|  | * **import:** dbml and query - senetize before import ([#699](https://github.com/chartdb/chartdb/issues/699)) ([34c0a71](https://github.com/chartdb/chartdb/commit/34c0a7163f47bde7ddfaa8f044341e3c971b7e03)) | ||||||
|  | * **navbar:** open diagram directly from diagram icon ([#694](https://github.com/chartdb/chartdb/issues/694)) ([7db86dc](https://github.com/chartdb/chartdb/commit/7db86dcf8c97d34b056e4b5b85a0dda0438322ea)) | ||||||
|  | * **performance:** Only render visible ([#672](https://github.com/chartdb/chartdb/issues/672)) ([83c4333](https://github.com/chartdb/chartdb/commit/83c43332d497e9fc148a18b9cb4d9ecc85e44183)) | ||||||
|  | * **performance:** update field only when changed ([#685](https://github.com/chartdb/chartdb/issues/685)) ([d3ddf7c](https://github.com/chartdb/chartdb/commit/d3ddf7c51eaa4b9cddb961defd52d423f39f281d)) | ||||||
|  | * **postgres:** fix import of postgres fks ([#700](https://github.com/chartdb/chartdb/issues/700)) ([89e3cea](https://github.com/chartdb/chartdb/commit/89e3ceab00defaabc079e165fc90e92ca00722cf)) | ||||||
|  | * **schema:** add areas to diagram schema ([#663](https://github.com/chartdb/chartdb/issues/663)) ([ecfa148](https://github.com/chartdb/chartdb/commit/ecfa14829bcb1b813c7b154b4bd59f24e3032d8f)) | ||||||
|  | * **sql-script:** change ddl to be sql-script ([#710](https://github.com/chartdb/chartdb/issues/710)) ([487fb2d](https://github.com/chartdb/chartdb/commit/487fb2d5c17b70ac54aa17af9a2ac9aded6b40ba)) | ||||||
|  | * **table:** enhance field focus behavior to include table hover state ([#676](https://github.com/chartdb/chartdb/issues/676)) ([19d2d0b](https://github.com/chartdb/chartdb/commit/19d2d0bddd3a464995b79e97e6caf6e652836081)) | ||||||
|  | * **translations:** Add some translations for ru-RU language ([#690](https://github.com/chartdb/chartdb/issues/690)) ([97d01d7](https://github.com/chartdb/chartdb/commit/97d01d72014e473c42348c9ebcbe7a0b973d31aa)) | ||||||
|  |  | ||||||
|  | ## [1.11.0](https://github.com/chartdb/chartdb/compare/v1.10.0...v1.11.0) (2025-04-17) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Features | ||||||
|  |  | ||||||
|  | * add sidebar footer help buttons ([#650](https://github.com/chartdb/chartdb/issues/650)) ([fc46cbb](https://github.com/chartdb/chartdb/commit/fc46cbb8933761c7bac3604664f7de812f6f5b6b)) | ||||||
|  | * **import-sql:** import postgresql via SQL (DDL script) ([#639](https://github.com/chartdb/chartdb/issues/639)) ([f7a6e0c](https://github.com/chartdb/chartdb/commit/f7a6e0cb5e4921dd9540739f9da269858e7ca7be)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Bug Fixes | ||||||
|  |  | ||||||
|  | * **import:** display query result formatted ([#644](https://github.com/chartdb/chartdb/issues/644)) ([caa81c2](https://github.com/chartdb/chartdb/commit/caa81c24a6535bc87129c38622aac5a62a6d479d)) | ||||||
|  | * **import:** strict parse of database metadata ([#635](https://github.com/chartdb/chartdb/issues/635)) ([0940d72](https://github.com/chartdb/chartdb/commit/0940d72d5d3726650213257639f24ba47e729854)) | ||||||
|  | * **mobile:** fix create diagram modal on mobile ([#646](https://github.com/chartdb/chartdb/issues/646)) ([25c4b42](https://github.com/chartdb/chartdb/commit/25c4b4253849575d7a781ed197281e2a35e7184a)) | ||||||
|  | * **mysql-ddl:** update the script to import - for create fks ([#642](https://github.com/chartdb/chartdb/issues/642)) ([cf81253](https://github.com/chartdb/chartdb/commit/cf81253535ca5a3b8a65add78287c1bdb283a1c7)) | ||||||
|  | * **performance:** Import deps dynamically ([#652](https://github.com/chartdb/chartdb/issues/652)) ([e3cb627](https://github.com/chartdb/chartdb/commit/e3cb62788c13f149e35e1a5020191bd43d14b52f)) | ||||||
|  | * remove unused links from help menu ([#623](https://github.com/chartdb/chartdb/issues/623)) ([85275e5](https://github.com/chartdb/chartdb/commit/85275e5dd6e7845f06f682eeceda7932fc87e875)) | ||||||
|  | * **sidebar:** turn sidebar to responsive for mobile ([#658](https://github.com/chartdb/chartdb/issues/658)) ([ce2389f](https://github.com/chartdb/chartdb/commit/ce2389f135d399d82c9848335d31174bac8a3791)) | ||||||
|  |  | ||||||
|  | ## [1.10.0](https://github.com/chartdb/chartdb/compare/v1.9.0...v1.10.0) (2025-03-25) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Features | ||||||
|  |  | ||||||
|  | * **cloudflare-d1:** add support to cloudflare-d1 + wrangler cli ([#632](https://github.com/chartdb/chartdb/issues/632)) ([794f226](https://github.com/chartdb/chartdb/commit/794f2262092fbe36e27e92220221ed98cb51ae37)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Bug Fixes | ||||||
|  |  | ||||||
|  | * **dbml-editor:** dealing with dbml editor for non-generic db-type ([#624](https://github.com/chartdb/chartdb/issues/624)) ([14de30b](https://github.com/chartdb/chartdb/commit/14de30b7aaa0ccaca8372f0213b692266d53f0de)) | ||||||
|  | * **export-sql:** move from AI sql-export for MySQL&MariaDB to deterministic script ([#628](https://github.com/chartdb/chartdb/issues/628)) ([2fbf347](https://github.com/chartdb/chartdb/commit/2fbf3476b87f1177af17de8242a74d195dae5f35)) | ||||||
|  | * **export-sql:** move from AI sql-export for postgres to deterministic script ([#626](https://github.com/chartdb/chartdb/issues/626)) ([18f228c](https://github.com/chartdb/chartdb/commit/18f228ca1d5a6c6056cb7c3bfc24d04ec470edf1)) | ||||||
|  | * **export-sql:** move from AI sql-export for sqlite to deterministic script ([#627](https://github.com/chartdb/chartdb/issues/627)) ([897ac60](https://github.com/chartdb/chartdb/commit/897ac60a829a00e9453d670cceeb2282e9e93f1c)) | ||||||
|  | * **sidebar:** add sidebar for diagram objects ([#618](https://github.com/chartdb/chartdb/issues/618)) ([63b5ba0](https://github.com/chartdb/chartdb/commit/63b5ba0bb9934c4e5c5d0d1b6f995afbbd3acf36)) | ||||||
|  | * **sidebar:** opens sidepanel in case its closed and click on sidebar ([#620](https://github.com/chartdb/chartdb/issues/620)) ([3faa39e](https://github.com/chartdb/chartdb/commit/3faa39e7875d836dfe526d94a10f8aed070ac1c1)) | ||||||
|  |  | ||||||
|  | ## [1.9.0](https://github.com/chartdb/chartdb/compare/v1.8.1...v1.9.0) (2025-03-13) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Features | ||||||
|  |  | ||||||
|  | * **canvas:** highlight the Show-All button when No-Tables are visible in the canvas ([#612](https://github.com/chartdb/chartdb/issues/612)) ([62beb68](https://github.com/chartdb/chartdb/commit/62beb68fa1ec22ccd4fe5e59a8ceb9d3e8f6d374)) | ||||||
|  | * **chart max length:** add support for edit char max length ([#613](https://github.com/chartdb/chartdb/issues/613)) ([09b1275](https://github.com/chartdb/chartdb/commit/09b12754757b9625ca287d91a92cf0d83c9e2b89)) | ||||||
|  | * **chart max length:** enable edit length from data type select box ([#616](https://github.com/chartdb/chartdb/issues/616)) ([bd67ccf](https://github.com/chartdb/chartdb/commit/bd67ccfbcf66b919453ca6c0bfd71e16772b3d8e)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Bug Fixes | ||||||
|  |  | ||||||
|  | * **cardinality:** set true as default ([#583](https://github.com/chartdb/chartdb/issues/583)) ([2939320](https://github.com/chartdb/chartdb/commit/2939320a15a9ccd9eccfe46c26e04ca1edca2420)) | ||||||
|  | * **performance:** Optimize performance of field comments editing ([#610](https://github.com/chartdb/chartdb/issues/610)) ([5dd7fe7](https://github.com/chartdb/chartdb/commit/5dd7fe75d1b0378ba406c75183c5e2356730c3b4)) | ||||||
|  | * remove Buckle dialog ([#617](https://github.com/chartdb/chartdb/issues/617)) ([502472b](https://github.com/chartdb/chartdb/commit/502472b08342be425e66e2b6c94e5fe37ba14aa9)) | ||||||
|  | * **shorcuts:** add shortcut to toggle the theme ([#602](https://github.com/chartdb/chartdb/issues/602)) ([a643852](https://github.com/chartdb/chartdb/commit/a6438528375ab54d3ec7d80ac6b6ddd65ea8cf1e)) | ||||||
|  |  | ||||||
|  | ## [1.8.1](https://github.com/chartdb/chartdb/compare/v1.8.0...v1.8.1) (2025-03-02) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Bug Fixes | ||||||
|  |  | ||||||
|  | * **add-docs:** add link to ChartDB documentation ([#597](https://github.com/chartdb/chartdb/issues/597)) ([b55d631](https://github.com/chartdb/chartdb/commit/b55d631146ff3a1f7d63c800d44b5d3d3a223c76)) | ||||||
|  | * components config ([#591](https://github.com/chartdb/chartdb/issues/591)) ([cbc4e85](https://github.com/chartdb/chartdb/commit/cbc4e85a14e24a43f9ff470518f8fe2845046bdb)) | ||||||
|  | * **docker config:** Environment Variable Handling and Configuration Logic ([#605](https://github.com/chartdb/chartdb/issues/605)) ([d6919f3](https://github.com/chartdb/chartdb/commit/d6919f30336cc846fe6e6505b5a5278aa14dcce6)) | ||||||
|  | * **empty-state:** show diff buttons on import-dbml when triggered by empty ([#574](https://github.com/chartdb/chartdb/issues/574)) ([4834247](https://github.com/chartdb/chartdb/commit/48342471ac231922f2ca4455b74a9879127a54f1)) | ||||||
|  | * **i18n:** add [FR] translation ([#579](https://github.com/chartdb/chartdb/issues/579)) ([ab89bad](https://github.com/chartdb/chartdb/commit/ab89bad6d544ba4c339a3360eeec7d29e5579511)) | ||||||
|  | * **img-export:** add ChartDB watermark to exported image ([#588](https://github.com/chartdb/chartdb/issues/588)) ([b935b7f](https://github.com/chartdb/chartdb/commit/b935b7f25111d5f72b7f8d7c552a4ea5974f791e)) | ||||||
|  | * **import-mssql:** fix import/export scripts to handle data correctly ([#598](https://github.com/chartdb/chartdb/issues/598)) ([e06eb2a](https://github.com/chartdb/chartdb/commit/e06eb2a48e6bd3bcf352f4bcf128214c7da4c1b1)) | ||||||
|  | * **menu-backup:** update export to be backup ([#590](https://github.com/chartdb/chartdb/issues/590)) ([26a0a5b](https://github.com/chartdb/chartdb/commit/26a0a5b550ef5e47e89b00d0232dc98936f63f23)) | ||||||
|  | * open create new diagram when there is no diagram ([#594](https://github.com/chartdb/chartdb/issues/594)) ([ef11892](https://github.com/chartdb/chartdb/commit/ef118929ad5d5cbfae0290061bd8ea30bd262496)) | ||||||
|  | * **open diagram:** in case there is no diagram, opens the dialog ([#593](https://github.com/chartdb/chartdb/issues/593)) ([68f4819](https://github.com/chartdb/chartdb/commit/68f48190c93f155398cca15dd7af2a025de2d45f)) | ||||||
|  | * **side-panel:** simplify how to add field and index ([#573](https://github.com/chartdb/chartdb/issues/573)) ([a1c0cf1](https://github.com/chartdb/chartdb/commit/a1c0cf102add4fb235e913e75078139b3961341b)) | ||||||
|  | * **sql_server_export:** use sql server export ([#600](https://github.com/chartdb/chartdb/issues/600)) ([56382a9](https://github.com/chartdb/chartdb/commit/56382a9fdc5e3044f8811873dd8a79f590771896)) | ||||||
|  | * **sqlite-import:** import nuallable columns correctly + add json type ([#571](https://github.com/chartdb/chartdb/issues/571)) ([deb2184](https://github.com/chartdb/chartdb/commit/deb218423f77f0c0945a93005696456f62b00ce3)) | ||||||
|  |  | ||||||
|  | ## [1.8.0](https://github.com/chartdb/chartdb/compare/v1.7.0...v1.8.0) (2025-02-13) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Features | ||||||
|  |  | ||||||
|  | * **dbml-import:** add error highlighting for dbml imports ([#556](https://github.com/chartdb/chartdb/issues/556)) ([190e4f4](https://github.com/chartdb/chartdb/commit/190e4f4ffa834fa621f264dc608ca3f3b393a331)) | ||||||
|  | * **docker image:** add support for custom inference servers ([#543](https://github.com/chartdb/chartdb/issues/543)) ([1878083](https://github.com/chartdb/chartdb/commit/1878083056ea4db7a05cdeeb38a4f7b9f5f95bd1)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Bug Fixes | ||||||
|  |  | ||||||
|  | * **canvas:** add right-click option to create relationships ([#568](https://github.com/chartdb/chartdb/issues/568)) ([e993f15](https://github.com/chartdb/chartdb/commit/e993f1549c4c86bb9e7e36062db803ba6613b3b3)) | ||||||
|  | * **canvas:** locate table from canvas ([#560](https://github.com/chartdb/chartdb/issues/560)) ([dc404c9](https://github.com/chartdb/chartdb/commit/dc404c9d7ee272c93aac69646bac859829a5234e)) | ||||||
|  | * **docker:** add option to hide popups ([#580](https://github.com/chartdb/chartdb/issues/580)) ([a96c2e1](https://github.com/chartdb/chartdb/commit/a96c2e107838d2dc13b586923fd9dbe06598cdd8)) | ||||||
|  | * **export-sql:** show create script for only filtered schemas ([#570](https://github.com/chartdb/chartdb/issues/570)) ([85fd14f](https://github.com/chartdb/chartdb/commit/85fd14fa02bb2879c36bba53369dbf2e7fa578d4)) | ||||||
|  | * **i18n:** fix Ukrainian ([#554](https://github.com/chartdb/chartdb/issues/554)) ([7b62719](https://github.com/chartdb/chartdb/commit/7b6271962a99bfe5ffbd0176e714c76368ef5c41)) | ||||||
|  | * **import dbml:** add import for indexes ([#566](https://github.com/chartdb/chartdb/issues/566)) ([0db67ea](https://github.com/chartdb/chartdb/commit/0db67ea42a5f9585ca1d246db7a7ff0239bec0ba)) | ||||||
|  | * **import-query:** improve the cleanup for messy json input ([#562](https://github.com/chartdb/chartdb/issues/562)) ([93d59f8](https://github.com/chartdb/chartdb/commit/93d59f8887765098d040a3184aaee32112f67267)) | ||||||
|  | * **index unique:** extract unique toggle for faster editing ([#559](https://github.com/chartdb/chartdb/issues/559)) ([dd4324d](https://github.com/chartdb/chartdb/commit/dd4324d64f7638ada5c022a2ab38bd8e6986af25)) | ||||||
|  | * **mssql-import:** improve script readability by adding edition comment ([#572](https://github.com/chartdb/chartdb/issues/572)) ([be65328](https://github.com/chartdb/chartdb/commit/be65328f24b0361638b9e2edb39eaa9906e77f67)) | ||||||
|  | * **realtionships section:** add the schema to source/target tables ([#561](https://github.com/chartdb/chartdb/issues/561)) ([b9e621b](https://github.com/chartdb/chartdb/commit/b9e621bd680730a0ffbf1054d735bfa418711cae)) | ||||||
|  | * **sqlserver-import:** open ssms guide when max chars ([#565](https://github.com/chartdb/chartdb/issues/565)) ([9c485b3](https://github.com/chartdb/chartdb/commit/9c485b3b01a131bf551c7e95916b0c416f6aa0b5)) | ||||||
|  | * **table actions:** fix size of table actions ([#578](https://github.com/chartdb/chartdb/issues/578)) ([26d95ee](https://github.com/chartdb/chartdb/commit/26d95eed25d86452d9168a9d93a301ba50d934e3)) | ||||||
|  |  | ||||||
|  | ## [1.7.0](https://github.com/chartdb/chartdb/compare/v1.6.1...v1.7.0) (2025-02-03) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Features | ||||||
|  |  | ||||||
|  | * **dbml-editor:** add dbml editor in side pannel ([#534](https://github.com/chartdb/chartdb/issues/534)) ([88be6c1](https://github.com/chartdb/chartdb/commit/88be6c1fd4a7e1f20937e8204c14d8fc1c2665b4)) | ||||||
|  | * **import-dbml:** add import dbml functionality ([#549](https://github.com/chartdb/chartdb/issues/549)) ([b424518](https://github.com/chartdb/chartdb/commit/b424518212290a870fdb7c420a303f65f5901429)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Bug Fixes | ||||||
|  |  | ||||||
|  | * **canvas edit:** add option to edit names in canvas ([#536](https://github.com/chartdb/chartdb/issues/536)) ([0dcc9b9](https://github.com/chartdb/chartdb/commit/0dcc9b9568cfe749d44d2e93cb365ba3d3a1e71c)) | ||||||
|  | * **dbml-editor:** add shortcuts to dbml and filter: [#534](https://github.com/chartdb/chartdb/issues/534) ([#535](https://github.com/chartdb/chartdb/issues/535)) ([3b3be08](https://github.com/chartdb/chartdb/commit/3b3be086b1e8d5acf999f8504580d9e2f956f7da)) | ||||||
|  | * **dbml:** add error handling ([#545](https://github.com/chartdb/chartdb/issues/545)) ([fef6d3f](https://github.com/chartdb/chartdb/commit/fef6d3f4996130a3769d1f25b4b1f2090293a1bf)) | ||||||
|  | * **empty-state:** fix dark-mode for empty-state ([#547](https://github.com/chartdb/chartdb/issues/547)) ([99a8201](https://github.com/chartdb/chartdb/commit/99a820139861546a012d7b562ddbb9b77698151a)) | ||||||
|  | * **examples:** fix employee example dbml ([#544](https://github.com/chartdb/chartdb/issues/544)) ([2118bce](https://github.com/chartdb/chartdb/commit/2118bce0f00d55eb19d22b9fa2d4964ba2533a09)) | ||||||
|  | * **i18n:** translation/Ukrainian ([#529](https://github.com/chartdb/chartdb/issues/529)) ([ff3269e](https://github.com/chartdb/chartdb/commit/ff3269ec0510bbae4bc114e65a1ea86a656e8785)) | ||||||
|  | * **open-diagram:** add arrow keys navigation in open diagram dialog ([#537](https://github.com/chartdb/chartdb/issues/537)) ([14f11c2](https://github.com/chartdb/chartdb/commit/14f11c27a7ad5b990131c8495148cabf12835082)) | ||||||
|  | * **performance:** fix bundle size ([#551](https://github.com/chartdb/chartdb/issues/551)) ([4c93326](https://github.com/chartdb/chartdb/commit/4c93326bb6e3eaa143373c500a0c641e95a53fb9)) | ||||||
|  | * **performance:** reduce bundle size ([#553](https://github.com/chartdb/chartdb/issues/553)) ([004d530](https://github.com/chartdb/chartdb/commit/004d530880a50dea6e9786eb9ae63cf592a4d852)) | ||||||
|  | * **performance:** resolve error on startup ([#552](https://github.com/chartdb/chartdb/issues/552)) ([fd2cc9f](https://github.com/chartdb/chartdb/commit/fd2cc9fcfc8f4a9f0bc79def47d89114159392fb)) | ||||||
|  | * **psql-import:** remove typo for import command (psql) ([#546](https://github.com/chartdb/chartdb/issues/546)) ([eb9b41e](https://github.com/chartdb/chartdb/commit/eb9b41e4f656bec1451c45763f4ea5b547aeec5c)) | ||||||
|  | * **scroll:** fix scroll area ([#550](https://github.com/chartdb/chartdb/issues/550)) ([ef3d7a8](https://github.com/chartdb/chartdb/commit/ef3d7a8b67431e923b75bf8287b86bbc8abe723b)) | ||||||
|  |  | ||||||
|  | ## [1.6.1](https://github.com/chartdb/chartdb/compare/v1.6.0...v1.6.1) (2025-01-26) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ### Bug Fixes | ||||||
|  |  | ||||||
|  | * change empty state image ([#531](https://github.com/chartdb/chartdb/issues/531)) ([42d4cba](https://github.com/chartdb/chartdb/commit/42d4cbac8ce352e0e4e155d7003bfb85296b897f)) | ||||||
|  | * **chat-type:** remove typo of char datatype in examples ([#530](https://github.com/chartdb/chartdb/issues/530)) ([58231c9](https://github.com/chartdb/chartdb/commit/58231c91393de30ebff817f0ebc57a5c5579f106)) | ||||||
|  | * **empty_state:** customize empty state ([#533](https://github.com/chartdb/chartdb/issues/533)) ([1643e7b](https://github.com/chartdb/chartdb/commit/1643e7bdeb1bbaf081ab064e871d102c87243c0a)) | ||||||
|  | * **Image Export:** importing css rules error while download image ([#524](https://github.com/chartdb/chartdb/issues/524)) ([e9e2736](https://github.com/chartdb/chartdb/commit/e9e2736cb2203702d53df9afc30b8e989a8c9953)) | ||||||
|  | * **shortcuts:** add zoom all shortcut ([#528](https://github.com/chartdb/chartdb/issues/528)) ([7452ca6](https://github.com/chartdb/chartdb/commit/7452ca6965b0332a93b686c397ddf51013e42506)) | ||||||
|  | * **filter-tables:** show clean filter if no-results ([#532](https://github.com/chartdb/chartdb/issues/532)) ([c36cd33](https://github.com/chartdb/chartdb/commit/c36cd33180badaa9b7f9e27c765f19cb03a50ccd)) | ||||||
|  |  | ||||||
| ## [1.6.0](https://github.com/chartdb/chartdb/compare/v1.5.1...v1.6.0) (2025-01-02) | ## [1.6.0](https://github.com/chartdb/chartdb/compare/v1.5.1...v1.6.0) (2025-01-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 | Instances of abusive, harassing, or otherwise unacceptable behavior may be | ||||||
| reported to the community leaders responsible for enforcement at | 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 complaints will be reviewed and investigated promptly and fairly. | ||||||
|  |  | ||||||
| All community leaders are obligated to respect the privacy and security of the | 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). | 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 | ### Creating a Branch | ||||||
|  |  | ||||||
| @@ -35,7 +35,7 @@ By contributing, you agree that your work will be licensed under ChartDB's [lice | |||||||
| ## Questions? | ## Questions? | ||||||
|  |  | ||||||
| Feel free to ask in `#contributing` on [Discord](https://discord.gg/QeFwyWSKwC) if you have questions about our process, how to proceed, etc. | 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) | ||||||
|  |  | ||||||
| --- | --- | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						| @@ -1,6 +1,9 @@ | |||||||
| FROM node:22-alpine AS builder | FROM node:22-alpine AS builder | ||||||
|  |  | ||||||
| ARG VITE_OPENAI_API_KEY | ARG VITE_OPENAI_API_KEY | ||||||
|  | ARG VITE_OPENAI_API_ENDPOINT | ||||||
|  | ARG VITE_LLM_MODEL_NAME | ||||||
|  | ARG VITE_HIDE_BUCKLE_DOT_DEV | ||||||
|  |  | ||||||
| WORKDIR /usr/src/app | WORKDIR /usr/src/app | ||||||
|  |  | ||||||
| @@ -10,9 +13,13 @@ RUN npm ci | |||||||
|  |  | ||||||
| COPY . . | COPY . . | ||||||
|  |  | ||||||
|  | RUN echo "VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}" > .env && \ | ||||||
|  |     echo "VITE_OPENAI_API_ENDPOINT=${VITE_OPENAI_API_ENDPOINT}" >> .env && \ | ||||||
|  |     echo "VITE_LLM_MODEL_NAME=${VITE_LLM_MODEL_NAME}" >> .env && \ | ||||||
|  |     echo "VITE_HIDE_BUCKLE_DOT_DEV=${VITE_HIDE_BUCKLE_DOT_DEV}" >> .env  | ||||||
|  |  | ||||||
| RUN npm run build | RUN npm run build | ||||||
|  |  | ||||||
| # Use a lightweight web server to serve the production build |  | ||||||
| FROM nginx:stable-alpine AS production | FROM nginx:stable-alpine AS production | ||||||
|  |  | ||||||
| COPY --from=builder /usr/src/app/dist /usr/share/nginx/html | COPY --from=builder /usr/src/app/dist /usr/share/nginx/html | ||||||
| @@ -20,7 +27,6 @@ COPY ./default.conf.template /etc/nginx/conf.d/default.conf.template | |||||||
| COPY entrypoint.sh /entrypoint.sh | COPY entrypoint.sh /entrypoint.sh | ||||||
| RUN chmod +x /entrypoint.sh | RUN chmod +x /entrypoint.sh | ||||||
|  |  | ||||||
| # Expose the default port for the Nginx web server |  | ||||||
| EXPOSE 80 | EXPOSE 80 | ||||||
|  |  | ||||||
| ENTRYPOINT ["/entrypoint.sh"] | ENTRYPOINT ["/entrypoint.sh"] | ||||||
							
								
								
									
										65
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -30,8 +30,8 @@ | |||||||
|   <a href="https://discord.gg/QeFwyWSKwC"> |   <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" /> |     <img src="https://img.shields.io/discord/1277047413705670678?color=5865F2&label=Discord&logo=discord&logoColor=white" alt="Discord community channel" /> | ||||||
|   </a> |   </a> | ||||||
|   <a href="https://x.com/chartdb_io"> |   <a href="https://x.com/intent/follow?screen_name=jonathanfishner"> | ||||||
|     <img src="https://img.shields.io/twitter/follow/ChartDB?style=social"/> |     <img src="https://img.shields.io/twitter/follow/jonathanfishner?style=social"/> | ||||||
|   </a> |   </a> | ||||||
|  |  | ||||||
| </h4> | </h4> | ||||||
| @@ -49,13 +49,13 @@ Instantly visualize your database schema with a single **"Smart Query."** Custom | |||||||
|  |  | ||||||
| **What it does**: | **What it does**: | ||||||
|  |  | ||||||
| -   **Instant Schema Import** | - **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. |   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** | - **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. |   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** | - **Interactive Editing** | ||||||
|     Fine-tune your database schema using our intuitive editor. Easily make adjustments or annotations to better visualize complex structures. |   Fine-tune your database schema using our intuitive editor. Easily make adjustments or annotations to better visualize complex structures. | ||||||
|  |  | ||||||
| ### Status | ### Status | ||||||
|  |  | ||||||
| @@ -63,13 +63,13 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif | |||||||
|  |  | ||||||
| ### Supported Databases | ### 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"/> ) | - ✅ 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 | - ✅ MySQL | ||||||
| -   ✅ SQL Server | - ✅ SQL Server | ||||||
| -   ✅ MariaDB | - ✅ MariaDB | ||||||
| -   ✅ SQLite | - ✅ 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 | - ✅ CockroachDB | ||||||
| -   ✅ ClickHouse | - ✅ ClickHouse | ||||||
|  |  | ||||||
| ## Getting Started | ## Getting Started | ||||||
|  |  | ||||||
| @@ -91,24 +91,51 @@ npm run build | |||||||
|  |  | ||||||
| Or like this if you want to have AI capabilities: | Or like this if you want to have AI capabilities: | ||||||
|  |  | ||||||
| ``` | ```bash | ||||||
| npm install | npm install | ||||||
| VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> npm run build | VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> npm run build | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### Run the Docker Container | ### Run the Docker Container | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 ghcr.io/chartdb/chartdb:latest | docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 ghcr.io/chartdb/chartdb:latest | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| #### Build and Run locally | #### Build and Run locally | ||||||
|  |  | ||||||
| ```bash | ```bash | ||||||
| docker build -t chartdb . | docker build -t chartdb . | ||||||
| docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 chartdb | docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 chartdb | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|  | #### Using Custom Inference Server | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | # Build | ||||||
|  | docker build \ | ||||||
|  |   --build-arg VITE_OPENAI_API_ENDPOINT=<YOUR_ENDPOINT> \ | ||||||
|  |   --build-arg VITE_LLM_MODEL_NAME=<YOUR_MODEL_NAME> \ | ||||||
|  |   -t chartdb . | ||||||
|  |  | ||||||
|  | # Run | ||||||
|  | docker run \ | ||||||
|  |   -e OPENAI_API_ENDPOINT=<YOUR_ENDPOINT> \ | ||||||
|  |   -e LLM_MODEL_NAME=<YOUR_MODEL_NAME> \ | ||||||
|  |   -p 8080:80 chartdb | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | > **Note:** You must configure either Option 1 (OpenAI API key) OR Option 2 (Custom endpoint and model name) for AI capabilities to work. Do not mix the two options. | ||||||
|  |  | ||||||
| Open your browser and navigate to `http://localhost:8080`. | Open your browser and navigate to `http://localhost:8080`. | ||||||
|  |  | ||||||
|  | Example configuration for a local vLLM server: | ||||||
|  |  | ||||||
|  | ```bash | ||||||
|  | VITE_OPENAI_API_ENDPOINT=http://localhost:8000/v1 | ||||||
|  | VITE_LLM_MODEL_NAME=Qwen/Qwen2.5-32B-Instruct-AWQ | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ## Try it on our website | ## Try it on our website | ||||||
|  |  | ||||||
| 1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2) | 1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2) | ||||||
| @@ -120,9 +147,9 @@ Open your browser and navigate to `http://localhost:8080`. | |||||||
|  |  | ||||||
| ## 💚 Community & Support | ## 💚 Community & Support | ||||||
|  |  | ||||||
| -   [Discord](https://discord.gg/QeFwyWSKwC) (For live discussion with the community and the ChartDB team) | - [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) | - [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) | - [Twitter](https://x.com/intent/follow?screen_name=jonathanfishner) (Get news fast) | ||||||
|  |  | ||||||
| ## Contributing | ## Contributing | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,17 +1,20 @@ | |||||||
| { | { | ||||||
|   "$schema": "https://ui.shadcn.com/schema.json", |     "$schema": "https://ui.shadcn.com/schema.json", | ||||||
|   "style": "new-york", |     "style": "new-york", | ||||||
|   "rsc": false, |     "rsc": false, | ||||||
|   "tsx": true, |     "tsx": true, | ||||||
|   "tailwind": { |     "tailwind": { | ||||||
|     "config": "tailwind.config.js", |         "config": "tailwind.config.js", | ||||||
|     "css": "src/globals.css", |         "css": "src/globals.css", | ||||||
|     "baseColor": "slate", |         "baseColor": "slate", | ||||||
|     "cssVariables": true, |         "cssVariables": true, | ||||||
|     "prefix": "" |         "prefix": "" | ||||||
|   }, |     }, | ||||||
|   "aliases": { |     "aliases": { | ||||||
|     "components": "src/components", |         "components": "src/components", | ||||||
|     "utils": "@/lib/utils" |         "utils": "src/lib/utils", | ||||||
|   } |         "ui": "src/components/ui", | ||||||
| } |         "lib": "src/lib", | ||||||
|  |         "hooks": "src/hooks" | ||||||
|  |     } | ||||||
|  | } | ||||||
|   | |||||||
| @@ -10,7 +10,12 @@ server { | |||||||
|  |  | ||||||
|     location /config.js { |     location /config.js { | ||||||
|         default_type application/javascript; |         default_type application/javascript; | ||||||
|         return 200 "window.env = { OPENAI_API_KEY: \"$OPENAI_API_KEY\" };"; |         return 200 "window.env = {  | ||||||
|  |             OPENAI_API_KEY: \"$OPENAI_API_KEY\", | ||||||
|  |             OPENAI_API_ENDPOINT: \"$OPENAI_API_ENDPOINT\", | ||||||
|  |             LLM_MODEL_NAME: \"$LLM_MODEL_NAME\", | ||||||
|  |             HIDE_BUCKLE_DOT_DEV: \"$HIDE_BUCKLE_DOT_DEV\" | ||||||
|  |         };"; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     error_page   500 502 503 504  /50x.html; |     error_page   500 502 503 504  /50x.html; | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| #!/bin/sh | #!/bin/sh | ||||||
|  |  | ||||||
| # Replace placeholders in nginx.conf | # Replace placeholders in nginx.conf | ||||||
| envsubst '${OPENAI_API_KEY}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf | envsubst '${OPENAI_API_KEY} ${OPENAI_API_ENDPOINT} ${LLM_MODEL_NAME} ${HIDE_BUCKLE_DOT_DEV}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf | ||||||
|  |  | ||||||
| # Start Nginx | # Start Nginx | ||||||
| nginx -g "daemon off;" | nginx -g "daemon off;" | ||||||
|   | |||||||
							
								
								
									
										5097
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										15
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|     "name": "chartdb", |     "name": "chartdb", | ||||||
|     "private": true, |     "private": true, | ||||||
|     "version": "1.6.0", |     "version": "1.13.0", | ||||||
|     "type": "module", |     "type": "module", | ||||||
|     "scripts": { |     "scripts": { | ||||||
|         "dev": "vite", |         "dev": "vite", | ||||||
| @@ -13,6 +13,7 @@ | |||||||
|     }, |     }, | ||||||
|     "dependencies": { |     "dependencies": { | ||||||
|         "@ai-sdk/openai": "^0.0.51", |         "@ai-sdk/openai": "^0.0.51", | ||||||
|  |         "@dbml/core": "^3.9.5", | ||||||
|         "@dnd-kit/sortable": "^8.0.0", |         "@dnd-kit/sortable": "^8.0.0", | ||||||
|         "@monaco-editor/react": "^4.6.0", |         "@monaco-editor/react": "^4.6.0", | ||||||
|         "@radix-ui/react-accordion": "^1.2.0", |         "@radix-ui/react-accordion": "^1.2.0", | ||||||
| @@ -21,27 +22,27 @@ | |||||||
|         "@radix-ui/react-checkbox": "^1.1.1", |         "@radix-ui/react-checkbox": "^1.1.1", | ||||||
|         "@radix-ui/react-collapsible": "^1.1.0", |         "@radix-ui/react-collapsible": "^1.1.0", | ||||||
|         "@radix-ui/react-context-menu": "^2.2.1", |         "@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-dropdown-menu": "^2.1.1", | ||||||
|         "@radix-ui/react-hover-card": "^1.1.1", |         "@radix-ui/react-hover-card": "^1.1.1", | ||||||
|         "@radix-ui/react-icons": "^1.3.0", |         "@radix-ui/react-icons": "^1.3.0", | ||||||
|         "@radix-ui/react-label": "^2.1.0", |         "@radix-ui/react-label": "^2.1.0", | ||||||
|         "@radix-ui/react-menubar": "^1.1.1", |         "@radix-ui/react-menubar": "^1.1.1", | ||||||
|         "@radix-ui/react-popover": "^1.1.1", |         "@radix-ui/react-popover": "^1.1.1", | ||||||
|         "@radix-ui/react-scroll-area": "^1.1.0", |         "@radix-ui/react-scroll-area": "1.2.0", | ||||||
|         "@radix-ui/react-select": "^2.1.1", |         "@radix-ui/react-select": "^2.1.1", | ||||||
|         "@radix-ui/react-separator": "^1.1.0", |         "@radix-ui/react-separator": "^1.1.2", | ||||||
|         "@radix-ui/react-slot": "^1.1.0", |         "@radix-ui/react-slot": "^1.1.2", | ||||||
|         "@radix-ui/react-tabs": "^1.1.0", |         "@radix-ui/react-tabs": "^1.1.0", | ||||||
|         "@radix-ui/react-toast": "^1.2.1", |         "@radix-ui/react-toast": "^1.2.1", | ||||||
|         "@radix-ui/react-toggle": "^1.1.0", |         "@radix-ui/react-toggle": "^1.1.0", | ||||||
|         "@radix-ui/react-toggle-group": "^1.1.0", |         "@radix-ui/react-toggle-group": "^1.1.0", | ||||||
|         "@radix-ui/react-tooltip": "^1.1.2", |         "@radix-ui/react-tooltip": "^1.1.8", | ||||||
|         "@uidotdev/usehooks": "^2.4.1", |         "@uidotdev/usehooks": "^2.4.1", | ||||||
|         "@xyflow/react": "^12.3.1", |         "@xyflow/react": "^12.3.1", | ||||||
|         "ahooks": "^3.8.1", |         "ahooks": "^3.8.1", | ||||||
|         "ai": "^3.3.14", |         "ai": "^3.3.14", | ||||||
|         "class-variance-authority": "^0.7.0", |         "class-variance-authority": "^0.7.1", | ||||||
|         "clsx": "^2.1.1", |         "clsx": "^2.1.1", | ||||||
|         "cmdk": "^1.0.0", |         "cmdk": "^1.0.0", | ||||||
|         "dexie": "^4.0.8", |         "dexie": "^4.0.8", | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/cloudflare_d1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 937 B | 
| Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 6.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/empty_state_dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.1 KiB | 
| Before Width: | Height: | Size: 416 KiB After Width: | Height: | Size: 482 KiB | 
| Before Width: | Height: | Size: 391 KiB After Width: | Height: | Size: 434 KiB | 
| Before Width: | Height: | Size: 441 KiB After Width: | Height: | Size: 543 KiB | 
| Before Width: | Height: | Size: 405 KiB After Width: | Height: | Size: 488 KiB | 
| Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 404 KiB | 
| Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 359 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/oracle_logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 21 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/oracle_logo_2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 18 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/oracle_logo_dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 19 KiB | 
| @@ -1,2 +1,3 @@ | |||||||
| import './config.ts'; | import './config.ts'; | ||||||
| export { Editor } from '@monaco-editor/react'; | 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 { useMonaco } from '@monaco-editor/react'; | ||||||
| import { useToast } from '@/components/toast/use-toast'; | import { useToast } from '@/components/toast/use-toast'; | ||||||
| import { Button } from '../button/button'; | import { Button } from '../button/button'; | ||||||
|  | import type { LucideIcon } from 'lucide-react'; | ||||||
| import { Copy, CopyCheck } from 'lucide-react'; | import { Copy, CopyCheck } from 'lucide-react'; | ||||||
| import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip'; | import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| @@ -12,29 +13,49 @@ import { DarkTheme } from './themes/dark'; | |||||||
| import { LightTheme } from './themes/light'; | import { LightTheme } from './themes/light'; | ||||||
| import './config.ts'; | import './config.ts'; | ||||||
|  |  | ||||||
| export interface CodeSnippetProps { |  | ||||||
|     className?: string; |  | ||||||
|     code: string; |  | ||||||
|     language?: 'sql' | 'shell'; |  | ||||||
|     loading?: boolean; |  | ||||||
|     autoScroll?: boolean; |  | ||||||
|     isComplete?: boolean; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const Editor = lazy(() => | export const Editor = lazy(() => | ||||||
|     import('./code-editor').then((module) => ({ |     import('./code-editor').then((module) => ({ | ||||||
|         default: module.Editor, |         default: module.Editor, | ||||||
|     })) |     })) | ||||||
| ); | ); | ||||||
|  |  | ||||||
|  | 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( | export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | ||||||
|     ({ |     ({ | ||||||
|         className, |         className, | ||||||
|         code, |         code, | ||||||
|  |         codeToCopy, | ||||||
|         loading, |         loading, | ||||||
|         language = 'sql', |         language = 'sql', | ||||||
|         autoScroll = false, |         autoScroll = false, | ||||||
|         isComplete = true, |         isComplete = true, | ||||||
|  |         editorProps, | ||||||
|  |         actions, | ||||||
|     }) => { |     }) => { | ||||||
|         const { t } = useTranslation(); |         const { t } = useTranslation(); | ||||||
|         const monaco = useMonaco(); |         const monaco = useMonaco(); | ||||||
| @@ -81,7 +102,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             try { |             try { | ||||||
|                 await navigator.clipboard.writeText(code); |                 await navigator.clipboard.writeText(codeToCopy ?? code); | ||||||
|                 setIsCopied(true); |                 setIsCopied(true); | ||||||
|             } catch { |             } catch { | ||||||
|                 setIsCopied(false); |                 setIsCopied(false); | ||||||
| @@ -93,7 +114,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | |||||||
|                     ), |                     ), | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|         }, [code, t, toast]); |         }, [code, codeToCopy, t, toast]); | ||||||
|  |  | ||||||
|         return ( |         return ( | ||||||
|             <div |             <div | ||||||
| @@ -107,36 +128,58 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | |||||||
|                 ) : ( |                 ) : ( | ||||||
|                     <Suspense fallback={<Spinner />}> |                     <Suspense fallback={<Spinner />}> | ||||||
|                         {isComplete ? ( |                         {isComplete ? ( | ||||||
|                             <Tooltip |                             <div className="absolute right-1 top-1 z-10 flex flex-col gap-1"> | ||||||
|                                 onOpenChange={setTooltipOpen} |                                 <Tooltip | ||||||
|                                 open={isCopied || tooltipOpen} |                                     onOpenChange={setTooltipOpen} | ||||||
|                             > |                                     open={isCopied || tooltipOpen} | ||||||
|                                 <TooltipTrigger |  | ||||||
|                                     asChild |  | ||||||
|                                     className="absolute right-1 top-1 z-10" |  | ||||||
|                                 > |                                 > | ||||||
|                                     <span> |                                     <TooltipTrigger asChild> | ||||||
|                                         <Button |                                         <span> | ||||||
|                                             className=" h-fit p-1.5" |                                             <Button | ||||||
|                                             variant="outline" |                                                 className="h-fit p-1.5" | ||||||
|                                             onClick={copyToClipboard} |                                                 variant="outline" | ||||||
|                                         > |                                                 onClick={copyToClipboard} | ||||||
|                                             {isCopied ? ( |                                             > | ||||||
|                                                 <CopyCheck size={16} /> |                                                 {isCopied ? ( | ||||||
|                                             ) : ( |                                                     <CopyCheck size={16} /> | ||||||
|                                                 <Copy size={16} /> |                                                 ) : ( | ||||||
|                                             )} |                                                     <Copy size={16} /> | ||||||
|                                         </Button> |                                                 )} | ||||||
|                                     </span> |                                             </Button> | ||||||
|                                 </TooltipTrigger> |                                         </span> | ||||||
|                                 <TooltipContent> |                                     </TooltipTrigger> | ||||||
|                                     {t( |                                     <TooltipContent> | ||||||
|                                         isCopied |                                         {t( | ||||||
|                                             ? 'copied' |                                             isCopied | ||||||
|                                             : 'copy_to_clipboard' |                                                 ? 'copied' | ||||||
|                                     )} |                                                 : 'copy_to_clipboard' | ||||||
|                                 </TooltipContent> |                                         )} | ||||||
|                             </Tooltip> |                                     </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} |                         ) : null} | ||||||
|  |  | ||||||
|                         <Editor |                         <Editor | ||||||
| @@ -144,27 +187,32 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | |||||||
|                             language={language} |                             language={language} | ||||||
|                             loading={<Spinner />} |                             loading={<Spinner />} | ||||||
|                             theme={effectiveTheme} |                             theme={effectiveTheme} | ||||||
|  |                             {...editorProps} | ||||||
|                             options={{ |                             options={{ | ||||||
|                                 minimap: { |  | ||||||
|                                     enabled: false, |  | ||||||
|                                 }, |  | ||||||
|                                 readOnly: true, |                                 readOnly: true, | ||||||
|                                 automaticLayout: true, |                                 automaticLayout: true, | ||||||
|                                 scrollbar: { |  | ||||||
|                                     vertical: 'hidden', |  | ||||||
|                                     horizontal: 'hidden', |  | ||||||
|                                     alwaysConsumeMouseWheel: false, |  | ||||||
|                                 }, |  | ||||||
|                                 scrollBeyondLastLine: false, |                                 scrollBeyondLastLine: false, | ||||||
|                                 renderValidationDecorations: 'off', |                                 renderValidationDecorations: 'off', | ||||||
|                                 lineDecorationsWidth: 0, |                                 lineDecorationsWidth: 0, | ||||||
|                                 overviewRulerBorder: false, |                                 overviewRulerBorder: false, | ||||||
|                                 overviewRulerLanes: 0, |                                 overviewRulerLanes: 0, | ||||||
|                                 hideCursorInOverviewRuler: true, |                                 hideCursorInOverviewRuler: true, | ||||||
|  |                                 contextmenu: false, | ||||||
|  |                                 ...editorProps?.options, | ||||||
|                                 guides: { |                                 guides: { | ||||||
|                                     indentation: false, |                                     indentation: false, | ||||||
|  |                                     ...editorProps?.options?.guides, | ||||||
|  |                                 }, | ||||||
|  |                                 scrollbar: { | ||||||
|  |                                     vertical: 'hidden', | ||||||
|  |                                     horizontal: 'hidden', | ||||||
|  |                                     alwaysConsumeMouseWheel: false, | ||||||
|  |                                     ...editorProps?.options?.scrollbar, | ||||||
|  |                                 }, | ||||||
|  |                                 minimap: { | ||||||
|  |                                     enabled: false, | ||||||
|  |                                     ...editorProps?.options?.minimap, | ||||||
|                                 }, |                                 }, | ||||||
|                                 contextmenu: false, |  | ||||||
|                             }} |                             }} | ||||||
|                         /> |                         /> | ||||||
|                         {!isComplete ? ( |                         {!isComplete ? ( | ||||||
|   | |||||||
							
								
								
									
										54
									
								
								src/components/code-snippet/languages/dbml-language.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,54 @@ | |||||||
|  | import type { Monaco } from '@monaco-editor/react'; | ||||||
|  | import { dataTypes } from '@/lib/data/data-types/data-types'; | ||||||
|  |  | ||||||
|  | export const setupDBMLLanguage = (monaco: Monaco) => { | ||||||
|  |     monaco.languages.register({ id: 'dbml' }); | ||||||
|  |  | ||||||
|  |     // Define themes for DBML | ||||||
|  |     monaco.editor.defineTheme('dbml-dark', { | ||||||
|  |         base: 'vs-dark', | ||||||
|  |         inherit: true, | ||||||
|  |         rules: [ | ||||||
|  |             { token: 'keyword', foreground: '569CD6' }, // Table, Ref keywords | ||||||
|  |             { token: 'string', foreground: 'CE9178' }, // Strings | ||||||
|  |             { token: 'annotation', foreground: '9CDCFE' }, // [annotations] | ||||||
|  |             { token: 'delimiter', foreground: 'D4D4D4' }, // Braces {} | ||||||
|  |             { token: 'operator', foreground: 'D4D4D4' }, // Operators | ||||||
|  |             { token: 'datatype', foreground: '4EC9B0' }, // Data types | ||||||
|  |         ], | ||||||
|  |         colors: {}, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     monaco.editor.defineTheme('dbml-light', { | ||||||
|  |         base: 'vs', | ||||||
|  |         inherit: true, | ||||||
|  |         rules: [ | ||||||
|  |             { token: 'keyword', foreground: '0000FF' }, // Table, Ref keywords | ||||||
|  |             { token: 'string', foreground: 'A31515' }, // Strings | ||||||
|  |             { token: 'annotation', foreground: '001080' }, // [annotations] | ||||||
|  |             { token: 'delimiter', foreground: '000000' }, // Braces {} | ||||||
|  |             { token: 'operator', foreground: '000000' }, // Operators | ||||||
|  |             { token: 'type', foreground: '267F99' }, // Data types | ||||||
|  |         ], | ||||||
|  |         colors: {}, | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     const dataTypesNames = dataTypes.map((dt) => dt.name); | ||||||
|  |     const datatypePattern = dataTypesNames.join('|'); | ||||||
|  |  | ||||||
|  |     monaco.languages.setMonarchTokensProvider('dbml', { | ||||||
|  |         keywords: ['Table', 'Ref', 'Indexes'], | ||||||
|  |         datatypes: dataTypesNames, | ||||||
|  |         tokenizer: { | ||||||
|  |             root: [ | ||||||
|  |                 [/\b(Table|Ref|Indexes)\b/, 'keyword'], | ||||||
|  |                 [/\[.*?\]/, 'annotation'], | ||||||
|  |                 [/".*?"/, 'string'], | ||||||
|  |                 [/'.*?'/, 'string'], | ||||||
|  |                 [/[{}]/, 'delimiter'], | ||||||
|  |                 [/[<>]/, 'operator'], | ||||||
|  |                 [new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching | ||||||
|  |             ], | ||||||
|  |         }, | ||||||
|  |     }); | ||||||
|  | }; | ||||||
| @@ -22,14 +22,15 @@ export interface DiagramIconProps | |||||||
| export const DiagramIcon = React.forwardRef< | export const DiagramIcon = React.forwardRef< | ||||||
|     React.ElementRef<typeof TooltipTrigger>, |     React.ElementRef<typeof TooltipTrigger>, | ||||||
|     DiagramIconProps |     DiagramIconProps | ||||||
| >(({ databaseType, databaseEdition, className, imgClassName }, ref) => | >(({ databaseType, databaseEdition, className, imgClassName, onClick }, ref) => | ||||||
|     databaseEdition ? ( |     databaseEdition ? ( | ||||||
|         <Tooltip> |         <Tooltip> | ||||||
|             <TooltipTrigger className={cn('mr-1', className)} ref={ref} asChild> |             <TooltipTrigger className={cn('mr-1', className)} ref={ref} asChild> | ||||||
|                 <img |                 <img | ||||||
|                     src={databaseEditionToImageMap[databaseEdition]} |                     src={databaseEditionToImageMap[databaseEdition]} | ||||||
|                     className={cn('h-5 max-w-fit rounded-full', imgClassName)} |                     className={cn('max-h-5 max-w-5 rounded-full', imgClassName)} | ||||||
|                     alt="database" |                     alt="database" | ||||||
|  |                     onClick={onClick} | ||||||
|                 /> |                 /> | ||||||
|             </TooltipTrigger> |             </TooltipTrigger> | ||||||
|             <TooltipContent> |             <TooltipContent> | ||||||
| @@ -41,8 +42,9 @@ export const DiagramIcon = React.forwardRef< | |||||||
|             <TooltipTrigger className={cn('mr-2', className)} ref={ref} asChild> |             <TooltipTrigger className={cn('mr-2', className)} ref={ref} asChild> | ||||||
|                 <img |                 <img | ||||||
|                     src={databaseSecondaryLogoMap[databaseType]} |                     src={databaseSecondaryLogoMap[databaseType]} | ||||||
|                     className={cn('h-5 max-w-fit', imgClassName)} |                     className={cn('max-h-5 max-w-5', imgClassName)} | ||||||
|                     alt="database" |                     alt="database" | ||||||
|  |                     onClick={onClick} | ||||||
|                 /> |                 /> | ||||||
|             </TooltipTrigger> |             </TooltipTrigger> | ||||||
|             <TooltipContent> |             <TooltipContent> | ||||||
|   | |||||||
| @@ -1,30 +1,66 @@ | |||||||
| import React, { forwardRef } from 'react'; | import React, { forwardRef } from 'react'; | ||||||
| import EmptyStateImage from '@/assets/empty_state.png'; | import EmptyStateImage from '@/assets/empty_state.png'; | ||||||
|  | import EmptyStateImageDark from '@/assets/empty_state_dark.png'; | ||||||
| import { Label } from '@/components/label/label'; | import { Label } from '@/components/label/label'; | ||||||
| import { cn } from '@/lib/utils'; | import { cn } from '@/lib/utils'; | ||||||
|  | import { useTheme } from '@/hooks/use-theme'; | ||||||
|  |  | ||||||
| export interface EmptyStateProps { | export interface EmptyStateProps { | ||||||
|     title: string; |     title: string; | ||||||
|     description: string; |     description: string; | ||||||
|  |     imageClassName?: string; | ||||||
|  |     titleClassName?: string; | ||||||
|  |     descriptionClassName?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const EmptyState = forwardRef< | export const EmptyState = forwardRef< | ||||||
|     HTMLDivElement, |     HTMLDivElement, | ||||||
|     React.HTMLAttributes<HTMLDivElement> & EmptyStateProps |     React.HTMLAttributes<HTMLDivElement> & EmptyStateProps | ||||||
| >(({ title, description, className }, ref) => ( | >( | ||||||
|     <div |     ( | ||||||
|         ref={ref} |         { | ||||||
|         className={cn( |             title, | ||||||
|             'flex flex-1 flex-col items-center justify-center space-y-1', |             description, | ||||||
|             className |             className, | ||||||
|         )} |             titleClassName, | ||||||
|     > |             descriptionClassName, | ||||||
|         <img src={EmptyStateImage} alt="Empty state" className="w-32" /> |             imageClassName, | ||||||
|         <Label className="text-base">{title}</Label> |         }, | ||||||
|         <Label className="text-sm font-normal text-muted-foreground"> |         ref | ||||||
|             {description} |     ) => { | ||||||
|         </Label> |         const { effectiveTheme } = useTheme(); | ||||||
|     </div> |  | ||||||
| )); |         return ( | ||||||
|  |             <div | ||||||
|  |                 ref={ref} | ||||||
|  |                 className={cn( | ||||||
|  |                     'flex flex-1 flex-col items-center justify-center space-y-1', | ||||||
|  |                     className | ||||||
|  |                 )} | ||||||
|  |             > | ||||||
|  |                 <img | ||||||
|  |                     src={ | ||||||
|  |                         effectiveTheme === 'dark' | ||||||
|  |                             ? EmptyStateImageDark | ||||||
|  |                             : EmptyStateImage | ||||||
|  |                     } | ||||||
|  |                     alt="Empty state" | ||||||
|  |                     className={cn('mb-2 w-20', imageClassName)} | ||||||
|  |                 /> | ||||||
|  |                 <Label className={cn('text-base', titleClassName)}> | ||||||
|  |                     {title} | ||||||
|  |                 </Label> | ||||||
|  |                 <Label | ||||||
|  |                     className={cn( | ||||||
|  |                         'text-sm font-normal text-muted-foreground', | ||||||
|  |                         descriptionClassName | ||||||
|  |                     )} | ||||||
|  |                 > | ||||||
|  |                     {description} | ||||||
|  |                 </Label> | ||||||
|  |             </div> | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | ); | ||||||
|  |  | ||||||
| EmptyState.displayName = 'EmptyState'; | EmptyState.displayName = 'EmptyState'; | ||||||
|   | |||||||
| @@ -24,12 +24,20 @@ export interface SelectBoxOption { | |||||||
|     value: string; |     value: string; | ||||||
|     label: string; |     label: string; | ||||||
|     description?: string; |     description?: string; | ||||||
|  |     regex?: string; | ||||||
|  |     extractRegex?: RegExp; | ||||||
|  |     group?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface SelectBoxProps { | export interface SelectBoxProps { | ||||||
|     options: SelectBoxOption[]; |     options: SelectBoxOption[]; | ||||||
|     value?: string[] | string; |     value?: string[] | string; | ||||||
|     onChange?: (values: string[] | string) => void; |     valueSuffix?: string; | ||||||
|  |     optionSuffix?: (option: SelectBoxOption) => string; | ||||||
|  |     onChange?: ( | ||||||
|  |         values: string[] | string, | ||||||
|  |         regexMatches?: string[] | string | ||||||
|  |     ) => void; | ||||||
|     placeholder?: string; |     placeholder?: string; | ||||||
|     inputPlaceholder?: string; |     inputPlaceholder?: string; | ||||||
|     emptyPlaceholder?: string; |     emptyPlaceholder?: string; | ||||||
| @@ -44,6 +52,7 @@ export interface SelectBoxProps { | |||||||
|     disabled?: boolean; |     disabled?: boolean; | ||||||
|     open?: boolean; |     open?: boolean; | ||||||
|     onOpenChange?: (open: boolean) => void; |     onOpenChange?: (open: boolean) => void; | ||||||
|  |     popoverClassName?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | ||||||
| @@ -55,10 +64,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | |||||||
|             className, |             className, | ||||||
|             options, |             options, | ||||||
|             value, |             value, | ||||||
|  |             valueSuffix, | ||||||
|             onChange, |             onChange, | ||||||
|             multiple, |             multiple, | ||||||
|             oneLine, |             oneLine, | ||||||
|             selectAll, |             selectAll, | ||||||
|  |             optionSuffix, | ||||||
|             deselectAll, |             deselectAll, | ||||||
|             clearText, |             clearText, | ||||||
|             showClear, |             showClear, | ||||||
| @@ -66,6 +77,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | |||||||
|             disabled, |             disabled, | ||||||
|             open, |             open, | ||||||
|             onOpenChange: setOpen, |             onOpenChange: setOpen, | ||||||
|  |             popoverClassName, | ||||||
|         }, |         }, | ||||||
|         ref |         ref | ||||||
|     ) => { |     ) => { | ||||||
| @@ -86,7 +98,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | |||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         const handleSelect = React.useCallback( |         const handleSelect = React.useCallback( | ||||||
|             (selectedValue: string) => { |             (selectedValue: string, regexMatches?: string[]) => { | ||||||
|                 if (multiple) { |                 if (multiple) { | ||||||
|                     const newValue = |                     const newValue = | ||||||
|                         value?.includes(selectedValue) && Array.isArray(value) |                         value?.includes(selectedValue) && Array.isArray(value) | ||||||
| @@ -94,7 +106,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | |||||||
|                             : [...(value ?? []), selectedValue]; |                             : [...(value ?? []), selectedValue]; | ||||||
|                     onChange?.(newValue); |                     onChange?.(newValue); | ||||||
|                 } else { |                 } else { | ||||||
|                     onChange?.(selectedValue); |                     onChange?.(selectedValue, regexMatches); | ||||||
|                     setIsOpen(false); |                     setIsOpen(false); | ||||||
|                 } |                 } | ||||||
|             }, |             }, | ||||||
| @@ -166,6 +178,101 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | |||||||
|             [isOpen, onOpenChange] |             [isOpen, onOpenChange] | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|  |         const groups = React.useMemo( | ||||||
|  |             () => | ||||||
|  |                 options.reduce( | ||||||
|  |                     (acc, option) => { | ||||||
|  |                         if (option.group) { | ||||||
|  |                             if (!acc[option.group]) { | ||||||
|  |                                 acc[option.group] = []; | ||||||
|  |                             } | ||||||
|  |                             acc[option.group].push(option); | ||||||
|  |                         } else { | ||||||
|  |                             if (!acc['default']) { | ||||||
|  |                                 acc['default'] = []; | ||||||
|  |                             } | ||||||
|  |                             acc['default'].push(option); | ||||||
|  |                         } | ||||||
|  |                         return acc; | ||||||
|  |                     }, | ||||||
|  |                     {} as Record<string, SelectBoxOption[]> | ||||||
|  |                 ), | ||||||
|  |             [options] | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         const hasGroups = React.useMemo( | ||||||
|  |             () => | ||||||
|  |                 Object.keys(groups).filter((group) => group !== 'default') | ||||||
|  |                     .length > 0, | ||||||
|  |             [groups] | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         const renderOption = React.useCallback( | ||||||
|  |             (option: SelectBoxOption) => { | ||||||
|  |                 const isSelected = | ||||||
|  |                     Array.isArray(value) && value.includes(option.value); | ||||||
|  |  | ||||||
|  |                 const isRegexMatch = | ||||||
|  |                     option.regex && new RegExp(option.regex)?.test(searchTerm); | ||||||
|  |  | ||||||
|  |                 const matches = option.extractRegex | ||||||
|  |                     ? searchTerm.match(option.extractRegex) | ||||||
|  |                     : undefined; | ||||||
|  |  | ||||||
|  |                 return ( | ||||||
|  |                     <CommandItem | ||||||
|  |                         className="flex items-center" | ||||||
|  |                         key={option.value} | ||||||
|  |                         keywords={option.regex ? [option.regex] : undefined} | ||||||
|  |                         onSelect={() => | ||||||
|  |                             handleSelect( | ||||||
|  |                                 option.value, | ||||||
|  |                                 matches?.map((match) => match.toString()) | ||||||
|  |                             ) | ||||||
|  |                         } | ||||||
|  |                     > | ||||||
|  |                         {multiple && ( | ||||||
|  |                             <div | ||||||
|  |                                 className={cn( | ||||||
|  |                                     'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary', | ||||||
|  |                                     isSelected | ||||||
|  |                                         ? 'bg-primary text-primary-foreground' | ||||||
|  |                                         : 'opacity-50 [&_svg]:invisible' | ||||||
|  |                                 )} | ||||||
|  |                             > | ||||||
|  |                                 <CheckIcon /> | ||||||
|  |                             </div> | ||||||
|  |                         )} | ||||||
|  |                         <div className="flex flex-1 items-center truncate"> | ||||||
|  |                             <span> | ||||||
|  |                                 {isRegexMatch ? searchTerm : option.label} | ||||||
|  |                                 {!isRegexMatch && optionSuffix | ||||||
|  |                                     ? optionSuffix(option) | ||||||
|  |                                     : ''} | ||||||
|  |                             </span> | ||||||
|  |                             {option.description && ( | ||||||
|  |                                 <span className="ml-1 w-0 flex-1 truncate text-xs text-muted-foreground"> | ||||||
|  |                                     {option.description} | ||||||
|  |                                 </span> | ||||||
|  |                             )} | ||||||
|  |                         </div> | ||||||
|  |                         {((!multiple && option.value === value) || | ||||||
|  |                             isRegexMatch) && ( | ||||||
|  |                             <CheckIcon | ||||||
|  |                                 className={cn( | ||||||
|  |                                     'ml-auto', | ||||||
|  |                                     option.value === value | ||||||
|  |                                         ? 'opacity-100' | ||||||
|  |                                         : 'opacity-0' | ||||||
|  |                                 )} | ||||||
|  |                             /> | ||||||
|  |                         )} | ||||||
|  |                     </CommandItem> | ||||||
|  |                 ); | ||||||
|  |             }, | ||||||
|  |             [value, multiple, searchTerm, handleSelect, optionSuffix] | ||||||
|  |         ); | ||||||
|  |  | ||||||
|         return ( |         return ( | ||||||
|             <Popover open={isOpen} onOpenChange={onOpenChange} modal={true}> |             <Popover open={isOpen} onOpenChange={onOpenChange} modal={true}> | ||||||
|                 <PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}> |                 <PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}> | ||||||
| @@ -199,6 +306,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | |||||||
|                                                 (opt) => opt.value === value |                                                 (opt) => opt.value === value | ||||||
|                                             )?.label |                                             )?.label | ||||||
|                                         } |                                         } | ||||||
|  |                                         {valueSuffix ? valueSuffix : ''} | ||||||
|                                     </div> |                                     </div> | ||||||
|                                 ) |                                 ) | ||||||
|                             ) : ( |                             ) : ( | ||||||
| @@ -235,15 +343,29 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | |||||||
|                     </div> |                     </div> | ||||||
|                 </PopoverTrigger> |                 </PopoverTrigger> | ||||||
|                 <PopoverContent |                 <PopoverContent | ||||||
|                     className="w-fit min-w-[var(--radix-popover-trigger-width)] p-0" |                     className={cn( | ||||||
|  |                         'w-fit min-w-[var(--radix-popover-trigger-width)] p-0', | ||||||
|  |                         popoverClassName | ||||||
|  |                     )} | ||||||
|                     align="center" |                     align="center" | ||||||
|                 > |                 > | ||||||
|                     <Command |                     <Command | ||||||
|                         filter={(value, search) => |                         filter={(value, search, keywords) => { | ||||||
|                             value.toLowerCase().includes(search.toLowerCase()) |                             if ( | ||||||
|  |                                 keywords?.length && | ||||||
|  |                                 keywords.some((keyword) => | ||||||
|  |                                     new RegExp(keyword).test(search) | ||||||
|  |                                 ) | ||||||
|  |                             ) { | ||||||
|  |                                 return 1; | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             return value | ||||||
|  |                                 .toLowerCase() | ||||||
|  |                                 .includes(search.toLowerCase()) | ||||||
|                                 ? 1 |                                 ? 1 | ||||||
|                                 : 0 |                                 : 0; | ||||||
|                         } |                         }} | ||||||
|                     > |                     > | ||||||
|                         <div className="relative"> |                         <div className="relative"> | ||||||
|                             <CommandInput |                             <CommandInput | ||||||
| @@ -298,61 +420,23 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | |||||||
|                             <div className="max-h-64 w-full"> |                             <div className="max-h-64 w-full"> | ||||||
|                                 <CommandGroup> |                                 <CommandGroup> | ||||||
|                                     <CommandList className="max-h-fit w-full"> |                                     <CommandList className="max-h-fit w-full"> | ||||||
|                                         {options.map((option) => { |                                         {hasGroups | ||||||
|                                             const isSelected = |                                             ? Object.entries(groups).map( | ||||||
|                                                 Array.isArray(value) && |                                                   ([ | ||||||
|                                                 value.includes(option.value); |                                                       groupName, | ||||||
|                                             return ( |                                                       groupOptions, | ||||||
|                                                 <CommandItem |                                                   ]) => ( | ||||||
|                                                     className="flex items-center" |                                                       <CommandGroup | ||||||
|                                                     key={option.value} |                                                           key={groupName} | ||||||
|                                                     // value={option.value} |                                                           heading={groupName} | ||||||
|                                                     onSelect={() => |                                                       > | ||||||
|                                                         handleSelect( |                                                           {groupOptions.map( | ||||||
|                                                             option.value |                                                               renderOption | ||||||
|                                                         ) |                                                           )} | ||||||
|                                                     } |                                                       </CommandGroup> | ||||||
|                                                 > |                                                   ) | ||||||
|                                                     {multiple && ( |                                               ) | ||||||
|                                                         <div |                                             : options.map(renderOption)} | ||||||
|                                                             className={cn( |  | ||||||
|                                                                 'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary', |  | ||||||
|                                                                 isSelected |  | ||||||
|                                                                     ? 'bg-primary text-primary-foreground' |  | ||||||
|                                                                     : 'opacity-50 [&_svg]:invisible' |  | ||||||
|                                                             )} |  | ||||||
|                                                         > |  | ||||||
|                                                             <CheckIcon /> |  | ||||||
|                                                         </div> |  | ||||||
|                                                     )} |  | ||||||
|                                                     <div className="flex items-center truncate"> |  | ||||||
|                                                         <span> |  | ||||||
|                                                             {option.label} |  | ||||||
|                                                         </span> |  | ||||||
|                                                         {option.description && ( |  | ||||||
|                                                             <span className="ml-1 text-xs text-muted-foreground"> |  | ||||||
|                                                                 { |  | ||||||
|                                                                     option.description |  | ||||||
|                                                                 } |  | ||||||
|                                                             </span> |  | ||||||
|                                                         )} |  | ||||||
|                                                     </div> |  | ||||||
|                                                     {!multiple && |  | ||||||
|                                                         option.value === |  | ||||||
|                                                             value && ( |  | ||||||
|                                                             <CheckIcon |  | ||||||
|                                                                 className={cn( |  | ||||||
|                                                                     'ml-auto', |  | ||||||
|                                                                     option.value === |  | ||||||
|                                                                         value |  | ||||||
|                                                                         ? 'opacity-100' |  | ||||||
|                                                                         : 'opacity-0' |  | ||||||
|                                                                 )} |  | ||||||
|                                                             /> |  | ||||||
|                                                         )} |  | ||||||
|                                                 </CommandItem> |  | ||||||
|                                             ); |  | ||||||
|                                         })} |  | ||||||
|                                     </CommandList> |                                     </CommandList> | ||||||
|                                 </CommandGroup> |                                 </CommandGroup> | ||||||
|                             </div> |                             </div> | ||||||
|   | |||||||
							
								
								
									
										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 }; | ||||||
							
								
								
									
										22
									
								
								src/context/canvas-context/canvas-context.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,22 @@ | |||||||
|  | import { createContext } from 'react'; | ||||||
|  | import { emptyFn } from '@/lib/utils'; | ||||||
|  | import type { Graph } from '@/lib/graph'; | ||||||
|  | import { createGraph } from '@/lib/graph'; | ||||||
|  |  | ||||||
|  | export interface CanvasContext { | ||||||
|  |     reorderTables: (options?: { updateHistory?: boolean }) => void; | ||||||
|  |     fitView: (options?: { | ||||||
|  |         duration?: number; | ||||||
|  |         padding?: number; | ||||||
|  |         maxZoom?: number; | ||||||
|  |     }) => void; | ||||||
|  |     setOverlapGraph: (graph: Graph<string>) => void; | ||||||
|  |     overlapGraph: Graph<string>; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const canvasContext = createContext<CanvasContext>({ | ||||||
|  |     reorderTables: emptyFn, | ||||||
|  |     fitView: emptyFn, | ||||||
|  |     setOverlapGraph: emptyFn, | ||||||
|  |     overlapGraph: createGraph(), | ||||||
|  | }); | ||||||
							
								
								
									
										85
									
								
								src/context/canvas-context/canvas-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,85 @@ | |||||||
|  | import React, { type ReactNode, useCallback, useState } from 'react'; | ||||||
|  | import { canvasContext } from './canvas-context'; | ||||||
|  | import { useChartDB } from '@/hooks/use-chartdb'; | ||||||
|  | import { | ||||||
|  |     adjustTablePositions, | ||||||
|  |     shouldShowTablesBySchemaFilter, | ||||||
|  | } from '@/lib/domain/db-table'; | ||||||
|  | import { useReactFlow } from '@xyflow/react'; | ||||||
|  | import { findOverlappingTables } from '@/pages/editor-page/canvas/canvas-utils'; | ||||||
|  | import type { Graph } from '@/lib/graph'; | ||||||
|  | import { createGraph } from '@/lib/graph'; | ||||||
|  |  | ||||||
|  | interface CanvasProviderProps { | ||||||
|  |     children: ReactNode; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const CanvasProvider = ({ children }: CanvasProviderProps) => { | ||||||
|  |     const { tables, relationships, updateTablesState, filteredSchemas } = | ||||||
|  |         useChartDB(); | ||||||
|  |     const { fitView } = useReactFlow(); | ||||||
|  |     const [overlapGraph, setOverlapGraph] = | ||||||
|  |         useState<Graph<string>>(createGraph()); | ||||||
|  |  | ||||||
|  |     const reorderTables = useCallback( | ||||||
|  |         ( | ||||||
|  |             options: { updateHistory?: boolean } = { | ||||||
|  |                 updateHistory: true, | ||||||
|  |             } | ||||||
|  |         ) => { | ||||||
|  |             const newTables = adjustTablePositions({ | ||||||
|  |                 relationships, | ||||||
|  |                 tables: tables.filter((table) => | ||||||
|  |                     shouldShowTablesBySchemaFilter(table, filteredSchemas) | ||||||
|  |                 ), | ||||||
|  |                 mode: 'all', // Use 'all' mode for manual reordering | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             const updatedOverlapGraph = findOverlappingTables({ | ||||||
|  |                 tables: newTables, | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             updateTablesState( | ||||||
|  |                 (currentTables) => | ||||||
|  |                     currentTables.map((table) => { | ||||||
|  |                         const newTable = newTables.find( | ||||||
|  |                             (t) => t.id === table.id | ||||||
|  |                         ); | ||||||
|  |                         return { | ||||||
|  |                             id: table.id, | ||||||
|  |                             x: newTable?.x ?? table.x, | ||||||
|  |                             y: newTable?.y ?? table.y, | ||||||
|  |                         }; | ||||||
|  |                     }), | ||||||
|  |                 { | ||||||
|  |                     updateHistory: options.updateHistory ?? true, | ||||||
|  |                     forceOverride: false, | ||||||
|  |                 } | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |             setOverlapGraph(updatedOverlapGraph); | ||||||
|  |  | ||||||
|  |             setTimeout(() => { | ||||||
|  |                 fitView({ | ||||||
|  |                     duration: 500, | ||||||
|  |                     padding: 0.2, | ||||||
|  |                     maxZoom: 0.8, | ||||||
|  |                 }); | ||||||
|  |             }, 500); | ||||||
|  |         }, | ||||||
|  |         [filteredSchemas, relationships, tables, updateTablesState, fitView] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |         <canvasContext.Provider | ||||||
|  |             value={{ | ||||||
|  |                 reorderTables, | ||||||
|  |                 fitView, | ||||||
|  |                 setOverlapGraph, | ||||||
|  |                 overlapGraph, | ||||||
|  |             }} | ||||||
|  |         > | ||||||
|  |             {children} | ||||||
|  |         </canvasContext.Provider> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @@ -10,6 +10,8 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition'; | |||||||
| import type { DBSchema } from '@/lib/domain/db-schema'; | import type { DBSchema } from '@/lib/domain/db-schema'; | ||||||
| import type { DBDependency } from '@/lib/domain/db-dependency'; | import type { DBDependency } from '@/lib/domain/db-dependency'; | ||||||
| import { EventEmitter } from 'ahooks/lib/useEventEmitter'; | import { EventEmitter } from 'ahooks/lib/useEventEmitter'; | ||||||
|  | import type { Area } from '@/lib/domain/area'; | ||||||
|  | import type { DBCustomType } from '@/lib/domain/db-custom-type'; | ||||||
|  |  | ||||||
| export type ChartDBEventType = | export type ChartDBEventType = | ||||||
|     | 'add_tables' |     | 'add_tables' | ||||||
| @@ -70,6 +72,8 @@ export interface ChartDBContext { | |||||||
|     schemas: DBSchema[]; |     schemas: DBSchema[]; | ||||||
|     relationships: DBRelationship[]; |     relationships: DBRelationship[]; | ||||||
|     dependencies: DBDependency[]; |     dependencies: DBDependency[]; | ||||||
|  |     areas: Area[]; | ||||||
|  |     customTypes: DBCustomType[]; | ||||||
|     currentDiagram: Diagram; |     currentDiagram: Diagram; | ||||||
|     events: EventEmitter<ChartDBEvent>; |     events: EventEmitter<ChartDBEvent>; | ||||||
|     readonly?: boolean; |     readonly?: boolean; | ||||||
| @@ -221,6 +225,58 @@ export interface ChartDBContext { | |||||||
|         dependency: Partial<DBDependency>, |         dependency: Partial<DBDependency>, | ||||||
|         options?: { updateHistory: boolean } |         options?: { updateHistory: boolean } | ||||||
|     ) => Promise<void>; |     ) => Promise<void>; | ||||||
|  |  | ||||||
|  |     // Area operations | ||||||
|  |     createArea: (attributes?: Partial<Omit<Area, 'id'>>) => Promise<Area>; | ||||||
|  |     addArea: ( | ||||||
|  |         area: Area, | ||||||
|  |         options?: { updateHistory: boolean } | ||||||
|  |     ) => Promise<void>; | ||||||
|  |     addAreas: ( | ||||||
|  |         areas: Area[], | ||||||
|  |         options?: { updateHistory: boolean } | ||||||
|  |     ) => Promise<void>; | ||||||
|  |     getArea: (id: string) => Area | null; | ||||||
|  |     removeArea: ( | ||||||
|  |         id: string, | ||||||
|  |         options?: { updateHistory: boolean } | ||||||
|  |     ) => Promise<void>; | ||||||
|  |     removeAreas: ( | ||||||
|  |         ids: string[], | ||||||
|  |         options?: { updateHistory: boolean } | ||||||
|  |     ) => Promise<void>; | ||||||
|  |     updateArea: ( | ||||||
|  |         id: string, | ||||||
|  |         area: Partial<Area>, | ||||||
|  |         options?: { updateHistory: boolean } | ||||||
|  |     ) => Promise<void>; | ||||||
|  |  | ||||||
|  |     // Custom type operations | ||||||
|  |     createCustomType: ( | ||||||
|  |         attributes?: Partial<Omit<DBCustomType, 'id'>> | ||||||
|  |     ) => Promise<DBCustomType>; | ||||||
|  |     addCustomType: ( | ||||||
|  |         customType: DBCustomType, | ||||||
|  |         options?: { updateHistory: boolean } | ||||||
|  |     ) => Promise<void>; | ||||||
|  |     addCustomTypes: ( | ||||||
|  |         customTypes: DBCustomType[], | ||||||
|  |         options?: { updateHistory: boolean } | ||||||
|  |     ) => Promise<void>; | ||||||
|  |     getCustomType: (id: string) => DBCustomType | null; | ||||||
|  |     removeCustomType: ( | ||||||
|  |         id: string, | ||||||
|  |         options?: { updateHistory: boolean } | ||||||
|  |     ) => Promise<void>; | ||||||
|  |     removeCustomTypes: ( | ||||||
|  |         ids: string[], | ||||||
|  |         options?: { updateHistory: boolean } | ||||||
|  |     ) => Promise<void>; | ||||||
|  |     updateCustomType: ( | ||||||
|  |         id: string, | ||||||
|  |         customType: Partial<DBCustomType>, | ||||||
|  |         options?: { updateHistory: boolean } | ||||||
|  |     ) => Promise<void>; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const chartDBContext = createContext<ChartDBContext>({ | export const chartDBContext = createContext<ChartDBContext>({ | ||||||
| @@ -230,6 +286,8 @@ export const chartDBContext = createContext<ChartDBContext>({ | |||||||
|     tables: [], |     tables: [], | ||||||
|     relationships: [], |     relationships: [], | ||||||
|     dependencies: [], |     dependencies: [], | ||||||
|  |     areas: [], | ||||||
|  |     customTypes: [], | ||||||
|     schemas: [], |     schemas: [], | ||||||
|     filteredSchemas: [], |     filteredSchemas: [], | ||||||
|     filterSchemas: emptyFn, |     filterSchemas: emptyFn, | ||||||
| @@ -296,4 +354,22 @@ export const chartDBContext = createContext<ChartDBContext>({ | |||||||
|     removeDependencies: emptyFn, |     removeDependencies: emptyFn, | ||||||
|     addDependencies: emptyFn, |     addDependencies: emptyFn, | ||||||
|     updateDependency: emptyFn, |     updateDependency: emptyFn, | ||||||
|  |  | ||||||
|  |     // Area operations | ||||||
|  |     createArea: emptyFn, | ||||||
|  |     addArea: emptyFn, | ||||||
|  |     addAreas: emptyFn, | ||||||
|  |     getArea: emptyFn, | ||||||
|  |     removeArea: emptyFn, | ||||||
|  |     removeAreas: emptyFn, | ||||||
|  |     updateArea: emptyFn, | ||||||
|  |  | ||||||
|  |     // Custom type operations | ||||||
|  |     createCustomType: emptyFn, | ||||||
|  |     addCustomType: emptyFn, | ||||||
|  |     addCustomTypes: emptyFn, | ||||||
|  |     getCustomType: emptyFn, | ||||||
|  |     removeCustomType: emptyFn, | ||||||
|  |     removeCustomTypes: emptyFn, | ||||||
|  |     updateCustomType: emptyFn, | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -21,7 +21,14 @@ import { useLocalConfig } from '@/hooks/use-local-config'; | |||||||
| import { defaultSchemas } from '@/lib/data/default-schemas'; | import { defaultSchemas } from '@/lib/data/default-schemas'; | ||||||
| import { useEventEmitter } from 'ahooks'; | import { useEventEmitter } from 'ahooks'; | ||||||
| import type { DBDependency } from '@/lib/domain/db-dependency'; | import type { DBDependency } from '@/lib/domain/db-dependency'; | ||||||
|  | import type { Area } from '@/lib/domain/area'; | ||||||
| import { storageInitialValue } from '../storage-context/storage-context'; | import { storageInitialValue } from '../storage-context/storage-context'; | ||||||
|  | import { useDiff } from '../diff-context/use-diff'; | ||||||
|  | import type { DiffCalculatedEvent } from '../diff-context/diff-context'; | ||||||
|  | import { | ||||||
|  |     DBCustomTypeKind, | ||||||
|  |     type DBCustomType, | ||||||
|  | } from '@/lib/domain/db-custom-type'; | ||||||
|  |  | ||||||
| export interface ChartDBProviderProps { | export interface ChartDBProviderProps { | ||||||
|     diagram?: Diagram; |     diagram?: Diagram; | ||||||
| @@ -30,7 +37,8 @@ export interface ChartDBProviderProps { | |||||||
|  |  | ||||||
| export const ChartDBProvider: React.FC< | export const ChartDBProvider: React.FC< | ||||||
|     React.PropsWithChildren<ChartDBProviderProps> |     React.PropsWithChildren<ChartDBProviderProps> | ||||||
| > = ({ children, diagram, readonly }) => { | > = ({ children, diagram, readonly: readonlyProp }) => { | ||||||
|  |     const { hasDiff } = useDiff(); | ||||||
|     let db = useStorage(); |     let db = useStorage(); | ||||||
|     const events = useEventEmitter<ChartDBEvent>(); |     const events = useEventEmitter<ChartDBEvent>(); | ||||||
|     const { setSchemasFilter, schemasFilter } = useLocalConfig(); |     const { setSchemasFilter, schemasFilter } = useLocalConfig(); | ||||||
| @@ -53,9 +61,37 @@ export const ChartDBProvider: React.FC< | |||||||
|     const [dependencies, setDependencies] = useState<DBDependency[]>( |     const [dependencies, setDependencies] = useState<DBDependency[]>( | ||||||
|         diagram?.dependencies ?? [] |         diagram?.dependencies ?? [] | ||||||
|     ); |     ); | ||||||
|  |     const [areas, setAreas] = useState<Area[]>(diagram?.areas ?? []); | ||||||
|  |     const [customTypes, setCustomTypes] = useState<DBCustomType[]>( | ||||||
|  |         diagram?.customTypes ?? [] | ||||||
|  |     ); | ||||||
|  |     const { events: diffEvents } = useDiff(); | ||||||
|  |  | ||||||
|  |     const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => { | ||||||
|  |         const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data; | ||||||
|  |         setTables((tables) => | ||||||
|  |             [...tables, ...(tablesAdded ?? [])].map((table) => { | ||||||
|  |                 const fields = fieldsAdded.get(table.id); | ||||||
|  |                 return fields | ||||||
|  |                     ? { ...table, fields: [...table.fields, ...fields] } | ||||||
|  |                     : table; | ||||||
|  |             }) | ||||||
|  |         ); | ||||||
|  |         setRelationships((relationships) => [ | ||||||
|  |             ...relationships, | ||||||
|  |             ...(relationshipsAdded ?? []), | ||||||
|  |         ]); | ||||||
|  |     }, []); | ||||||
|  |  | ||||||
|  |     diffEvents.useSubscription(diffCalculatedHandler); | ||||||
|  |  | ||||||
|     const defaultSchemaName = defaultSchemas[databaseType]; |     const defaultSchemaName = defaultSchemas[databaseType]; | ||||||
|  |  | ||||||
|  |     const readonly = useMemo( | ||||||
|  |         () => readonlyProp ?? hasDiff ?? false, | ||||||
|  |         [readonlyProp, hasDiff] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     if (readonly) { |     if (readonly) { | ||||||
|         db = storageInitialValue; |         db = storageInitialValue; | ||||||
|     } |     } | ||||||
| @@ -125,6 +161,8 @@ export const ChartDBProvider: React.FC< | |||||||
|             tables, |             tables, | ||||||
|             relationships, |             relationships, | ||||||
|             dependencies, |             dependencies, | ||||||
|  |             areas, | ||||||
|  |             customTypes, | ||||||
|         }), |         }), | ||||||
|         [ |         [ | ||||||
|             diagramId, |             diagramId, | ||||||
| @@ -134,6 +172,8 @@ export const ChartDBProvider: React.FC< | |||||||
|             tables, |             tables, | ||||||
|             relationships, |             relationships, | ||||||
|             dependencies, |             dependencies, | ||||||
|  |             areas, | ||||||
|  |             customTypes, | ||||||
|             diagramCreatedAt, |             diagramCreatedAt, | ||||||
|             diagramUpdatedAt, |             diagramUpdatedAt, | ||||||
|         ] |         ] | ||||||
| @@ -145,6 +185,8 @@ export const ChartDBProvider: React.FC< | |||||||
|             setTables([]); |             setTables([]); | ||||||
|             setRelationships([]); |             setRelationships([]); | ||||||
|             setDependencies([]); |             setDependencies([]); | ||||||
|  |             setAreas([]); | ||||||
|  |             setCustomTypes([]); | ||||||
|             setDiagramUpdatedAt(updatedAt); |             setDiagramUpdatedAt(updatedAt); | ||||||
|  |  | ||||||
|             resetRedoStack(); |             resetRedoStack(); | ||||||
| @@ -155,6 +197,8 @@ export const ChartDBProvider: React.FC< | |||||||
|                 db.deleteDiagramTables(diagramId), |                 db.deleteDiagramTables(diagramId), | ||||||
|                 db.deleteDiagramRelationships(diagramId), |                 db.deleteDiagramRelationships(diagramId), | ||||||
|                 db.deleteDiagramDependencies(diagramId), |                 db.deleteDiagramDependencies(diagramId), | ||||||
|  |                 db.deleteDiagramAreas(diagramId), | ||||||
|  |                 db.deleteDiagramCustomTypes(diagramId), | ||||||
|             ]); |             ]); | ||||||
|         }, [db, diagramId, resetRedoStack, resetUndoStack]); |         }, [db, diagramId, resetRedoStack, resetUndoStack]); | ||||||
|  |  | ||||||
| @@ -167,6 +211,8 @@ export const ChartDBProvider: React.FC< | |||||||
|             setTables([]); |             setTables([]); | ||||||
|             setRelationships([]); |             setRelationships([]); | ||||||
|             setDependencies([]); |             setDependencies([]); | ||||||
|  |             setAreas([]); | ||||||
|  |             setCustomTypes([]); | ||||||
|             resetRedoStack(); |             resetRedoStack(); | ||||||
|             resetUndoStack(); |             resetUndoStack(); | ||||||
|  |  | ||||||
| @@ -175,6 +221,8 @@ export const ChartDBProvider: React.FC< | |||||||
|                 db.deleteDiagramRelationships(diagramId), |                 db.deleteDiagramRelationships(diagramId), | ||||||
|                 db.deleteDiagram(diagramId), |                 db.deleteDiagram(diagramId), | ||||||
|                 db.deleteDiagramDependencies(diagramId), |                 db.deleteDiagramDependencies(diagramId), | ||||||
|  |                 db.deleteDiagramAreas(diagramId), | ||||||
|  |                 db.deleteDiagramCustomTypes(diagramId), | ||||||
|             ]); |             ]); | ||||||
|         }, [db, diagramId, resetRedoStack, resetUndoStack]); |         }, [db, diagramId, resetRedoStack, resetUndoStack]); | ||||||
|  |  | ||||||
| @@ -1336,6 +1384,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'] = |     const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] = | ||||||
|         useCallback( |         useCallback( | ||||||
|             async (diagram) => { |             async (diagram) => { | ||||||
| @@ -1346,6 +1518,8 @@ export const ChartDBProvider: React.FC< | |||||||
|                 setTables(diagram?.tables ?? []); |                 setTables(diagram?.tables ?? []); | ||||||
|                 setRelationships(diagram?.relationships ?? []); |                 setRelationships(diagram?.relationships ?? []); | ||||||
|                 setDependencies(diagram?.dependencies ?? []); |                 setDependencies(diagram?.dependencies ?? []); | ||||||
|  |                 setAreas(diagram?.areas ?? []); | ||||||
|  |                 setCustomTypes(diagram?.customTypes ?? []); | ||||||
|                 setDiagramCreatedAt(diagram.createdAt); |                 setDiagramCreatedAt(diagram.createdAt); | ||||||
|                 setDiagramUpdatedAt(diagram.updatedAt); |                 setDiagramUpdatedAt(diagram.updatedAt); | ||||||
|  |  | ||||||
| @@ -1359,6 +1533,8 @@ export const ChartDBProvider: React.FC< | |||||||
|                 setTables, |                 setTables, | ||||||
|                 setRelationships, |                 setRelationships, | ||||||
|                 setDependencies, |                 setDependencies, | ||||||
|  |                 setAreas, | ||||||
|  |                 setCustomTypes, | ||||||
|                 setDiagramCreatedAt, |                 setDiagramCreatedAt, | ||||||
|                 setDiagramUpdatedAt, |                 setDiagramUpdatedAt, | ||||||
|                 events, |                 events, | ||||||
| @@ -1371,6 +1547,8 @@ export const ChartDBProvider: React.FC< | |||||||
|                 includeRelationships: true, |                 includeRelationships: true, | ||||||
|                 includeTables: true, |                 includeTables: true, | ||||||
|                 includeDependencies: true, |                 includeDependencies: true, | ||||||
|  |                 includeAreas: true, | ||||||
|  |                 includeCustomTypes: true, | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             if (diagram) { |             if (diagram) { | ||||||
| @@ -1382,6 +1560,150 @@ export const ChartDBProvider: React.FC< | |||||||
|         [db, loadDiagramFromData] |         [db, loadDiagramFromData] | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     // Custom type operations | ||||||
|  |     const getCustomType: ChartDBContext['getCustomType'] = useCallback( | ||||||
|  |         (id: string) => customTypes.find((type) => type.id === id) ?? null, | ||||||
|  |         [customTypes] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const addCustomTypes: ChartDBContext['addCustomTypes'] = useCallback( | ||||||
|  |         async ( | ||||||
|  |             customTypes: DBCustomType[], | ||||||
|  |             options = { updateHistory: true } | ||||||
|  |         ) => { | ||||||
|  |             setCustomTypes((currentTypes) => [...currentTypes, ...customTypes]); | ||||||
|  |             const updatedAt = new Date(); | ||||||
|  |             setDiagramUpdatedAt(updatedAt); | ||||||
|  |  | ||||||
|  |             await Promise.all([ | ||||||
|  |                 db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), | ||||||
|  |                 ...customTypes.map((customType) => | ||||||
|  |                     db.addCustomType({ diagramId, customType }) | ||||||
|  |                 ), | ||||||
|  |             ]); | ||||||
|  |  | ||||||
|  |             if (options.updateHistory) { | ||||||
|  |                 addUndoAction({ | ||||||
|  |                     action: 'addCustomTypes', | ||||||
|  |                     redoData: { customTypes }, | ||||||
|  |                     undoData: { customTypeIds: customTypes.map((t) => t.id) }, | ||||||
|  |                 }); | ||||||
|  |                 resetRedoStack(); | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         [db, diagramId, setCustomTypes, addUndoAction, resetRedoStack] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const addCustomType: ChartDBContext['addCustomType'] = useCallback( | ||||||
|  |         async (customType: DBCustomType, options = { updateHistory: true }) => { | ||||||
|  |             return addCustomTypes([customType], options); | ||||||
|  |         }, | ||||||
|  |         [addCustomTypes] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const createCustomType: ChartDBContext['createCustomType'] = useCallback( | ||||||
|  |         async (attributes) => { | ||||||
|  |             const customType: DBCustomType = { | ||||||
|  |                 id: generateId(), | ||||||
|  |                 name: `type_${customTypes.length + 1}`, | ||||||
|  |                 kind: DBCustomTypeKind.enum, | ||||||
|  |                 values: [], | ||||||
|  |                 fields: [], | ||||||
|  |                 ...attributes, | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             await addCustomType(customType); | ||||||
|  |             return customType; | ||||||
|  |         }, | ||||||
|  |         [addCustomType, customTypes] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const removeCustomTypes: ChartDBContext['removeCustomTypes'] = useCallback( | ||||||
|  |         async (ids, options = { updateHistory: true }) => { | ||||||
|  |             const typesToRemove = ids | ||||||
|  |                 .map((id) => getCustomType(id)) | ||||||
|  |                 .filter(Boolean) as DBCustomType[]; | ||||||
|  |  | ||||||
|  |             setCustomTypes((types) => | ||||||
|  |                 types.filter((type) => !ids.includes(type.id)) | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |             const updatedAt = new Date(); | ||||||
|  |             setDiagramUpdatedAt(updatedAt); | ||||||
|  |  | ||||||
|  |             await Promise.all([ | ||||||
|  |                 db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), | ||||||
|  |                 ...ids.map((id) => db.deleteCustomType({ diagramId, id })), | ||||||
|  |             ]); | ||||||
|  |  | ||||||
|  |             if (typesToRemove.length > 0 && options.updateHistory) { | ||||||
|  |                 addUndoAction({ | ||||||
|  |                     action: 'removeCustomTypes', | ||||||
|  |                     redoData: { | ||||||
|  |                         customTypeIds: ids, | ||||||
|  |                     }, | ||||||
|  |                     undoData: { | ||||||
|  |                         customTypes: typesToRemove, | ||||||
|  |                     }, | ||||||
|  |                 }); | ||||||
|  |                 resetRedoStack(); | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         [ | ||||||
|  |             db, | ||||||
|  |             diagramId, | ||||||
|  |             setCustomTypes, | ||||||
|  |             addUndoAction, | ||||||
|  |             resetRedoStack, | ||||||
|  |             getCustomType, | ||||||
|  |         ] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const removeCustomType: ChartDBContext['removeCustomType'] = useCallback( | ||||||
|  |         async (id: string, options = { updateHistory: true }) => { | ||||||
|  |             return removeCustomTypes([id], options); | ||||||
|  |         }, | ||||||
|  |         [removeCustomTypes] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const updateCustomType: ChartDBContext['updateCustomType'] = useCallback( | ||||||
|  |         async ( | ||||||
|  |             id: string, | ||||||
|  |             customType: Partial<DBCustomType>, | ||||||
|  |             options = { updateHistory: true } | ||||||
|  |         ) => { | ||||||
|  |             const prevCustomType = getCustomType(id); | ||||||
|  |             setCustomTypes((types) => | ||||||
|  |                 types.map((t) => (t.id === id ? { ...t, ...customType } : t)) | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |             const updatedAt = new Date(); | ||||||
|  |             setDiagramUpdatedAt(updatedAt); | ||||||
|  |  | ||||||
|  |             await Promise.all([ | ||||||
|  |                 db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), | ||||||
|  |                 db.updateCustomType({ id, attributes: customType }), | ||||||
|  |             ]); | ||||||
|  |  | ||||||
|  |             if (!!prevCustomType && options.updateHistory) { | ||||||
|  |                 addUndoAction({ | ||||||
|  |                     action: 'updateCustomType', | ||||||
|  |                     redoData: { customTypeId: id, customType }, | ||||||
|  |                     undoData: { customTypeId: id, customType: prevCustomType }, | ||||||
|  |                 }); | ||||||
|  |                 resetRedoStack(); | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         [ | ||||||
|  |             db, | ||||||
|  |             setCustomTypes, | ||||||
|  |             addUndoAction, | ||||||
|  |             resetRedoStack, | ||||||
|  |             getCustomType, | ||||||
|  |             diagramId, | ||||||
|  |         ] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <chartDBContext.Provider |         <chartDBContext.Provider | ||||||
|             value={{ |             value={{ | ||||||
| @@ -1391,6 +1713,7 @@ export const ChartDBProvider: React.FC< | |||||||
|                 tables, |                 tables, | ||||||
|                 relationships, |                 relationships, | ||||||
|                 dependencies, |                 dependencies, | ||||||
|  |                 areas, | ||||||
|                 currentDiagram, |                 currentDiagram, | ||||||
|                 schemas, |                 schemas, | ||||||
|                 filteredSchemas, |                 filteredSchemas, | ||||||
| @@ -1438,6 +1761,21 @@ export const ChartDBProvider: React.FC< | |||||||
|                 removeDependency, |                 removeDependency, | ||||||
|                 removeDependencies, |                 removeDependencies, | ||||||
|                 updateDependency, |                 updateDependency, | ||||||
|  |                 createArea, | ||||||
|  |                 addArea, | ||||||
|  |                 addAreas, | ||||||
|  |                 getArea, | ||||||
|  |                 removeArea, | ||||||
|  |                 removeAreas, | ||||||
|  |                 updateArea, | ||||||
|  |                 customTypes, | ||||||
|  |                 createCustomType, | ||||||
|  |                 addCustomType, | ||||||
|  |                 addCustomTypes, | ||||||
|  |                 getCustomType, | ||||||
|  |                 removeCustomType, | ||||||
|  |                 removeCustomTypes, | ||||||
|  |                 updateCustomType, | ||||||
|             }} |             }} | ||||||
|         > |         > | ||||||
|             {children} |             {children} | ||||||
|   | |||||||
| @@ -4,7 +4,10 @@ import type { ChartDBConfig } from '@/lib/domain/config'; | |||||||
|  |  | ||||||
| export interface ConfigContext { | export interface ConfigContext { | ||||||
|     config?: ChartDBConfig; |     config?: ChartDBConfig; | ||||||
|     updateConfig: (config: Partial<ChartDBConfig>) => Promise<void>; |     updateConfig: (params: { | ||||||
|  |         config?: Partial<ChartDBConfig>; | ||||||
|  |         updateFn?: (config: ChartDBConfig) => ChartDBConfig; | ||||||
|  |     }) => Promise<void>; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const ConfigContext = createContext<ConfigContext>({ | export const ConfigContext = createContext<ConfigContext>({ | ||||||
|   | |||||||
| @@ -19,15 +19,29 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|         loadConfig(); |         loadConfig(); | ||||||
|     }, [getConfig]); |     }, [getConfig]); | ||||||
|  |  | ||||||
|     const updateConfig: ConfigContext['updateConfig'] = async ( |     const updateConfig: ConfigContext['updateConfig'] = async ({ | ||||||
|         config: Partial<ChartDBConfig> |         config, | ||||||
|     ) => { |         updateFn, | ||||||
|         await updateDataConfig(config); |     }) => { | ||||||
|         setConfig((prevConfig) => |         const promise = new Promise<void>((resolve) => { | ||||||
|             prevConfig |             setConfig((prevConfig) => { | ||||||
|                 ? { ...prevConfig, ...config } |                 let baseConfig: ChartDBConfig = { defaultDiagramId: '' }; | ||||||
|                 : { ...{ defaultDiagramId: '' }, ...config } |                 if (prevConfig) { | ||||||
|         ); |                     baseConfig = prevConfig; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 const updatedConfig = updateFn | ||||||
|  |                     ? updateFn(baseConfig) | ||||||
|  |                     : { ...baseConfig, ...config }; | ||||||
|  |  | ||||||
|  |                 updateDataConfig(updatedConfig).then(() => { | ||||||
|  |                     resolve(); | ||||||
|  |                 }); | ||||||
|  |                 return updatedConfig; | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         return promise; | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|   | |||||||
| @@ -6,14 +6,22 @@ import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sq | |||||||
| import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog'; | import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog'; | ||||||
| import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog'; | import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog'; | ||||||
| import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/import-diagram-dialog'; | import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/import-diagram-dialog'; | ||||||
|  | import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog'; | ||||||
|  | import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog'; | ||||||
|  | import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog'; | ||||||
|  | import type { CreateDiagramDialogProps } from '@/dialogs/create-diagram-dialog/create-diagram-dialog'; | ||||||
|  |  | ||||||
| export interface DialogContext { | export interface DialogContext { | ||||||
|     // Create diagram dialog |     // Create diagram dialog | ||||||
|     openCreateDiagramDialog: () => void; |     openCreateDiagramDialog: ( | ||||||
|  |         params?: Omit<CreateDiagramDialogProps, 'dialog'> | ||||||
|  |     ) => void; | ||||||
|     closeCreateDiagramDialog: () => void; |     closeCreateDiagramDialog: () => void; | ||||||
|  |  | ||||||
|     // Open diagram dialog |     // Open diagram dialog | ||||||
|     openOpenDiagramDialog: () => void; |     openOpenDiagramDialog: ( | ||||||
|  |         params?: Omit<OpenDiagramDialogProps, 'dialog'> | ||||||
|  |     ) => void; | ||||||
|     closeOpenDiagramDialog: () => void; |     closeOpenDiagramDialog: () => void; | ||||||
|  |  | ||||||
|     // Export SQL dialog |     // Export SQL dialog | ||||||
| @@ -21,7 +29,9 @@ export interface DialogContext { | |||||||
|     closeExportSQLDialog: () => void; |     closeExportSQLDialog: () => void; | ||||||
|  |  | ||||||
|     // Create relationship dialog |     // Create relationship dialog | ||||||
|     openCreateRelationshipDialog: () => void; |     openCreateRelationshipDialog: ( | ||||||
|  |         params?: Omit<CreateRelationshipDialogProps, 'dialog'> | ||||||
|  |     ) => void; | ||||||
|     closeCreateRelationshipDialog: () => void; |     closeCreateRelationshipDialog: () => void; | ||||||
|  |  | ||||||
|     // Import database dialog |     // Import database dialog | ||||||
| @@ -40,10 +50,6 @@ export interface DialogContext { | |||||||
|     openStarUsDialog: () => void; |     openStarUsDialog: () => void; | ||||||
|     closeStarUsDialog: () => void; |     closeStarUsDialog: () => void; | ||||||
|  |  | ||||||
|     // Buckle dialog |  | ||||||
|     openBuckleDialog: () => void; |  | ||||||
|     closeBuckleDialog: () => void; |  | ||||||
|  |  | ||||||
|     // Export image dialog |     // Export image dialog | ||||||
|     openExportImageDialog: ( |     openExportImageDialog: ( | ||||||
|         params: Omit<ExportImageDialogProps, 'dialog'> |         params: Omit<ExportImageDialogProps, 'dialog'> | ||||||
| @@ -61,6 +67,12 @@ export interface DialogContext { | |||||||
|         params: Omit<ImportDiagramDialogProps, 'dialog'> |         params: Omit<ImportDiagramDialogProps, 'dialog'> | ||||||
|     ) => void; |     ) => void; | ||||||
|     closeImportDiagramDialog: () => void; |     closeImportDiagramDialog: () => void; | ||||||
|  |  | ||||||
|  |     // Import DBML dialog | ||||||
|  |     openImportDBMLDialog: ( | ||||||
|  |         params?: Omit<ImportDBMLDialogProps, 'dialog'> | ||||||
|  |     ) => void; | ||||||
|  |     closeImportDBMLDialog: () => void; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const dialogContext = createContext<DialogContext>({ | export const dialogContext = createContext<DialogContext>({ | ||||||
| @@ -84,6 +96,6 @@ export const dialogContext = createContext<DialogContext>({ | |||||||
|     closeExportDiagramDialog: emptyFn, |     closeExportDiagramDialog: emptyFn, | ||||||
|     openImportDiagramDialog: emptyFn, |     openImportDiagramDialog: emptyFn, | ||||||
|     closeImportDiagramDialog: emptyFn, |     closeImportDiagramDialog: emptyFn, | ||||||
|     openBuckleDialog: emptyFn, |     openImportDBMLDialog: emptyFn, | ||||||
|     closeBuckleDialog: emptyFn, |     closeImportDBMLDialog: emptyFn, | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -1,11 +1,14 @@ | |||||||
| import React, { useCallback, useState } from 'react'; | import React, { useCallback, useState } from 'react'; | ||||||
| import type { DialogContext } from './dialog-context'; | import type { DialogContext } from './dialog-context'; | ||||||
| import { 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 { CreateDiagramDialog } from '@/dialogs/create-diagram-dialog/create-diagram-dialog'; | ||||||
|  | import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog'; | ||||||
| import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog'; | import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog'; | ||||||
| import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog'; | import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog'; | ||||||
| import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog'; | import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog'; | ||||||
| import { DatabaseType } from '@/lib/domain/database-type'; | import { DatabaseType } from '@/lib/domain/database-type'; | ||||||
|  | import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog'; | ||||||
| import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog'; | import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog'; | ||||||
| import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog'; | import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog'; | ||||||
| import { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog'; | import { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog'; | ||||||
| @@ -17,18 +20,51 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor | |||||||
| import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog'; | import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog'; | ||||||
| import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog'; | import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog'; | ||||||
| import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog'; | import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog'; | ||||||
| import { BuckleDialog } from '@/dialogs/buckle-dialog/buckle-dialog'; | import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog'; | ||||||
|  | import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog'; | ||||||
|  |  | ||||||
| export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | ||||||
|     children, |     children, | ||||||
| }) => { | }) => { | ||||||
|     const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false); |     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 [openOpenDiagramDialog, setOpenOpenDiagramDialog] = useState(false); | ||||||
|  |     const [openDiagramDialogParams, setOpenDiagramDialogParams] = | ||||||
|  |         useState<Omit<OpenDiagramDialogProps, 'dialog'>>(); | ||||||
|  |  | ||||||
|  |     const openOpenDiagramDialogHandler: DialogContext['openOpenDiagramDialog'] = | ||||||
|  |         useCallback( | ||||||
|  |             (props) => { | ||||||
|  |                 setOpenDiagramDialogParams(props); | ||||||
|  |                 setOpenOpenDiagramDialog(true); | ||||||
|  |             }, | ||||||
|  |             [setOpenOpenDiagramDialog] | ||||||
|  |         ); | ||||||
|  |  | ||||||
|     const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] = |     const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] = | ||||||
|         useState(false); |         useState(false); | ||||||
|  |     const [createRelationshipDialogParams, setCreateRelationshipDialogParams] = | ||||||
|  |         useState<Omit<CreateRelationshipDialogProps, 'dialog'>>(); | ||||||
|  |     const openCreateRelationshipDialogHandler: DialogContext['openCreateRelationshipDialog'] = | ||||||
|  |         useCallback( | ||||||
|  |             (params) => { | ||||||
|  |                 setCreateRelationshipDialogParams(params); | ||||||
|  |                 setOpenCreateRelationshipDialog(true); | ||||||
|  |             }, | ||||||
|  |             [setOpenCreateRelationshipDialog] | ||||||
|  |         ); | ||||||
|  |  | ||||||
|     const [openStarUsDialog, setOpenStarUsDialog] = useState(false); |     const [openStarUsDialog, setOpenStarUsDialog] = useState(false); | ||||||
|     const [openBuckleDialog, setOpenBuckleDialog] = useState(false); |  | ||||||
|  |  | ||||||
|     // Export image dialog |     // Export image dialog | ||||||
|     const [openExportImageDialog, setOpenExportImageDialog] = useState(false); |     const [openExportImageDialog, setOpenExportImageDialog] = useState(false); | ||||||
| @@ -88,7 +124,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             [setOpenTableSchemaDialog] |             [setOpenTableSchemaDialog] | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|     // Export image dialog |     // Export diagram dialog | ||||||
|     const [openExportDiagramDialog, setOpenExportDiagramDialog] = |     const [openExportDiagramDialog, setOpenExportDiagramDialog] = | ||||||
|         useState(false); |         useState(false); | ||||||
|  |  | ||||||
| @@ -96,17 +132,22 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|     const [openImportDiagramDialog, setOpenImportDiagramDialog] = |     const [openImportDiagramDialog, setOpenImportDiagramDialog] = | ||||||
|         useState(false); |         useState(false); | ||||||
|  |  | ||||||
|  |     // Import DBML dialog | ||||||
|  |     const [openImportDBMLDialog, setOpenImportDBMLDialog] = useState(false); | ||||||
|  |     const [importDBMLDialogParams, setImportDBMLDialogParams] = | ||||||
|  |         useState<Omit<ImportDBMLDialogProps, 'dialog'>>(); | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <dialogContext.Provider |         <dialogContext.Provider | ||||||
|             value={{ |             value={{ | ||||||
|                 openCreateDiagramDialog: () => setOpenNewDiagramDialog(true), |                 openCreateDiagramDialog: openNewDiagramDialogHandler, | ||||||
|                 closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false), |                 closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false), | ||||||
|                 openOpenDiagramDialog: () => setOpenOpenDiagramDialog(true), |                 openOpenDiagramDialog: openOpenDiagramDialogHandler, | ||||||
|                 closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false), |                 closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false), | ||||||
|                 openExportSQLDialog: openExportSQLDialogHandler, |                 openExportSQLDialog: openExportSQLDialogHandler, | ||||||
|                 closeExportSQLDialog: () => setOpenExportSQLDialog(false), |                 closeExportSQLDialog: () => setOpenExportSQLDialog(false), | ||||||
|                 openCreateRelationshipDialog: () => |                 openCreateRelationshipDialog: | ||||||
|                     setOpenCreateRelationshipDialog(true), |                     openCreateRelationshipDialogHandler, | ||||||
|                 closeCreateRelationshipDialog: () => |                 closeCreateRelationshipDialog: () => | ||||||
|                     setOpenCreateRelationshipDialog(false), |                     setOpenCreateRelationshipDialog(false), | ||||||
|                 openImportDatabaseDialog: openImportDatabaseDialogHandler, |                 openImportDatabaseDialog: openImportDatabaseDialogHandler, | ||||||
| @@ -116,8 +157,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|                 closeTableSchemaDialog: () => setOpenTableSchemaDialog(false), |                 closeTableSchemaDialog: () => setOpenTableSchemaDialog(false), | ||||||
|                 openStarUsDialog: () => setOpenStarUsDialog(true), |                 openStarUsDialog: () => setOpenStarUsDialog(true), | ||||||
|                 closeStarUsDialog: () => setOpenStarUsDialog(false), |                 closeStarUsDialog: () => setOpenStarUsDialog(false), | ||||||
|                 closeBuckleDialog: () => setOpenBuckleDialog(false), |  | ||||||
|                 openBuckleDialog: () => setOpenBuckleDialog(true), |  | ||||||
|                 closeExportImageDialog: () => setOpenExportImageDialog(false), |                 closeExportImageDialog: () => setOpenExportImageDialog(false), | ||||||
|                 openExportImageDialog: openExportImageDialogHandler, |                 openExportImageDialog: openExportImageDialogHandler, | ||||||
|                 openExportDiagramDialog: () => setOpenExportDiagramDialog(true), |                 openExportDiagramDialog: () => setOpenExportDiagramDialog(true), | ||||||
| @@ -126,17 +165,29 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|                 openImportDiagramDialog: () => setOpenImportDiagramDialog(true), |                 openImportDiagramDialog: () => setOpenImportDiagramDialog(true), | ||||||
|                 closeImportDiagramDialog: () => |                 closeImportDiagramDialog: () => | ||||||
|                     setOpenImportDiagramDialog(false), |                     setOpenImportDiagramDialog(false), | ||||||
|  |                 openImportDBMLDialog: (params) => { | ||||||
|  |                     setImportDBMLDialogParams(params); | ||||||
|  |                     setOpenImportDBMLDialog(true); | ||||||
|  |                 }, | ||||||
|  |                 closeImportDBMLDialog: () => setOpenImportDBMLDialog(false), | ||||||
|             }} |             }} | ||||||
|         > |         > | ||||||
|             {children} |             {children} | ||||||
|             <CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} /> |             <CreateDiagramDialog | ||||||
|             <OpenDiagramDialog dialog={{ open: openOpenDiagramDialog }} /> |                 dialog={{ open: openNewDiagramDialog }} | ||||||
|  |                 {...newDiagramDialogParams} | ||||||
|  |             /> | ||||||
|  |             <OpenDiagramDialog | ||||||
|  |                 dialog={{ open: openOpenDiagramDialog }} | ||||||
|  |                 {...openDiagramDialogParams} | ||||||
|  |             /> | ||||||
|             <ExportSQLDialog |             <ExportSQLDialog | ||||||
|                 dialog={{ open: openExportSQLDialog }} |                 dialog={{ open: openExportSQLDialog }} | ||||||
|                 {...exportSQLDialogParams} |                 {...exportSQLDialogParams} | ||||||
|             /> |             /> | ||||||
|             <CreateRelationshipDialog |             <CreateRelationshipDialog | ||||||
|                 dialog={{ open: openCreateRelationshipDialog }} |                 dialog={{ open: openCreateRelationshipDialog }} | ||||||
|  |                 {...createRelationshipDialogParams} | ||||||
|             /> |             /> | ||||||
|             <ImportDatabaseDialog |             <ImportDatabaseDialog | ||||||
|                 dialog={{ open: openImportDatabaseDialog }} |                 dialog={{ open: openImportDatabaseDialog }} | ||||||
| @@ -153,7 +204,10 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             /> |             /> | ||||||
|             <ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} /> |             <ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} /> | ||||||
|             <ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} /> |             <ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} /> | ||||||
|             <BuckleDialog dialog={{ open: openBuckleDialog }} /> |             <ImportDBMLDialog | ||||||
|  |                 dialog={{ open: openImportDBMLDialog }} | ||||||
|  |                 {...importDBMLDialogParams} | ||||||
|  |             /> | ||||||
|         </dialogContext.Provider> |         </dialogContext.Provider> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										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 type ImageType = 'png' | 'jpeg' | 'svg'; | ||||||
| export interface ExportImageContext { | 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>({ | export const exportImageContext = createContext<ExportImageContext>({ | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import React, { useCallback, useMemo } from 'react'; | import React, { useCallback, useMemo, useEffect, useState } from 'react'; | ||||||
| import type { ExportImageContext, ImageType } from './export-image-context'; | import type { ExportImageContext, ImageType } from './export-image-context'; | ||||||
| import { exportImageContext } from './export-image-context'; | import { exportImageContext } from './export-image-context'; | ||||||
| import { toJpeg, toPng, toSvg } from 'html-to-image'; | import { toJpeg, toPng, toSvg } from 'html-to-image'; | ||||||
| @@ -6,6 +6,9 @@ import { useReactFlow } from '@xyflow/react'; | |||||||
| import { useChartDB } from '@/hooks/use-chartdb'; | import { useChartDB } from '@/hooks/use-chartdb'; | ||||||
| import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner'; | import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner'; | ||||||
| import { useTheme } from '@/hooks/use-theme'; | 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> = ({ | export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | ||||||
|     children, |     children, | ||||||
| @@ -14,6 +17,24 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|     const { setNodes, getViewport } = useReactFlow(); |     const { setNodes, getViewport } = useReactFlow(); | ||||||
|     const { effectiveTheme } = useTheme(); |     const { effectiveTheme } = useTheme(); | ||||||
|     const { diagramName } = useChartDB(); |     const { diagramName } = useChartDB(); | ||||||
|  |     const [logoBase64, setLogoBase64] = useState<string>(''); | ||||||
|  |  | ||||||
|  |     useEffect(() => { | ||||||
|  |         // Convert logo to base64 on component mount | ||||||
|  |         const img = new Image(); | ||||||
|  |         img.src = effectiveTheme === 'light' ? logoLight : logoDark; | ||||||
|  |         img.onload = () => { | ||||||
|  |             const canvas = document.createElement('canvas'); | ||||||
|  |             canvas.width = img.width; | ||||||
|  |             canvas.height = img.height; | ||||||
|  |             const ctx = canvas.getContext('2d'); | ||||||
|  |             if (ctx) { | ||||||
|  |                 ctx.drawImage(img, 0, 0); | ||||||
|  |                 const base64 = canvas.toDataURL('image/png'); | ||||||
|  |                 setLogoBase64(base64); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |     }, [effectiveTheme]); | ||||||
|  |  | ||||||
|     const downloadImage = useCallback( |     const downloadImage = useCallback( | ||||||
|         (dataUrl: string, type: ImageType) => { |         (dataUrl: string, type: ImageType) => { | ||||||
| @@ -37,8 +58,16 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|         [] |         [] | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     const getBackgroundColor = useCallback( | ||||||
|  |         (theme: EffectiveTheme, transparent: boolean): string => { | ||||||
|  |             if (transparent) return 'transparent'; | ||||||
|  |             return theme === 'light' ? '#ffffff' : '#141414'; | ||||||
|  |         }, | ||||||
|  |         [] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     const exportImage: ExportImageContext['exportImage'] = useCallback( |     const exportImage: ExportImageContext['exportImage'] = useCallback( | ||||||
|         async (type, scale = 1) => { |         async (type, { includePatternBG, transparent, scale }) => { | ||||||
|             showLoader({ |             showLoader({ | ||||||
|                 animated: false, |                 animated: false, | ||||||
|             }); |             }); | ||||||
| @@ -94,50 +123,59 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|                     defs.innerHTML = markerDefs.innerHTML; |                     defs.innerHTML = markerDefs.innerHTML; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 const pattern = document.createElementNS( |                 if (includePatternBG) { | ||||||
|                     'http://www.w3.org/2000/svg', |                     const pattern = document.createElementNS( | ||||||
|                     'pattern' |                         'http://www.w3.org/2000/svg', | ||||||
|                 ); |                         'pattern' | ||||||
|                 pattern.setAttribute('id', 'background-pattern'); |                     ); | ||||||
|                 pattern.setAttribute('width', String(16 * viewport.zoom)); |                     pattern.setAttribute('id', 'background-pattern'); | ||||||
|                 pattern.setAttribute('height', String(16 * viewport.zoom)); |                     pattern.setAttribute('width', String(16 * viewport.zoom)); | ||||||
|                 pattern.setAttribute('patternUnits', 'userSpaceOnUse'); |                     pattern.setAttribute('height', String(16 * viewport.zoom)); | ||||||
|                 pattern.setAttribute( |                     pattern.setAttribute('patternUnits', 'userSpaceOnUse'); | ||||||
|                     'patternTransform', |                     pattern.setAttribute( | ||||||
|                     `translate(${viewport.x % (16 * viewport.zoom)} ${viewport.y % (16 * viewport.zoom)})` |                         'patternTransform', | ||||||
|                 ); |                         `translate(${viewport.x % (16 * viewport.zoom)} ${viewport.y % (16 * viewport.zoom)})` | ||||||
|  |                     ); | ||||||
|  |  | ||||||
|                 const dot = document.createElementNS( |                     const dot = document.createElementNS( | ||||||
|                     'http://www.w3.org/2000/svg', |                         'http://www.w3.org/2000/svg', | ||||||
|                     'circle' |                         'circle' | ||||||
|                 ); |                     ); | ||||||
|  |  | ||||||
|                 const dotSize = viewport.zoom * 0.5; |                     const dotSize = viewport.zoom * 0.5; | ||||||
|                 dot.setAttribute('cx', String(viewport.zoom)); |                     dot.setAttribute('cx', String(viewport.zoom)); | ||||||
|                 dot.setAttribute('cy', String(viewport.zoom)); |                     dot.setAttribute('cy', String(viewport.zoom)); | ||||||
|                 dot.setAttribute('r', String(dotSize)); |                     dot.setAttribute('r', String(dotSize)); | ||||||
|                 const dotColor = |                     const dotColor = | ||||||
|                     effectiveTheme === 'light' ? '#92939C' : '#777777'; |                         effectiveTheme === 'light' ? '#92939C' : '#777777'; | ||||||
|                 dot.setAttribute('fill', dotColor); |                     dot.setAttribute('fill', dotColor); | ||||||
|  |  | ||||||
|  |                     pattern.appendChild(dot); | ||||||
|  |                     defs.appendChild(pattern); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 pattern.appendChild(dot); |  | ||||||
|                 defs.appendChild(pattern); |  | ||||||
|                 tempSvg.appendChild(defs); |                 tempSvg.appendChild(defs); | ||||||
|  |  | ||||||
|                 const backgroundRect = document.createElementNS( |                 const backgroundRect = document.createElementNS( | ||||||
|                     'http://www.w3.org/2000/svg', |                     'http://www.w3.org/2000/svg', | ||||||
|                     'rect' |                     'rect' | ||||||
|                 ); |                 ); | ||||||
|                 const padding = 2000; |                 const bgPadding = 2000; | ||||||
|                 backgroundRect.setAttribute('x', String(-viewport.x - padding)); |                 backgroundRect.setAttribute( | ||||||
|                 backgroundRect.setAttribute('y', String(-viewport.y - padding)); |                     'x', | ||||||
|  |                     String(-viewport.x - bgPadding) | ||||||
|  |                 ); | ||||||
|  |                 backgroundRect.setAttribute( | ||||||
|  |                     'y', | ||||||
|  |                     String(-viewport.y - bgPadding) | ||||||
|  |                 ); | ||||||
|                 backgroundRect.setAttribute( |                 backgroundRect.setAttribute( | ||||||
|                     'width', |                     'width', | ||||||
|                     String(reactFlowBounds.width + 2 * padding) |                     String(reactFlowBounds.width + 2 * bgPadding) | ||||||
|                 ); |                 ); | ||||||
|                 backgroundRect.setAttribute( |                 backgroundRect.setAttribute( | ||||||
|                     'height', |                     'height', | ||||||
|                     String(reactFlowBounds.height + 2 * padding) |                     String(reactFlowBounds.height + 2 * bgPadding) | ||||||
|                 ); |                 ); | ||||||
|                 backgroundRect.setAttribute('fill', 'url(#background-pattern)'); |                 backgroundRect.setAttribute('fill', 'url(#background-pattern)'); | ||||||
|                 tempSvg.appendChild(backgroundRect); |                 tempSvg.appendChild(backgroundRect); | ||||||
| @@ -148,27 +186,110 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|                 ); |                 ); | ||||||
|  |  | ||||||
|                 try { |                 try { | ||||||
|                     const dataUrl = await imageCreateFn(viewportElement, { |                     // Handle SVG export differently | ||||||
|                         ...(type === 'jpeg' || type === 'png' |                     if (type === 'svg') { | ||||||
|                             ? { |                         const dataUrl = await imageCreateFn(viewportElement, { | ||||||
|                                   backgroundColor: |                             width: reactFlowBounds.width, | ||||||
|                                       effectiveTheme === 'light' |                             height: reactFlowBounds.height, | ||||||
|                                           ? '#ffffff' |                             style: { | ||||||
|                                           : '#141414', |                                 width: `${reactFlowBounds.width}px`, | ||||||
|                               } |                                 height: `${reactFlowBounds.height}px`, | ||||||
|                             : {}), |                                 transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`, | ||||||
|                         width: reactFlowBounds.width, |                             }, | ||||||
|                         height: reactFlowBounds.height, |                             quality: 1, | ||||||
|                         style: { |                             pixelRatio: scale, | ||||||
|                             width: `${reactFlowBounds.width}px`, |                             skipFonts: true, | ||||||
|                             height: `${reactFlowBounds.height}px`, |                         }); | ||||||
|                             transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`, |                         downloadImage(dataUrl, type); | ||||||
|                         }, |                         return; | ||||||
|                         quality: 1, |                     } | ||||||
|                         pixelRatio: scale, |  | ||||||
|                     }); |  | ||||||
|  |  | ||||||
|                     downloadImage(dataUrl, type); |                     // For PNG and JPEG, continue with the watermark process | ||||||
|  |                     const initialDataUrl = await imageCreateFn( | ||||||
|  |                         viewportElement, | ||||||
|  |                         { | ||||||
|  |                             backgroundColor: getBackgroundColor( | ||||||
|  |                                 effectiveTheme, | ||||||
|  |                                 transparent | ||||||
|  |                             ), | ||||||
|  |                             width: reactFlowBounds.width, | ||||||
|  |                             height: reactFlowBounds.height, | ||||||
|  |                             style: { | ||||||
|  |                                 width: `${reactFlowBounds.width}px`, | ||||||
|  |                                 height: `${reactFlowBounds.height}px`, | ||||||
|  |                                 transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`, | ||||||
|  |                             }, | ||||||
|  |                             quality: 1, | ||||||
|  |                             pixelRatio: scale, | ||||||
|  |                             skipFonts: true, | ||||||
|  |                         } | ||||||
|  |                     ); | ||||||
|  |  | ||||||
|  |                     // Create a canvas to combine the diagram and watermark | ||||||
|  |                     const canvas = document.createElement('canvas'); | ||||||
|  |                     const ctx = canvas.getContext('2d'); | ||||||
|  |  | ||||||
|  |                     if (!ctx) { | ||||||
|  |                         downloadImage(initialDataUrl, type); | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     // Set canvas size to match the export size | ||||||
|  |                     canvas.width = reactFlowBounds.width * scale; | ||||||
|  |                     canvas.height = reactFlowBounds.height * scale; | ||||||
|  |  | ||||||
|  |                     // Load the exported diagram | ||||||
|  |                     const diagramImage = new Image(); | ||||||
|  |                     diagramImage.src = initialDataUrl; | ||||||
|  |  | ||||||
|  |                     await new Promise((resolve) => { | ||||||
|  |                         diagramImage.onload = async () => { | ||||||
|  |                             // Draw the diagram | ||||||
|  |                             ctx.drawImage(diagramImage, 0, 0); | ||||||
|  |  | ||||||
|  |                             // Calculate logo size | ||||||
|  |                             const logoHeight = Math.max( | ||||||
|  |                                 24, | ||||||
|  |                                 Math.floor(canvas.width * 0.024) | ||||||
|  |                             ); | ||||||
|  |                             const padding = Math.max( | ||||||
|  |                                 12, | ||||||
|  |                                 Math.floor(logoHeight * 0.5) | ||||||
|  |                             ); | ||||||
|  |  | ||||||
|  |                             // Load and draw the logo | ||||||
|  |                             const logoImage = new Image(); | ||||||
|  |                             logoImage.src = logoBase64; | ||||||
|  |  | ||||||
|  |                             await new Promise((resolve) => { | ||||||
|  |                                 logoImage.onload = () => { | ||||||
|  |                                     // Calculate logo width while maintaining aspect ratio | ||||||
|  |                                     const logoWidth = | ||||||
|  |                                         (logoImage.width / logoImage.height) * | ||||||
|  |                                         logoHeight; | ||||||
|  |  | ||||||
|  |                                     // Draw logo in bottom-left corner | ||||||
|  |                                     ctx.globalAlpha = 0.9; | ||||||
|  |                                     ctx.drawImage( | ||||||
|  |                                         logoImage, | ||||||
|  |                                         padding, | ||||||
|  |                                         canvas.height - logoHeight - padding, | ||||||
|  |                                         logoWidth, | ||||||
|  |                                         logoHeight | ||||||
|  |                                     ); | ||||||
|  |                                     ctx.globalAlpha = 1; | ||||||
|  |                                     resolve(null); | ||||||
|  |                                 }; | ||||||
|  |                             }); | ||||||
|  |  | ||||||
|  |                             // Convert canvas to data URL | ||||||
|  |                             const finalDataUrl = canvas.toDataURL( | ||||||
|  |                                 type === 'png' ? 'image/png' : 'image/jpeg' | ||||||
|  |                             ); | ||||||
|  |                             downloadImage(finalDataUrl, type); | ||||||
|  |                             resolve(null); | ||||||
|  |                         }; | ||||||
|  |                     }); | ||||||
|                 } finally { |                 } finally { | ||||||
|                     viewportElement.removeChild(tempSvg); |                     viewportElement.removeChild(tempSvg); | ||||||
|                     hideLoader(); |                     hideLoader(); | ||||||
| @@ -176,6 +297,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             }, 0); |             }, 0); | ||||||
|         }, |         }, | ||||||
|         [ |         [ | ||||||
|  |             getBackgroundColor, | ||||||
|             downloadImage, |             downloadImage, | ||||||
|             getViewport, |             getViewport, | ||||||
|             hideLoader, |             hideLoader, | ||||||
| @@ -183,6 +305,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             setNodes, |             setNodes, | ||||||
|             showLoader, |             showLoader, | ||||||
|             effectiveTheme, |             effectiveTheme, | ||||||
|  |             logoBase64, | ||||||
|         ] |         ] | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -33,6 +33,12 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|         removeIndex, |         removeIndex, | ||||||
|         updateIndex, |         updateIndex, | ||||||
|         removeRelationships, |         removeRelationships, | ||||||
|  |         addAreas, | ||||||
|  |         removeAreas, | ||||||
|  |         updateArea, | ||||||
|  |         addCustomTypes, | ||||||
|  |         removeCustomTypes, | ||||||
|  |         updateCustomType, | ||||||
|     } = useChartDB(); |     } = useChartDB(); | ||||||
|  |  | ||||||
|     const redoActionHandlers = useMemo( |     const redoActionHandlers = useMemo( | ||||||
| @@ -107,6 +113,28 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|                     updateHistory: false, |                     updateHistory: false, | ||||||
|                 }); |                 }); | ||||||
|             }, |             }, | ||||||
|  |             addAreas: ({ redoData: { areas } }) => { | ||||||
|  |                 return addAreas(areas, { updateHistory: false }); | ||||||
|  |             }, | ||||||
|  |             removeAreas: ({ redoData: { areaIds } }) => { | ||||||
|  |                 return removeAreas(areaIds, { updateHistory: false }); | ||||||
|  |             }, | ||||||
|  |             updateArea: ({ redoData: { areaId, area } }) => { | ||||||
|  |                 return updateArea(areaId, area, { updateHistory: false }); | ||||||
|  |             }, | ||||||
|  |             addCustomTypes: ({ redoData: { customTypes } }) => { | ||||||
|  |                 return addCustomTypes(customTypes, { updateHistory: false }); | ||||||
|  |             }, | ||||||
|  |             removeCustomTypes: ({ redoData: { customTypeIds } }) => { | ||||||
|  |                 return removeCustomTypes(customTypeIds, { | ||||||
|  |                     updateHistory: false, | ||||||
|  |                 }); | ||||||
|  |             }, | ||||||
|  |             updateCustomType: ({ redoData: { customTypeId, customType } }) => { | ||||||
|  |                 return updateCustomType(customTypeId, customType, { | ||||||
|  |                     updateHistory: false, | ||||||
|  |                 }); | ||||||
|  |             }, | ||||||
|         }), |         }), | ||||||
|         [ |         [ | ||||||
|             addTables, |             addTables, | ||||||
| @@ -126,6 +154,12 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             addDependencies, |             addDependencies, | ||||||
|             removeDependencies, |             removeDependencies, | ||||||
|             updateDependency, |             updateDependency, | ||||||
|  |             addAreas, | ||||||
|  |             removeAreas, | ||||||
|  |             updateArea, | ||||||
|  |             addCustomTypes, | ||||||
|  |             removeCustomTypes, | ||||||
|  |             updateCustomType, | ||||||
|         ] |         ] | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
| @@ -215,6 +249,28 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|                     updateHistory: false, |                     updateHistory: false, | ||||||
|                 }); |                 }); | ||||||
|             }, |             }, | ||||||
|  |             addAreas: ({ undoData: { areaIds } }) => { | ||||||
|  |                 return removeAreas(areaIds, { updateHistory: false }); | ||||||
|  |             }, | ||||||
|  |             removeAreas: ({ undoData: { areas } }) => { | ||||||
|  |                 return addAreas(areas, { updateHistory: false }); | ||||||
|  |             }, | ||||||
|  |             updateArea: ({ undoData: { areaId, area } }) => { | ||||||
|  |                 return updateArea(areaId, area, { updateHistory: false }); | ||||||
|  |             }, | ||||||
|  |             addCustomTypes: ({ undoData: { customTypeIds } }) => { | ||||||
|  |                 return removeCustomTypes(customTypeIds, { | ||||||
|  |                     updateHistory: false, | ||||||
|  |                 }); | ||||||
|  |             }, | ||||||
|  |             removeCustomTypes: ({ undoData: { customTypes } }) => { | ||||||
|  |                 return addCustomTypes(customTypes, { updateHistory: false }); | ||||||
|  |             }, | ||||||
|  |             updateCustomType: ({ undoData: { customTypeId, customType } }) => { | ||||||
|  |                 return updateCustomType(customTypeId, customType, { | ||||||
|  |                     updateHistory: false, | ||||||
|  |                 }); | ||||||
|  |             }, | ||||||
|         }), |         }), | ||||||
|         [ |         [ | ||||||
|             addTables, |             addTables, | ||||||
| @@ -234,6 +290,12 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             addDependencies, |             addDependencies, | ||||||
|             removeDependencies, |             removeDependencies, | ||||||
|             updateDependency, |             updateDependency, | ||||||
|  |             addAreas, | ||||||
|  |             removeAreas, | ||||||
|  |             updateArea, | ||||||
|  |             addCustomTypes, | ||||||
|  |             removeCustomTypes, | ||||||
|  |             updateCustomType, | ||||||
|         ] |         ] | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,6 +4,8 @@ import type { DBField } from '@/lib/domain/db-field'; | |||||||
| import type { DBIndex } from '@/lib/domain/db-index'; | import type { DBIndex } from '@/lib/domain/db-index'; | ||||||
| import type { DBRelationship } from '@/lib/domain/db-relationship'; | import type { DBRelationship } from '@/lib/domain/db-relationship'; | ||||||
| import type { DBDependency } from '@/lib/domain/db-dependency'; | import type { DBDependency } from '@/lib/domain/db-dependency'; | ||||||
|  | import type { Area } from '@/lib/domain/area'; | ||||||
|  | import type { DBCustomType } from '@/lib/domain/db-custom-type'; | ||||||
|  |  | ||||||
| type Action = keyof ChartDBContext; | type Action = keyof ChartDBContext; | ||||||
|  |  | ||||||
| @@ -123,6 +125,42 @@ type RedoUndoActionRemoveDependencies = RedoUndoActionBase< | |||||||
|     { dependencies: DBDependency[] } |     { dependencies: DBDependency[] } | ||||||
| >; | >; | ||||||
|  |  | ||||||
|  | type RedoUndoActionAddAreas = RedoUndoActionBase< | ||||||
|  |     'addAreas', | ||||||
|  |     { areas: Area[] }, | ||||||
|  |     { areaIds: string[] } | ||||||
|  | >; | ||||||
|  |  | ||||||
|  | type RedoUndoActionUpdateArea = RedoUndoActionBase< | ||||||
|  |     'updateArea', | ||||||
|  |     { areaId: string; area: Partial<Area> }, | ||||||
|  |     { areaId: string; area: Partial<Area> } | ||||||
|  | >; | ||||||
|  |  | ||||||
|  | type RedoUndoActionRemoveAreas = RedoUndoActionBase< | ||||||
|  |     'removeAreas', | ||||||
|  |     { areaIds: string[] }, | ||||||
|  |     { areas: Area[] } | ||||||
|  | >; | ||||||
|  |  | ||||||
|  | type RedoUndoActionAddCustomTypes = RedoUndoActionBase< | ||||||
|  |     'addCustomTypes', | ||||||
|  |     { customTypes: DBCustomType[] }, | ||||||
|  |     { customTypeIds: string[] } | ||||||
|  | >; | ||||||
|  |  | ||||||
|  | type RedoUndoActionUpdateCustomType = RedoUndoActionBase< | ||||||
|  |     'updateCustomType', | ||||||
|  |     { customTypeId: string; customType: Partial<DBCustomType> }, | ||||||
|  |     { customTypeId: string; customType: Partial<DBCustomType> } | ||||||
|  | >; | ||||||
|  |  | ||||||
|  | type RedoUndoActionRemoveCustomTypes = RedoUndoActionBase< | ||||||
|  |     'removeCustomTypes', | ||||||
|  |     { customTypeIds: string[] }, | ||||||
|  |     { customTypes: DBCustomType[] } | ||||||
|  | >; | ||||||
|  |  | ||||||
| export type RedoUndoAction = | export type RedoUndoAction = | ||||||
|     | RedoUndoActionAddTables |     | RedoUndoActionAddTables | ||||||
|     | RedoUndoActionRemoveTables |     | RedoUndoActionRemoveTables | ||||||
| @@ -140,7 +178,13 @@ export type RedoUndoAction = | |||||||
|     | RedoUndoActionRemoveRelationships |     | RedoUndoActionRemoveRelationships | ||||||
|     | RedoUndoActionAddDependencies |     | RedoUndoActionAddDependencies | ||||||
|     | RedoUndoActionUpdateDependency |     | RedoUndoActionUpdateDependency | ||||||
|     | RedoUndoActionRemoveDependencies; |     | RedoUndoActionRemoveDependencies | ||||||
|  |     | RedoUndoActionAddAreas | ||||||
|  |     | RedoUndoActionUpdateArea | ||||||
|  |     | RedoUndoActionRemoveAreas | ||||||
|  |     | RedoUndoActionAddCustomTypes | ||||||
|  |     | RedoUndoActionUpdateCustomType | ||||||
|  |     | RedoUndoActionRemoveCustomTypes; | ||||||
|  |  | ||||||
| export type RedoActionData<T extends Action> = Extract< | export type RedoActionData<T extends Action> = Extract< | ||||||
|     RedoUndoAction, |     RedoUndoAction, | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ import { useHistory } from '@/hooks/use-history'; | |||||||
| import { useDialog } from '@/hooks/use-dialog'; | import { useDialog } from '@/hooks/use-dialog'; | ||||||
| import { useChartDB } from '@/hooks/use-chartdb'; | import { useChartDB } from '@/hooks/use-chartdb'; | ||||||
| import { useLayout } from '@/hooks/use-layout'; | import { useLayout } from '@/hooks/use-layout'; | ||||||
|  | import { useReactFlow } from '@xyflow/react'; | ||||||
|  |  | ||||||
| export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({ | export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({ | ||||||
|     children, |     children, | ||||||
| @@ -17,6 +18,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|     const { openOpenDiagramDialog } = useDialog(); |     const { openOpenDiagramDialog } = useDialog(); | ||||||
|     const { updateDiagramUpdatedAt } = useChartDB(); |     const { updateDiagramUpdatedAt } = useChartDB(); | ||||||
|     const { toggleSidePanel } = useLayout(); |     const { toggleSidePanel } = useLayout(); | ||||||
|  |     const { fitView } = useReactFlow(); | ||||||
|  |  | ||||||
|     useHotkeys( |     useHotkeys( | ||||||
|         keyboardShortcutsForOS[KeyboardShortcutAction.REDO].keyCombination, |         keyboardShortcutsForOS[KeyboardShortcutAction.REDO].keyCombination, | ||||||
| @@ -37,7 +39,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|     useHotkeys( |     useHotkeys( | ||||||
|         keyboardShortcutsForOS[KeyboardShortcutAction.OPEN_DIAGRAM] |         keyboardShortcutsForOS[KeyboardShortcutAction.OPEN_DIAGRAM] | ||||||
|             .keyCombination, |             .keyCombination, | ||||||
|         openOpenDiagramDialog, |         () => openOpenDiagramDialog(), | ||||||
|         { |         { | ||||||
|             preventDefault: true, |             preventDefault: true, | ||||||
|         }, |         }, | ||||||
| @@ -61,6 +63,20 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|         }, |         }, | ||||||
|         [toggleSidePanel] |         [toggleSidePanel] | ||||||
|     ); |     ); | ||||||
|  |     useHotkeys( | ||||||
|  |         keyboardShortcutsForOS[KeyboardShortcutAction.SHOW_ALL].keyCombination, | ||||||
|  |         () => { | ||||||
|  |             fitView({ | ||||||
|  |                 duration: 500, | ||||||
|  |                 padding: 0.1, | ||||||
|  |                 maxZoom: 0.8, | ||||||
|  |             }); | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             preventDefault: true, | ||||||
|  |         }, | ||||||
|  |         [fitView] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <keyboardShortcutsContext.Provider value={{}}> |         <keyboardShortcutsContext.Provider value={{}}> | ||||||
|   | |||||||
| @@ -6,6 +6,8 @@ export enum KeyboardShortcutAction { | |||||||
|     OPEN_DIAGRAM = 'open_diagram', |     OPEN_DIAGRAM = 'open_diagram', | ||||||
|     SAVE_DIAGRAM = 'save_diagram', |     SAVE_DIAGRAM = 'save_diagram', | ||||||
|     TOGGLE_SIDE_PANEL = 'toggle_side_panel', |     TOGGLE_SIDE_PANEL = 'toggle_side_panel', | ||||||
|  |     SHOW_ALL = 'show_all', | ||||||
|  |     TOGGLE_THEME = 'toggle_theme', | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface KeyboardShortcut { | export interface KeyboardShortcut { | ||||||
| @@ -55,6 +57,20 @@ export const keyboardShortcuts: Record< | |||||||
|         keyCombinationMac: 'meta+b', |         keyCombinationMac: 'meta+b', | ||||||
|         keyCombinationWin: 'ctrl+b', |         keyCombinationWin: 'ctrl+b', | ||||||
|     }, |     }, | ||||||
|  |     [KeyboardShortcutAction.SHOW_ALL]: { | ||||||
|  |         action: KeyboardShortcutAction.SHOW_ALL, | ||||||
|  |         keyCombinationLabelMac: '⌘0', | ||||||
|  |         keyCombinationLabelWin: 'Ctrl+0', | ||||||
|  |         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 { | export interface KeyboardShortcutForOS { | ||||||
|   | |||||||
| @@ -1,7 +1,12 @@ | |||||||
| import { emptyFn } from '@/lib/utils'; | import { emptyFn } from '@/lib/utils'; | ||||||
| import { createContext } from 'react'; | import { createContext } from 'react'; | ||||||
|  |  | ||||||
| export type SidebarSection = 'tables' | 'relationships' | 'dependencies'; | export type SidebarSection = | ||||||
|  |     | 'tables' | ||||||
|  |     | 'relationships' | ||||||
|  |     | 'dependencies' | ||||||
|  |     | 'areas' | ||||||
|  |     | 'customTypes'; | ||||||
|  |  | ||||||
| export interface LayoutContext { | export interface LayoutContext { | ||||||
|     openedTableInSidebar: string | undefined; |     openedTableInSidebar: string | undefined; | ||||||
| @@ -16,6 +21,14 @@ export interface LayoutContext { | |||||||
|     openDependencyFromSidebar: (dependencyId: string) => void; |     openDependencyFromSidebar: (dependencyId: string) => void; | ||||||
|     closeAllDependenciesInSidebar: () => void; |     closeAllDependenciesInSidebar: () => void; | ||||||
|  |  | ||||||
|  |     openedAreaInSidebar: string | undefined; | ||||||
|  |     openAreaFromSidebar: (areaId: string) => void; | ||||||
|  |     closeAllAreasInSidebar: () => void; | ||||||
|  |  | ||||||
|  |     openedCustomTypeInSidebar: string | undefined; | ||||||
|  |     openCustomTypeFromSidebar: (customTypeId: string) => void; | ||||||
|  |     closeAllCustomTypesInSidebar: () => void; | ||||||
|  |  | ||||||
|     selectedSidebarSection: SidebarSection; |     selectedSidebarSection: SidebarSection; | ||||||
|     selectSidebarSection: (section: SidebarSection) => void; |     selectSidebarSection: (section: SidebarSection) => void; | ||||||
|  |  | ||||||
| @@ -41,6 +54,14 @@ export const layoutContext = createContext<LayoutContext>({ | |||||||
|     openDependencyFromSidebar: emptyFn, |     openDependencyFromSidebar: emptyFn, | ||||||
|     closeAllDependenciesInSidebar: emptyFn, |     closeAllDependenciesInSidebar: emptyFn, | ||||||
|  |  | ||||||
|  |     openedAreaInSidebar: undefined, | ||||||
|  |     openAreaFromSidebar: emptyFn, | ||||||
|  |     closeAllAreasInSidebar: emptyFn, | ||||||
|  |  | ||||||
|  |     openedCustomTypeInSidebar: undefined, | ||||||
|  |     openCustomTypeFromSidebar: emptyFn, | ||||||
|  |     closeAllCustomTypesInSidebar: emptyFn, | ||||||
|  |  | ||||||
|     selectSidebarSection: emptyFn, |     selectSidebarSection: emptyFn, | ||||||
|     openTableFromSidebar: emptyFn, |     openTableFromSidebar: emptyFn, | ||||||
|     closeAllTablesInSidebar: emptyFn, |     closeAllTablesInSidebar: emptyFn, | ||||||
|   | |||||||
| @@ -14,6 +14,11 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|         React.useState<string | undefined>(); |         React.useState<string | undefined>(); | ||||||
|     const [openedDependencyInSidebar, setOpenedDependencyInSidebar] = |     const [openedDependencyInSidebar, setOpenedDependencyInSidebar] = | ||||||
|         React.useState<string | undefined>(); |         React.useState<string | undefined>(); | ||||||
|  |     const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState< | ||||||
|  |         string | undefined | ||||||
|  |     >(); | ||||||
|  |     const [openedCustomTypeInSidebar, setOpenedCustomTypeInSidebar] = | ||||||
|  |         React.useState<string | undefined>(); | ||||||
|     const [selectedSidebarSection, setSelectedSidebarSection] = |     const [selectedSidebarSection, setSelectedSidebarSection] = | ||||||
|         React.useState<SidebarSection>('tables'); |         React.useState<SidebarSection>('tables'); | ||||||
|     const [isSidePanelShowed, setIsSidePanelShowed] = |     const [isSidePanelShowed, setIsSidePanelShowed] = | ||||||
| @@ -30,6 +35,12 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|     const closeAllDependenciesInSidebar: LayoutContext['closeAllDependenciesInSidebar'] = |     const closeAllDependenciesInSidebar: LayoutContext['closeAllDependenciesInSidebar'] = | ||||||
|         () => setOpenedDependencyInSidebar(''); |         () => setOpenedDependencyInSidebar(''); | ||||||
|  |  | ||||||
|  |     const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] = | ||||||
|  |         () => setOpenedAreaInSidebar(''); | ||||||
|  |  | ||||||
|  |     const closeAllCustomTypesInSidebar: LayoutContext['closeAllCustomTypesInSidebar'] = | ||||||
|  |         () => setOpenedCustomTypeInSidebar(''); | ||||||
|  |  | ||||||
|     const hideSidePanel: LayoutContext['hideSidePanel'] = () => |     const hideSidePanel: LayoutContext['hideSidePanel'] = () => | ||||||
|         setIsSidePanelShowed(false); |         setIsSidePanelShowed(false); | ||||||
|  |  | ||||||
| @@ -62,6 +73,21 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             setOpenedDependencyInSidebar(dependencyId); |             setOpenedDependencyInSidebar(dependencyId); | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|  |     const openAreaFromSidebar: LayoutContext['openAreaFromSidebar'] = ( | ||||||
|  |         areaId | ||||||
|  |     ) => { | ||||||
|  |         showSidePanel(); | ||||||
|  |         setSelectedSidebarSection('areas'); | ||||||
|  |         setOpenedAreaInSidebar(areaId); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const openCustomTypeFromSidebar: LayoutContext['openCustomTypeFromSidebar'] = | ||||||
|  |         (customTypeId) => { | ||||||
|  |             showSidePanel(); | ||||||
|  |             setSelectedSidebarSection('customTypes'); | ||||||
|  |             setOpenedTableInSidebar(customTypeId); | ||||||
|  |         }; | ||||||
|  |  | ||||||
|     const openSelectSchema: LayoutContext['openSelectSchema'] = () => |     const openSelectSchema: LayoutContext['openSelectSchema'] = () => | ||||||
|         setIsSelectSchemaOpen(true); |         setIsSelectSchemaOpen(true); | ||||||
|  |  | ||||||
| @@ -88,6 +114,12 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|                 openedDependencyInSidebar, |                 openedDependencyInSidebar, | ||||||
|                 openDependencyFromSidebar, |                 openDependencyFromSidebar, | ||||||
|                 closeAllDependenciesInSidebar, |                 closeAllDependenciesInSidebar, | ||||||
|  |                 openedAreaInSidebar, | ||||||
|  |                 openAreaFromSidebar, | ||||||
|  |                 closeAllAreasInSidebar, | ||||||
|  |                 openedCustomTypeInSidebar, | ||||||
|  |                 openCustomTypeFromSidebar, | ||||||
|  |                 closeAllCustomTypesInSidebar, | ||||||
|             }} |             }} | ||||||
|         > |         > | ||||||
|             {children} |             {children} | ||||||
|   | |||||||
| @@ -30,12 +30,6 @@ export interface LocalConfigContext { | |||||||
|     starUsDialogLastOpen: number; |     starUsDialogLastOpen: number; | ||||||
|     setStarUsDialogLastOpen: (lastOpen: number) => void; |     setStarUsDialogLastOpen: (lastOpen: number) => void; | ||||||
|  |  | ||||||
|     buckleWaitlistOpened: boolean; |  | ||||||
|     setBuckleWaitlistOpened: (githubRepoOpened: boolean) => void; |  | ||||||
|  |  | ||||||
|     buckleDialogLastOpen: number; |  | ||||||
|     setBuckleDialogLastOpen: (lastOpen: number) => void; |  | ||||||
|  |  | ||||||
|     showDependenciesOnCanvas: boolean; |     showDependenciesOnCanvas: boolean; | ||||||
|     setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void; |     setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void; | ||||||
|  |  | ||||||
| @@ -53,7 +47,7 @@ export const LocalConfigContext = createContext<LocalConfigContext>({ | |||||||
|     schemasFilter: {}, |     schemasFilter: {}, | ||||||
|     setSchemasFilter: emptyFn, |     setSchemasFilter: emptyFn, | ||||||
|  |  | ||||||
|     showCardinality: false, |     showCardinality: true, | ||||||
|     setShowCardinality: emptyFn, |     setShowCardinality: emptyFn, | ||||||
|  |  | ||||||
|     hideMultiSchemaNotification: false, |     hideMultiSchemaNotification: false, | ||||||
| @@ -65,12 +59,6 @@ export const LocalConfigContext = createContext<LocalConfigContext>({ | |||||||
|     starUsDialogLastOpen: 0, |     starUsDialogLastOpen: 0, | ||||||
|     setStarUsDialogLastOpen: emptyFn, |     setStarUsDialogLastOpen: emptyFn, | ||||||
|  |  | ||||||
|     buckleWaitlistOpened: false, |  | ||||||
|     setBuckleWaitlistOpened: emptyFn, |  | ||||||
|  |  | ||||||
|     buckleDialogLastOpen: 0, |  | ||||||
|     setBuckleDialogLastOpen: emptyFn, |  | ||||||
|  |  | ||||||
|     showDependenciesOnCanvas: false, |     showDependenciesOnCanvas: false, | ||||||
|     setShowDependenciesOnCanvas: emptyFn, |     setShowDependenciesOnCanvas: emptyFn, | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,8 +10,6 @@ const showCardinalityKey = 'show_cardinality'; | |||||||
| const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification'; | const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification'; | ||||||
| const githubRepoOpenedKey = 'github_repo_opened'; | const githubRepoOpenedKey = 'github_repo_opened'; | ||||||
| const starUsDialogLastOpenKey = 'star_us_dialog_last_open'; | const starUsDialogLastOpenKey = 'star_us_dialog_last_open'; | ||||||
| const buckleWaitlistOpenedKey = 'buckle_waitlist_opened'; |  | ||||||
| const buckleDialogLastOpenKey = 'buckle_dialog_last_open'; |  | ||||||
| const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas'; | const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas'; | ||||||
| const showMiniMapOnCanvasKey = 'show_minimap_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>( |     const [showCardinality, setShowCardinality] = React.useState<boolean>( | ||||||
|         (localStorage.getItem(showCardinalityKey) || 'false') === 'true' |         (localStorage.getItem(showCardinalityKey) || 'true') === 'true' | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const [hideMultiSchemaNotification, setHideMultiSchemaNotification] = |     const [hideMultiSchemaNotification, setHideMultiSchemaNotification] = | ||||||
| @@ -51,17 +49,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0') |             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] = |     const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] = | ||||||
|         React.useState<boolean>( |         React.useState<boolean>( | ||||||
|             (localStorage.getItem(showDependenciesOnCanvasKey) || 'false') === |             (localStorage.getItem(showDependenciesOnCanvasKey) || 'false') === | ||||||
| @@ -84,20 +71,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|         localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString()); |         localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString()); | ||||||
|     }, [githubRepoOpened]); |     }, [githubRepoOpened]); | ||||||
|  |  | ||||||
|     useEffect(() => { |  | ||||||
|         localStorage.setItem( |  | ||||||
|             buckleDialogLastOpenKey, |  | ||||||
|             buckleDialogLastOpen.toString() |  | ||||||
|         ); |  | ||||||
|     }, [buckleDialogLastOpen]); |  | ||||||
|  |  | ||||||
|     useEffect(() => { |  | ||||||
|         localStorage.setItem( |  | ||||||
|             buckleWaitlistOpenedKey, |  | ||||||
|             buckleWaitlistOpened.toString() |  | ||||||
|         ); |  | ||||||
|     }, [buckleWaitlistOpened]); |  | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         localStorage.setItem( |         localStorage.setItem( | ||||||
|             hideMultiSchemaNotificationKey, |             hideMultiSchemaNotificationKey, | ||||||
| @@ -154,10 +127,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|                 setStarUsDialogLastOpen, |                 setStarUsDialogLastOpen, | ||||||
|                 showDependenciesOnCanvas, |                 showDependenciesOnCanvas, | ||||||
|                 setShowDependenciesOnCanvas, |                 setShowDependenciesOnCanvas, | ||||||
|                 setBuckleDialogLastOpen, |  | ||||||
|                 buckleDialogLastOpen, |  | ||||||
|                 buckleWaitlistOpened, |  | ||||||
|                 setBuckleWaitlistOpened, |  | ||||||
|                 showMiniMapOnCanvas, |                 showMiniMapOnCanvas, | ||||||
|                 setShowMiniMapOnCanvas, |                 setShowMiniMapOnCanvas, | ||||||
|             }} |             }} | ||||||
|   | |||||||
| @@ -5,6 +5,8 @@ import type { DBRelationship } from '@/lib/domain/db-relationship'; | |||||||
| import type { DBTable } from '@/lib/domain/db-table'; | import type { DBTable } from '@/lib/domain/db-table'; | ||||||
| import type { ChartDBConfig } from '@/lib/domain/config'; | import type { ChartDBConfig } from '@/lib/domain/config'; | ||||||
| import type { DBDependency } from '@/lib/domain/db-dependency'; | import type { DBDependency } from '@/lib/domain/db-dependency'; | ||||||
|  | import type { Area } from '@/lib/domain/area'; | ||||||
|  | import type { DBCustomType } from '@/lib/domain/db-custom-type'; | ||||||
|  |  | ||||||
| export interface StorageContext { | export interface StorageContext { | ||||||
|     // Config operations |     // Config operations | ||||||
| @@ -17,6 +19,8 @@ export interface StorageContext { | |||||||
|         includeTables?: boolean; |         includeTables?: boolean; | ||||||
|         includeRelationships?: boolean; |         includeRelationships?: boolean; | ||||||
|         includeDependencies?: boolean; |         includeDependencies?: boolean; | ||||||
|  |         includeAreas?: boolean; | ||||||
|  |         includeCustomTypes?: boolean; | ||||||
|     }) => Promise<Diagram[]>; |     }) => Promise<Diagram[]>; | ||||||
|     getDiagram: ( |     getDiagram: ( | ||||||
|         id: string, |         id: string, | ||||||
| @@ -24,6 +28,8 @@ export interface StorageContext { | |||||||
|             includeTables?: boolean; |             includeTables?: boolean; | ||||||
|             includeRelationships?: boolean; |             includeRelationships?: boolean; | ||||||
|             includeDependencies?: boolean; |             includeDependencies?: boolean; | ||||||
|  |             includeAreas?: boolean; | ||||||
|  |             includeCustomTypes?: boolean; | ||||||
|         } |         } | ||||||
|     ) => Promise<Diagram | undefined>; |     ) => Promise<Diagram | undefined>; | ||||||
|     updateDiagram: (params: { |     updateDiagram: (params: { | ||||||
| @@ -86,6 +92,40 @@ export interface StorageContext { | |||||||
|     }) => Promise<void>; |     }) => Promise<void>; | ||||||
|     listDependencies: (diagramId: string) => Promise<DBDependency[]>; |     listDependencies: (diagramId: string) => Promise<DBDependency[]>; | ||||||
|     deleteDiagramDependencies: (diagramId: string) => Promise<void>; |     deleteDiagramDependencies: (diagramId: string) => Promise<void>; | ||||||
|  |  | ||||||
|  |     // Area operations | ||||||
|  |     addArea: (params: { diagramId: string; area: Area }) => Promise<void>; | ||||||
|  |     getArea: (params: { | ||||||
|  |         diagramId: string; | ||||||
|  |         id: string; | ||||||
|  |     }) => Promise<Area | undefined>; | ||||||
|  |     updateArea: (params: { | ||||||
|  |         id: string; | ||||||
|  |         attributes: Partial<Area>; | ||||||
|  |     }) => Promise<void>; | ||||||
|  |     deleteArea: (params: { diagramId: string; id: string }) => Promise<void>; | ||||||
|  |     listAreas: (diagramId: string) => Promise<Area[]>; | ||||||
|  |     deleteDiagramAreas: (diagramId: string) => Promise<void>; | ||||||
|  |  | ||||||
|  |     // Custom type operations | ||||||
|  |     addCustomType: (params: { | ||||||
|  |         diagramId: string; | ||||||
|  |         customType: DBCustomType; | ||||||
|  |     }) => Promise<void>; | ||||||
|  |     getCustomType: (params: { | ||||||
|  |         diagramId: string; | ||||||
|  |         id: string; | ||||||
|  |     }) => Promise<DBCustomType | undefined>; | ||||||
|  |     updateCustomType: (params: { | ||||||
|  |         id: string; | ||||||
|  |         attributes: Partial<DBCustomType>; | ||||||
|  |     }) => Promise<void>; | ||||||
|  |     deleteCustomType: (params: { | ||||||
|  |         diagramId: string; | ||||||
|  |         id: string; | ||||||
|  |     }) => Promise<void>; | ||||||
|  |     listCustomTypes: (diagramId: string) => Promise<DBCustomType[]>; | ||||||
|  |     deleteDiagramCustomTypes: (diagramId: string) => Promise<void>; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const storageInitialValue: StorageContext = { | export const storageInitialValue: StorageContext = { | ||||||
| @@ -119,6 +159,21 @@ export const storageInitialValue: StorageContext = { | |||||||
|     deleteDependency: emptyFn, |     deleteDependency: emptyFn, | ||||||
|     listDependencies: emptyFn, |     listDependencies: emptyFn, | ||||||
|     deleteDiagramDependencies: emptyFn, |     deleteDiagramDependencies: emptyFn, | ||||||
|  |  | ||||||
|  |     addArea: emptyFn, | ||||||
|  |     getArea: emptyFn, | ||||||
|  |     updateArea: emptyFn, | ||||||
|  |     deleteArea: emptyFn, | ||||||
|  |     listAreas: emptyFn, | ||||||
|  |     deleteDiagramAreas: emptyFn, | ||||||
|  |  | ||||||
|  |     // Custom type operations | ||||||
|  |     addCustomType: emptyFn, | ||||||
|  |     getCustomType: emptyFn, | ||||||
|  |     updateCustomType: emptyFn, | ||||||
|  |     deleteCustomType: emptyFn, | ||||||
|  |     listCustomTypes: emptyFn, | ||||||
|  |     deleteDiagramCustomTypes: emptyFn, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const storageContext = | export const storageContext = | ||||||
|   | |||||||
| @@ -8,6 +8,8 @@ import type { DBRelationship } from '@/lib/domain/db-relationship'; | |||||||
| import { determineCardinalities } from '@/lib/domain/db-relationship'; | import { determineCardinalities } from '@/lib/domain/db-relationship'; | ||||||
| import type { ChartDBConfig } from '@/lib/domain/config'; | import type { ChartDBConfig } from '@/lib/domain/config'; | ||||||
| import type { DBDependency } from '@/lib/domain/db-dependency'; | import type { DBDependency } from '@/lib/domain/db-dependency'; | ||||||
|  | import type { Area } from '@/lib/domain/area'; | ||||||
|  | import type { DBCustomType } from '@/lib/domain/db-custom-type'; | ||||||
|  |  | ||||||
| export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | ||||||
|     children, |     children, | ||||||
| @@ -29,6 +31,14 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             DBDependency & { diagramId: string }, |             DBDependency & { diagramId: string }, | ||||||
|             'id' // primary key "id" (for the typings only) |             'id' // primary key "id" (for the typings only) | ||||||
|         >; |         >; | ||||||
|  |         areas: EntityTable< | ||||||
|  |             Area & { diagramId: string }, | ||||||
|  |             'id' // primary key "id" (for the typings only) | ||||||
|  |         >; | ||||||
|  |         db_custom_types: EntityTable< | ||||||
|  |             DBCustomType & { diagramId: string }, | ||||||
|  |             'id' // primary key "id" (for the typings only) | ||||||
|  |         >; | ||||||
|         config: EntityTable< |         config: EntityTable< | ||||||
|             ChartDBConfig & { id: number }, |             ChartDBConfig & { id: number }, | ||||||
|             'id' // primary key "id" (for the typings only) |             'id' // primary key "id" (for the typings only) | ||||||
| @@ -148,6 +158,33 @@ 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.version(11).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', | ||||||
|  |         db_custom_types: '++id, diagramId, schema, type, kind, values, fields', | ||||||
|  |         config: '++id, defaultDiagramId', | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     db.on('ready', async () => { |     db.on('ready', async () => { | ||||||
|         const config = await getConfig(); |         const config = await getConfig(); | ||||||
|  |  | ||||||
| @@ -209,6 +246,18 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             ) |             ) | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|  |         const areas = diagram.areas ?? []; | ||||||
|  |         promises.push( | ||||||
|  |             ...areas.map((area) => addArea({ diagramId: diagram.id, area })) | ||||||
|  |         ); | ||||||
|  |  | ||||||
|  |         const customTypes = diagram.customTypes ?? []; | ||||||
|  |         promises.push( | ||||||
|  |             ...customTypes.map((customType) => | ||||||
|  |                 addCustomType({ diagramId: diagram.id, customType }) | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |  | ||||||
|         await Promise.all(promises); |         await Promise.all(promises); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
| @@ -217,10 +266,14 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             includeTables?: boolean; |             includeTables?: boolean; | ||||||
|             includeRelationships?: boolean; |             includeRelationships?: boolean; | ||||||
|             includeDependencies?: boolean; |             includeDependencies?: boolean; | ||||||
|  |             includeAreas?: boolean; | ||||||
|  |             includeCustomTypes?: boolean; | ||||||
|         } = { |         } = { | ||||||
|             includeRelationships: false, |             includeRelationships: false, | ||||||
|             includeTables: false, |             includeTables: false, | ||||||
|             includeDependencies: false, |             includeDependencies: false, | ||||||
|  |             includeAreas: false, | ||||||
|  |             includeCustomTypes: false, | ||||||
|         } |         } | ||||||
|     ): Promise<Diagram[]> => { |     ): Promise<Diagram[]> => { | ||||||
|         let diagrams = await db.diagrams.toArray(); |         let diagrams = await db.diagrams.toArray(); | ||||||
| @@ -252,6 +305,24 @@ 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; | ||||||
|  |                 }) | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (options.includeCustomTypes) { | ||||||
|  |             diagrams = await Promise.all( | ||||||
|  |                 diagrams.map(async (diagram) => { | ||||||
|  |                     diagram.customTypes = await listCustomTypes(diagram.id); | ||||||
|  |                     return diagram; | ||||||
|  |                 }) | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         return diagrams; |         return diagrams; | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
| @@ -261,10 +332,14 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             includeTables?: boolean; |             includeTables?: boolean; | ||||||
|             includeRelationships?: boolean; |             includeRelationships?: boolean; | ||||||
|             includeDependencies?: boolean; |             includeDependencies?: boolean; | ||||||
|  |             includeAreas?: boolean; | ||||||
|  |             includeCustomTypes?: boolean; | ||||||
|         } = { |         } = { | ||||||
|             includeRelationships: false, |             includeRelationships: false, | ||||||
|             includeTables: false, |             includeTables: false, | ||||||
|             includeDependencies: false, |             includeDependencies: false, | ||||||
|  |             includeAreas: false, | ||||||
|  |             includeCustomTypes: false, | ||||||
|         } |         } | ||||||
|     ): Promise<Diagram | undefined> => { |     ): Promise<Diagram | undefined> => { | ||||||
|         const diagram = await db.diagrams.get(id); |         const diagram = await db.diagrams.get(id); | ||||||
| @@ -285,6 +360,14 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             diagram.dependencies = await listDependencies(id); |             diagram.dependencies = await listDependencies(id); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (options.includeAreas) { | ||||||
|  |             diagram.areas = await listAreas(id); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (options.includeCustomTypes) { | ||||||
|  |             diagram.customTypes = await listCustomTypes(id); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         return diagram; |         return diagram; | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
| @@ -311,6 +394,13 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|                     .where('diagramId') |                     .where('diagramId') | ||||||
|                     .equals(id) |                     .equals(id) | ||||||
|                     .modify({ diagramId: attributes.id }), |                     .modify({ diagramId: attributes.id }), | ||||||
|  |                 db.areas.where('diagramId').equals(id).modify({ | ||||||
|  |                     diagramId: attributes.id, | ||||||
|  |                 }), | ||||||
|  |                 db.db_custom_types | ||||||
|  |                     .where('diagramId') | ||||||
|  |                     .equals(id) | ||||||
|  |                     .modify({ diagramId: attributes.id }), | ||||||
|             ]); |             ]); | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| @@ -323,6 +413,8 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             db.db_tables.where('diagramId').equals(id).delete(), |             db.db_tables.where('diagramId').equals(id).delete(), | ||||||
|             db.db_relationships.where('diagramId').equals(id).delete(), |             db.db_relationships.where('diagramId').equals(id).delete(), | ||||||
|             db.db_dependencies.where('diagramId').equals(id).delete(), |             db.db_dependencies.where('diagramId').equals(id).delete(), | ||||||
|  |             db.areas.where('diagramId').equals(id).delete(), | ||||||
|  |             db.db_custom_types.where('diagramId').equals(id).delete(), | ||||||
|         ]); |         ]); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
| @@ -504,6 +596,106 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|                 .delete(); |                 .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(); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // Custom type operations | ||||||
|  |     const addCustomType: StorageContext['addCustomType'] = async ({ | ||||||
|  |         diagramId, | ||||||
|  |         customType, | ||||||
|  |     }: { | ||||||
|  |         diagramId: string; | ||||||
|  |         customType: DBCustomType; | ||||||
|  |     }) => { | ||||||
|  |         await db.db_custom_types.add({ | ||||||
|  |             ...customType, | ||||||
|  |             diagramId, | ||||||
|  |         }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const getCustomType: StorageContext['getCustomType'] = async ({ | ||||||
|  |         diagramId, | ||||||
|  |         id, | ||||||
|  |     }: { | ||||||
|  |         diagramId: string; | ||||||
|  |         id: string; | ||||||
|  |     }): Promise<DBCustomType | undefined> => { | ||||||
|  |         return await db.db_custom_types.get({ id, diagramId }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const updateCustomType: StorageContext['updateCustomType'] = async ({ | ||||||
|  |         id, | ||||||
|  |         attributes, | ||||||
|  |     }: { | ||||||
|  |         id: string; | ||||||
|  |         attributes: Partial<DBCustomType>; | ||||||
|  |     }) => { | ||||||
|  |         await db.db_custom_types.update(id, attributes); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const deleteCustomType: StorageContext['deleteCustomType'] = async ({ | ||||||
|  |         diagramId, | ||||||
|  |         id, | ||||||
|  |     }: { | ||||||
|  |         id: string; | ||||||
|  |         diagramId: string; | ||||||
|  |     }) => { | ||||||
|  |         await db.db_custom_types.where({ id, diagramId }).delete(); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const listCustomTypes: StorageContext['listCustomTypes'] = async ( | ||||||
|  |         diagramId: string | ||||||
|  |     ): Promise<DBCustomType[]> => { | ||||||
|  |         return ( | ||||||
|  |             await db.db_custom_types | ||||||
|  |                 .where('diagramId') | ||||||
|  |                 .equals(diagramId) | ||||||
|  |                 .toArray() | ||||||
|  |         ).sort((a, b) => { | ||||||
|  |             return a.name.localeCompare(b.name); | ||||||
|  |         }); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const deleteDiagramCustomTypes: StorageContext['deleteDiagramCustomTypes'] = | ||||||
|  |         async (diagramId: string) => { | ||||||
|  |             await db.db_custom_types | ||||||
|  |                 .where('diagramId') | ||||||
|  |                 .equals(diagramId) | ||||||
|  |                 .delete(); | ||||||
|  |         }; | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <storageContext.Provider |         <storageContext.Provider | ||||||
|             value={{ |             value={{ | ||||||
| @@ -533,6 +725,18 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|                 deleteDependency, |                 deleteDependency, | ||||||
|                 listDependencies, |                 listDependencies, | ||||||
|                 deleteDiagramDependencies, |                 deleteDiagramDependencies, | ||||||
|  |                 addArea, | ||||||
|  |                 getArea, | ||||||
|  |                 updateArea, | ||||||
|  |                 deleteArea, | ||||||
|  |                 listAreas, | ||||||
|  |                 deleteDiagramAreas, | ||||||
|  |                 addCustomType, | ||||||
|  |                 getCustomType, | ||||||
|  |                 updateCustomType, | ||||||
|  |                 deleteCustomType, | ||||||
|  |                 listCustomTypes, | ||||||
|  |                 deleteDiagramCustomTypes, | ||||||
|             }} |             }} | ||||||
|         > |         > | ||||||
|             {children} |             {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 type { EffectiveTheme } from './theme-context'; | ||||||
| import { ThemeContext } from './theme-context'; | import { ThemeContext } from './theme-context'; | ||||||
| import { useMediaQuery } from 'react-responsive'; | import { useMediaQuery } from 'react-responsive'; | ||||||
| import { useLocalConfig } from '@/hooks/use-local-config'; | 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> = ({ | export const ThemeProvider: React.FC<React.PropsWithChildren> = ({ | ||||||
|     children, |     children, | ||||||
| @@ -29,6 +34,24 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|         } |         } | ||||||
|     }, [effectiveTheme]); |     }, [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 ( |     return ( | ||||||
|         <ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}> |         <ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}> | ||||||
|             {children} |             {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 { Button } from '@/components/button/button'; | ||||||
| import { | import { | ||||||
|     DialogClose, |     DialogClose, | ||||||
| @@ -8,31 +14,10 @@ import { | |||||||
|     DialogInternalContent, |     DialogInternalContent, | ||||||
|     DialogTitle, |     DialogTitle, | ||||||
| } from '@/components/dialog/dialog'; | } from '@/components/dialog/dialog'; | ||||||
| import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group'; | import type { DatabaseType } from '@/lib/domain/database-type'; | ||||||
| import { DatabaseType } from '@/lib/domain/database-type'; | import { Editor } from '@/components/code-snippet/code-snippet'; | ||||||
| import { databaseSecondaryLogoMap } from '@/lib/databases'; |  | ||||||
| import { CodeSnippet } from '@/components/code-snippet/code-snippet'; |  | ||||||
| import { Textarea } from '@/components/textarea/textarea'; |  | ||||||
| import type { DatabaseEdition } from '@/lib/domain/database-edition'; | 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 { 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 { ZoomableImage } from '@/components/zoomable-image/zoomable-image'; | ||||||
| import { useBreakpoint } from '@/hooks/use-breakpoint'; | import { useBreakpoint } from '@/hooks/use-breakpoint'; | ||||||
| import { Spinner } from '@/components/spinner/spinner'; | import { Spinner } from '@/components/spinner/spinner'; | ||||||
| @@ -40,9 +25,63 @@ import { | |||||||
|     fixMetadataJson, |     fixMetadataJson, | ||||||
|     isStringMetadataJson, |     isStringMetadataJson, | ||||||
| } from '@/lib/data/import-metadata/utils'; | } 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 = | 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 { | export interface ImportDatabaseProps { | ||||||
|     goBack?: () => void; |     goBack?: () => void; | ||||||
| @@ -57,6 +96,8 @@ export interface ImportDatabaseProps { | |||||||
|     >; |     >; | ||||||
|     keepDialogAfterImport?: boolean; |     keepDialogAfterImport?: boolean; | ||||||
|     title: string; |     title: string; | ||||||
|  |     importMethod: 'query' | 'ddl'; | ||||||
|  |     setImportMethod: (method: 'query' | 'ddl') => void; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||||
| @@ -70,32 +111,52 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | |||||||
|     setDatabaseEdition, |     setDatabaseEdition, | ||||||
|     keepDialogAfterImport, |     keepDialogAfterImport, | ||||||
|     title, |     title, | ||||||
|  |     importMethod, | ||||||
|  |     setImportMethod, | ||||||
| }) => { | }) => { | ||||||
|     const databaseClients = databaseTypeToClientsMap[databaseType]; |     const { effectiveTheme } = useTheme(); | ||||||
|     const [errorMessage, setErrorMessage] = useState(''); |     const [errorMessage, setErrorMessage] = useState(''); | ||||||
|     const [databaseClient, setDatabaseClient] = useState< |     const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null); | ||||||
|         DatabaseClient | undefined |  | ||||||
|     >(); |  | ||||||
|     const { t } = useTranslation(); |  | ||||||
|     const [importMetadataScripts, setImportMetadataScripts] = |  | ||||||
|         useState<ImportMetadataScripts | null>(null); |  | ||||||
|  |  | ||||||
|  |     const { t } = useTranslation(); | ||||||
|     const { isSm: isDesktop } = useBreakpoint('sm'); |     const { isSm: isDesktop } = useBreakpoint('sm'); | ||||||
|  |  | ||||||
|     const [showCheckJsonButton, setShowCheckJsonButton] = useState(false); |     const [showCheckJsonButton, setShowCheckJsonButton] = useState(false); | ||||||
|     const [isCheckingJson, setIsCheckingJson] = useState(false); |     const [isCheckingJson, setIsCheckingJson] = useState(false); | ||||||
|  |     const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false); | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         const loadScripts = async () => { |         setScriptResult(''); | ||||||
|             const { importMetadataScripts } = await import( |         setErrorMessage(''); | ||||||
|                 '@/lib/data/import-metadata/scripts/scripts' |         setShowCheckJsonButton(false); | ||||||
|             ); |     }, [importMethod, setScriptResult]); | ||||||
|             setImportMetadataScripts(importMetadataScripts); |  | ||||||
|         }; |  | ||||||
|         loadScripts(); |  | ||||||
|     }, []); |  | ||||||
|  |  | ||||||
|  |     // Check if the ddl is valid | ||||||
|     useEffect(() => { |     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) { |         if (scriptResult.trim().length === 0) { | ||||||
|             setErrorMessage(''); |             setErrorMessage(''); | ||||||
|             setShowCheckJsonButton(false); |             setShowCheckJsonButton(false); | ||||||
| @@ -115,7 +176,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | |||||||
|             setErrorMessage(errorScriptOutputMessage); |             setErrorMessage(errorScriptOutputMessage); | ||||||
|             setShowCheckJsonButton(false); |             setShowCheckJsonButton(false); | ||||||
|         } |         } | ||||||
|     }, [scriptResult]); |     }, [scriptResult, importMethod]); | ||||||
|  |  | ||||||
|     const handleImport = useCallback(() => { |     const handleImport = useCallback(() => { | ||||||
|         if (errorMessage.length === 0 && scriptResult.trim().length !== 0) { |         if (errorMessage.length === 0 && scriptResult.trim().length !== 0) { | ||||||
| @@ -123,14 +184,30 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | |||||||
|         } |         } | ||||||
|     }, [errorMessage.length, onImport, scriptResult]); |     }, [errorMessage.length, onImport, scriptResult]); | ||||||
|  |  | ||||||
|     const handleInputChange = useCallback( |     const formatEditor = useCallback(() => { | ||||||
|         (e: React.ChangeEvent<HTMLTextAreaElement>) => { |         if (editorRef.current) { | ||||||
|             const inputValue = e.target.value; |             setTimeout(() => { | ||||||
|             setScriptResult(inputValue); |                 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) { | ||||||
|  |                 setShowSSMSInfoDialog(true); | ||||||
|  |             } | ||||||
|         }, |         }, | ||||||
|         [setScriptResult] |         [setScriptResult] | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     const debouncedHandleInputChange = useDebounce(handleInputChange, 500); | ||||||
|  |  | ||||||
|     const handleCheckJson = useCallback(async () => { |     const handleCheckJson = useCallback(async () => { | ||||||
|         setIsCheckingJson(true); |         setIsCheckingJson(true); | ||||||
|  |  | ||||||
| @@ -139,14 +216,49 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | |||||||
|         if (isStringMetadataJson(fixedJson)) { |         if (isStringMetadataJson(fixedJson)) { | ||||||
|             setScriptResult(fixedJson); |             setScriptResult(fixedJson); | ||||||
|             setErrorMessage(''); |             setErrorMessage(''); | ||||||
|  |             formatEditor(); | ||||||
|         } else { |         } else { | ||||||
|             setScriptResult(fixedJson); |             setScriptResult(fixedJson); | ||||||
|             setErrorMessage(errorScriptOutputMessage); |             setErrorMessage(errorScriptOutputMessage); | ||||||
|  |             formatEditor(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         setShowCheckJsonButton(false); |         setShowCheckJsonButton(false); | ||||||
|         setIsCheckingJson(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(() => { |     const renderHeader = useCallback(() => { | ||||||
|         return ( |         return ( | ||||||
| @@ -157,223 +269,131 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | |||||||
|         ); |         ); | ||||||
|     }, [title]); |     }, [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(() => { |     const renderContent = useCallback(() => { | ||||||
|         return ( |         return ( | ||||||
|             <DialogInternalContent> |             <DialogInternalContent> | ||||||
|                 <div className="flex w-full flex-1 flex-col gap-6"> |                 {isDesktop ? ( | ||||||
|                     {databaseTypeToEditionMap[databaseType].length > 0 ? ( |                     <ResizablePanelGroup | ||||||
|                         <div className="flex flex-col gap-1 md:flex-row"> |                         direction={isDesktop ? 'horizontal' : 'vertical'} | ||||||
|                             <p className="text-sm leading-6 text-muted-foreground"> |                         className="min-h-[500px]" | ||||||
|                                 {t( |                     > | ||||||
|                                     'new_diagram_dialog.import_database.database_edition' |                         <ResizablePanel | ||||||
|                                 )} |                             defaultSize={25} | ||||||
|                             </p> |                             minSize={25} | ||||||
|                             <ToggleGroup |                             maxSize={99} | ||||||
|                                 type="single" |                             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" | ||||||
|                                 className="ml-1 flex-wrap gap-2" |                         > | ||||||
|                                 value={ |                             {renderInstructions()} | ||||||
|                                     !databaseEdition |                         </ResizablePanel> | ||||||
|                                         ? 'regular' |                         <ResizableHandle withHandle /> | ||||||
|                                         : databaseEdition |                         <ResizablePanel className="min-h-40 py-2 md:px-2 md:py-0"> | ||||||
|                                 } |                             {renderOutputTextArea()} | ||||||
|                                 onValueChange={(value) => { |                         </ResizablePanel> | ||||||
|                                     setDatabaseEdition( |                     </ResizablePanelGroup> | ||||||
|                                         value === 'regular' |                 ) : ( | ||||||
|                                             ? undefined |                     <div className="flex flex-col gap-2"> | ||||||
|                                             : (value as DatabaseEdition) |                         {renderInstructions()} | ||||||
|                                     ); |                         {renderOutputTextArea()} | ||||||
|                                 }} |  | ||||||
|                             > |  | ||||||
|                                 <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 /> |  | ||||||
|                             )} |  | ||||||
|                         </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" |  | ||||||
|                             /> |  | ||||||
|                         )} |  | ||||||
|                     </div> |                     </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> |             </DialogInternalContent> | ||||||
|         ); |         ); | ||||||
|     }, [ |     }, [renderOutputTextArea, renderInstructions, isDesktop]); | ||||||
|         databaseEdition, |  | ||||||
|         databaseType, |  | ||||||
|         errorMessage, |  | ||||||
|         handleInputChange, |  | ||||||
|         scriptResult, |  | ||||||
|         setDatabaseEdition, |  | ||||||
|         databaseClients, |  | ||||||
|         databaseClient, |  | ||||||
|         importMetadataScripts, |  | ||||||
|         t, |  | ||||||
|         showCheckJsonButton, |  | ||||||
|         isCheckingJson, |  | ||||||
|         handleCheckJson, |  | ||||||
|     ]); |  | ||||||
|  |  | ||||||
|     const renderFooter = useCallback(() => { |     const renderFooter = useCallback(() => { | ||||||
|         return ( |         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"> |                 <div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2"> | ||||||
|                     {goBack && ( |                     {goBack && ( | ||||||
|                         <Button |                         <Button | ||||||
| @@ -407,7 +427,22 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | |||||||
|                         </DialogClose> |                         </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 |                         <Button | ||||||
|                             type="button" |                             type="button" | ||||||
|                             variant="default" |                             variant="default" | ||||||
| @@ -425,7 +460,6 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | |||||||
|                                 type="button" |                                 type="button" | ||||||
|                                 variant="default" |                                 variant="default" | ||||||
|                                 disabled={ |                                 disabled={ | ||||||
|                                     showCheckJsonButton || |  | ||||||
|                                     scriptResult.trim().length === 0 || |                                     scriptResult.trim().length === 0 || | ||||||
|                                     errorMessage.length > 0 |                                     errorMessage.length > 0 | ||||||
|                                 } |                                 } | ||||||
| @@ -456,6 +490,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | |||||||
|         errorMessage.length, |         errorMessage.length, | ||||||
|         scriptResult, |         scriptResult, | ||||||
|         showCheckJsonButton, |         showCheckJsonButton, | ||||||
|  |         isCheckingJson, | ||||||
|  |         handleCheckJson, | ||||||
|         goBack, |         goBack, | ||||||
|         t, |         t, | ||||||
|     ]); |     ]); | ||||||
|   | |||||||
| @@ -0,0 +1,179 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import logo from '@/assets/logo-2.png'; | ||||||
|  | import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group'; | ||||||
|  | import { DatabaseType } from '@/lib/domain/database-type'; | ||||||
|  | import { databaseSecondaryLogoMap } from '@/lib/databases'; | ||||||
|  | import type { DatabaseEdition } from '@/lib/domain/database-edition'; | ||||||
|  | import { | ||||||
|  |     databaseEditionToImageMap, | ||||||
|  |     databaseEditionToLabelMap, | ||||||
|  |     databaseTypeToEditionMap, | ||||||
|  | } from '@/lib/domain/database-edition'; | ||||||
|  | import { | ||||||
|  |     Avatar, | ||||||
|  |     AvatarFallback, | ||||||
|  |     AvatarImage, | ||||||
|  | } from '@/components/avatar/avatar'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { Code } from 'lucide-react'; | ||||||
|  | import { SmartQueryInstructions } from './instructions/smart-query-instructions'; | ||||||
|  | import { DDLInstructions } from './instructions/ddl-instructions'; | ||||||
|  |  | ||||||
|  | const DatabasesWithoutDDLInstructions: DatabaseType[] = [ | ||||||
|  |     DatabaseType.CLICKHOUSE, | ||||||
|  |     DatabaseType.ORACLE, | ||||||
|  | ]; | ||||||
|  |  | ||||||
|  | export interface InstructionsSectionProps { | ||||||
|  |     databaseType: DatabaseType; | ||||||
|  |     databaseEdition?: DatabaseEdition; | ||||||
|  |     setDatabaseEdition: React.Dispatch< | ||||||
|  |         React.SetStateAction<DatabaseEdition | undefined> | ||||||
|  |     >; | ||||||
|  |     importMethod: 'query' | 'ddl'; | ||||||
|  |     setImportMethod: (method: 'query' | 'ddl') => void; | ||||||
|  |     showSSMSInfoDialog: boolean; | ||||||
|  |     setShowSSMSInfoDialog: (show: boolean) => void; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const InstructionsSection: React.FC<InstructionsSectionProps> = ({ | ||||||
|  |     databaseType, | ||||||
|  |     databaseEdition, | ||||||
|  |     setDatabaseEdition, | ||||||
|  |     importMethod, | ||||||
|  |     setImportMethod, | ||||||
|  |     setShowSSMSInfoDialog, | ||||||
|  |     showSSMSInfoDialog, | ||||||
|  | }) => { | ||||||
|  |     const { t } = useTranslation(); | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |         <div className="flex w-full flex-1 flex-col gap-4"> | ||||||
|  |             {databaseTypeToEditionMap[databaseType].length > 0 ? ( | ||||||
|  |                 <div className="flex flex-col gap-1"> | ||||||
|  |                     <p className="text-sm leading-6 text-primary"> | ||||||
|  |                         {t( | ||||||
|  |                             'new_diagram_dialog.import_database.database_edition' | ||||||
|  |                         )} | ||||||
|  |                     </p> | ||||||
|  |                     <ToggleGroup | ||||||
|  |                         type="single" | ||||||
|  |                         className="ml-1 flex-wrap justify-start gap-2" | ||||||
|  |                         value={!databaseEdition ? 'regular' : databaseEdition} | ||||||
|  |                         onValueChange={(value) => { | ||||||
|  |                             setDatabaseEdition( | ||||||
|  |                                 value === 'regular' | ||||||
|  |                                     ? undefined | ||||||
|  |                                     : (value as DatabaseEdition) | ||||||
|  |                             ); | ||||||
|  |                         }} | ||||||
|  |                     > | ||||||
|  |                         <ToggleGroupItem | ||||||
|  |                             value="regular" | ||||||
|  |                             variant="outline" | ||||||
|  |                             className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700" | ||||||
|  |                         > | ||||||
|  |                             <Avatar className="size-4 rounded-none"> | ||||||
|  |                                 <AvatarImage | ||||||
|  |                                     src={databaseSecondaryLogoMap[databaseType]} | ||||||
|  |                                     alt="Regular" | ||||||
|  |                                 /> | ||||||
|  |                                 <AvatarFallback>Regular</AvatarFallback> | ||||||
|  |                             </Avatar> | ||||||
|  |                             Regular | ||||||
|  |                         </ToggleGroupItem> | ||||||
|  |                         {databaseTypeToEditionMap[databaseType].map( | ||||||
|  |                             (edition) => ( | ||||||
|  |                                 <ToggleGroupItem | ||||||
|  |                                     value={edition} | ||||||
|  |                                     key={edition} | ||||||
|  |                                     variant="outline" | ||||||
|  |                                     className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700" | ||||||
|  |                                 > | ||||||
|  |                                     <Avatar className="size-4"> | ||||||
|  |                                         <AvatarImage | ||||||
|  |                                             src={ | ||||||
|  |                                                 databaseEditionToImageMap[ | ||||||
|  |                                                     edition | ||||||
|  |                                                 ] | ||||||
|  |                                             } | ||||||
|  |                                             alt={ | ||||||
|  |                                                 databaseEditionToLabelMap[ | ||||||
|  |                                                     edition | ||||||
|  |                                                 ] | ||||||
|  |                                             } | ||||||
|  |                                         /> | ||||||
|  |                                         <AvatarFallback> | ||||||
|  |                                             {databaseEditionToLabelMap[edition]} | ||||||
|  |                                         </AvatarFallback> | ||||||
|  |                                     </Avatar> | ||||||
|  |                                     {databaseEditionToLabelMap[edition]} | ||||||
|  |                                 </ToggleGroupItem> | ||||||
|  |                             ) | ||||||
|  |                         )} | ||||||
|  |                     </ToggleGroup> | ||||||
|  |                 </div> | ||||||
|  |             ) : null} | ||||||
|  |  | ||||||
|  |             {DatabasesWithoutDDLInstructions.includes(databaseType) ? null : ( | ||||||
|  |                 <div className="flex flex-col gap-1"> | ||||||
|  |                     <p className="text-sm leading-6 text-primary"> | ||||||
|  |                         How would you like to import? | ||||||
|  |                     </p> | ||||||
|  |                     <ToggleGroup | ||||||
|  |                         type="single" | ||||||
|  |                         className="ml-1 flex-wrap justify-start gap-2" | ||||||
|  |                         value={importMethod} | ||||||
|  |                         onValueChange={(value) => { | ||||||
|  |                             let selectedImportMethod: 'query' | 'ddl' = 'query'; | ||||||
|  |                             if (value) { | ||||||
|  |                                 selectedImportMethod = value as 'query' | 'ddl'; | ||||||
|  |                             } | ||||||
|  |  | ||||||
|  |                             setImportMethod(selectedImportMethod); | ||||||
|  |                         }} | ||||||
|  |                     > | ||||||
|  |                         <ToggleGroupItem | ||||||
|  |                             value="query" | ||||||
|  |                             variant="outline" | ||||||
|  |                             className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700" | ||||||
|  |                         > | ||||||
|  |                             <Avatar className="h-3 w-4 rounded-none"> | ||||||
|  |                                 <AvatarImage src={logo} alt="query" /> | ||||||
|  |                                 <AvatarFallback>Query</AvatarFallback> | ||||||
|  |                             </Avatar> | ||||||
|  |                             Smart Query | ||||||
|  |                         </ToggleGroupItem> | ||||||
|  |                         <ToggleGroupItem | ||||||
|  |                             value="ddl" | ||||||
|  |                             variant="outline" | ||||||
|  |                             className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700" | ||||||
|  |                         > | ||||||
|  |                             <Avatar className="size-4 rounded-none"> | ||||||
|  |                                 <Code size={16} /> | ||||||
|  |                             </Avatar> | ||||||
|  |                             SQL Script | ||||||
|  |                         </ToggleGroupItem> | ||||||
|  |                     </ToggleGroup> | ||||||
|  |                 </div> | ||||||
|  |             )} | ||||||
|  |  | ||||||
|  |             <div className="flex flex-col gap-2"> | ||||||
|  |                 <div className="text-sm font-semibold">Instructions:</div> | ||||||
|  |                 {importMethod === 'query' ? ( | ||||||
|  |                     <SmartQueryInstructions | ||||||
|  |                         databaseType={databaseType} | ||||||
|  |                         databaseEdition={databaseEdition} | ||||||
|  |                         showSSMSInfoDialog={showSSMSInfoDialog} | ||||||
|  |                         setShowSSMSInfoDialog={setShowSSMSInfoDialog} | ||||||
|  |                     /> | ||||||
|  |                 ) : ( | ||||||
|  |                     <DDLInstructions | ||||||
|  |                         databaseType={databaseType} | ||||||
|  |                         databaseEdition={databaseEdition} | ||||||
|  |                     /> | ||||||
|  |                 )} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @@ -0,0 +1,48 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import { CodeSnippet } from '@/components/code-snippet/code-snippet'; | ||||||
|  |  | ||||||
|  | export interface DDLInstructionStepProps { | ||||||
|  |     index: number; | ||||||
|  |     text: string; | ||||||
|  |     code?: string; | ||||||
|  |     example?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const DDLInstructionStep: React.FC<DDLInstructionStepProps> = ({ | ||||||
|  |     index, | ||||||
|  |     text, | ||||||
|  |     code, | ||||||
|  |     example, | ||||||
|  | }) => { | ||||||
|  |     return ( | ||||||
|  |         <div className="flex flex-col gap-1"> | ||||||
|  |             <div className="flex flex-col gap-1 text-sm text-primary"> | ||||||
|  |                 <div> | ||||||
|  |                     <span className="font-medium">{index}.</span> {text} | ||||||
|  |                 </div> | ||||||
|  |  | ||||||
|  |                 {code ? ( | ||||||
|  |                     <div className="h-[60px]"> | ||||||
|  |                         <CodeSnippet | ||||||
|  |                             className="h-full" | ||||||
|  |                             code={code} | ||||||
|  |                             language={'shell'} | ||||||
|  |                         /> | ||||||
|  |                     </div> | ||||||
|  |                 ) : null} | ||||||
|  |                 {example ? ( | ||||||
|  |                     <> | ||||||
|  |                         <div className="my-2">Example:</div> | ||||||
|  |                         <div className="h-[60px]"> | ||||||
|  |                             <CodeSnippet | ||||||
|  |                                 className="h-full" | ||||||
|  |                                 code={example} | ||||||
|  |                                 language={'shell'} | ||||||
|  |                             /> | ||||||
|  |                         </div> | ||||||
|  |                     </> | ||||||
|  |                 ) : null} | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @@ -0,0 +1,118 @@ | |||||||
|  | import React from 'react'; | ||||||
|  | import { DatabaseType } from '@/lib/domain/database-type'; | ||||||
|  | import type { DatabaseEdition } from '@/lib/domain/database-edition'; | ||||||
|  | import { DDLInstructionStep } from './ddl-instruction-step'; | ||||||
|  |  | ||||||
|  | interface DDLInstruction { | ||||||
|  |     text: string; | ||||||
|  |     code?: string; | ||||||
|  |     example?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const DDLInstructionsMap: Record<DatabaseType, DDLInstruction[]> = { | ||||||
|  |     [DatabaseType.GENERIC]: [], | ||||||
|  |     [DatabaseType.MYSQL]: [ | ||||||
|  |         { | ||||||
|  |             text: 'Install mysqldump.', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             text: 'Execute the following command in your terminal (prefix with sudo on Linux if needed):', | ||||||
|  |             code: `mysqldump -h <host> -u <username>\n-P <port> -p --no-data\n<database_name> > <output_path>`, | ||||||
|  |             example: `mysqldump -h localhost -u root -P\n3306 -p --no-data my_db >\nschema_export.sql`, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             text: 'Open the exported SQL file, copy its contents, and paste them here.', | ||||||
|  |         }, | ||||||
|  |     ], | ||||||
|  |     [DatabaseType.POSTGRESQL]: [ | ||||||
|  |         { | ||||||
|  |             text: 'Install pg_dump.', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             text: 'Execute the following command in your terminal (prefix with sudo on Linux if needed):', | ||||||
|  |             code: `pg_dump -h <host> -p <port> -d <database_name> \n  -U <username> -s -F p -E UTF-8 \n  -f <output_file_path>`, | ||||||
|  |             example: `pg_dump -h localhost -p 5432 -d my_db \n  -U postgres -s -F p -E UTF-8 \n  -f schema_export.sql`, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             text: 'Open the exported SQL file, copy its contents, and paste them here.', | ||||||
|  |         }, | ||||||
|  |     ], | ||||||
|  |     [DatabaseType.SQLITE]: [ | ||||||
|  |         { | ||||||
|  |             text: 'Install sqlite3.', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             text: 'Execute the following command in your terminal:', | ||||||
|  |             code: `sqlite3 <database_file_path>\n.dump > <output_file_path>`, | ||||||
|  |             example: `sqlite3 my_db.db\n.dump > schema_export.sql`, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             text: 'Open the exported SQL file, copy its contents, and paste them here.', | ||||||
|  |         }, | ||||||
|  |     ], | ||||||
|  |     [DatabaseType.SQL_SERVER]: [ | ||||||
|  |         { | ||||||
|  |             text: 'Download and install SQL Server Management Studio (SSMS).', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             text: 'Connect to your SQL Server instance using SSMS.', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             text: 'Right-click on the database you want to export and select Script Database as > CREATE To > New Query Editor Window.', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             text: 'Copy the generated script and paste it here.', | ||||||
|  |         }, | ||||||
|  |     ], | ||||||
|  |     [DatabaseType.CLICKHOUSE]: [], | ||||||
|  |     [DatabaseType.COCKROACHDB]: [ | ||||||
|  |         { | ||||||
|  |             text: 'Install pg_dump.', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             text: 'Execute the following command in your terminal (prefix with sudo on Linux if needed):', | ||||||
|  |             code: `pg_dump -h <host> -p <port> -d <database_name> \n  -U <username> -s -F p -E UTF-8 \n  -f <output_file_path>`, | ||||||
|  |             example: `pg_dump -h localhost -p 5432 -d my_db \n  -U postgres -s -F p -E UTF-8 \n  -f schema_export.sql`, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             text: 'Open the exported SQL file, copy its contents, and paste them here.', | ||||||
|  |         }, | ||||||
|  |     ], | ||||||
|  |     [DatabaseType.MARIADB]: [ | ||||||
|  |         { | ||||||
|  |             text: 'Install mysqldump.', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             text: 'Execute the following command in your terminal (prefix with sudo on Linux if needed):', | ||||||
|  |             code: `mysqldump -h <host> -u <username>\n-P <port> -p --no-data\n<database_name> > <output_path>`, | ||||||
|  |             example: `mysqldump -h localhost -u root -P\n3306 -p --no-data my_db >\nschema_export.sql`, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             text: 'Open the exported SQL file, copy its contents, and paste them here.', | ||||||
|  |         }, | ||||||
|  |     ], | ||||||
|  |     [DatabaseType.ORACLE]: [], | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export interface DDLInstructionsProps { | ||||||
|  |     databaseType: DatabaseType; | ||||||
|  |     databaseEdition?: DatabaseEdition; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const DDLInstructions: React.FC<DDLInstructionsProps> = ({ | ||||||
|  |     databaseType, | ||||||
|  | }) => { | ||||||
|  |     return ( | ||||||
|  |         <> | ||||||
|  |             {DDLInstructionsMap[databaseType].map((instruction, index) => ( | ||||||
|  |                 <DDLInstructionStep | ||||||
|  |                     key={index} | ||||||
|  |                     index={index + 1} | ||||||
|  |                     text={instruction.text} | ||||||
|  |                     code={instruction.code} | ||||||
|  |                     example={instruction.example} | ||||||
|  |                 /> | ||||||
|  |             ))} | ||||||
|  |         </> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @@ -0,0 +1,147 @@ | |||||||
|  | import React, { useEffect, useMemo, useState } from 'react'; | ||||||
|  | import { DatabaseType } from '@/lib/domain/database-type'; | ||||||
|  | import { CodeSnippet } from '@/components/code-snippet/code-snippet'; | ||||||
|  | import type { DatabaseEdition } from '@/lib/domain/database-edition'; | ||||||
|  | import { SSMSInfo } from './ssms-info/ssms-info'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { Tabs, TabsList, TabsTrigger } from '@/components/tabs/tabs'; | ||||||
|  | import type { DatabaseClient } from '@/lib/domain/database-clients'; | ||||||
|  | import { minimizeQuery } from '@/lib/data/import-metadata/utils'; | ||||||
|  | import { | ||||||
|  |     databaseClientToLabelMap, | ||||||
|  |     databaseTypeToClientsMap, | ||||||
|  |     databaseEditionToClientsMap, | ||||||
|  | } from '@/lib/domain/database-clients'; | ||||||
|  | import type { ImportMetadataScripts } from '@/lib/data/import-metadata/scripts/scripts'; | ||||||
|  |  | ||||||
|  | export interface SmartQueryInstructionsProps { | ||||||
|  |     databaseType: DatabaseType; | ||||||
|  |     databaseEdition?: DatabaseEdition; | ||||||
|  |     showSSMSInfoDialog: boolean; | ||||||
|  |     setShowSSMSInfoDialog: (show: boolean) => void; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const SmartQueryInstructions: React.FC<SmartQueryInstructionsProps> = ({ | ||||||
|  |     databaseType, | ||||||
|  |     databaseEdition, | ||||||
|  |     showSSMSInfoDialog, | ||||||
|  |     setShowSSMSInfoDialog, | ||||||
|  | }) => { | ||||||
|  |     const databaseClients = useMemo( | ||||||
|  |         () => [ | ||||||
|  |             ...databaseTypeToClientsMap[databaseType], | ||||||
|  |             ...(databaseEdition | ||||||
|  |                 ? databaseEditionToClientsMap[databaseEdition] | ||||||
|  |                 : []), | ||||||
|  |         ], | ||||||
|  |         [databaseType, databaseEdition] | ||||||
|  |     ); | ||||||
|  |     const [databaseClient, setDatabaseClient] = useState< | ||||||
|  |         DatabaseClient | undefined | ||||||
|  |     >(); | ||||||
|  |     const { t } = useTranslation(); | ||||||
|  |     const [importMetadataScripts, setImportMetadataScripts] = | ||||||
|  |         useState<ImportMetadataScripts | null>(null); | ||||||
|  |  | ||||||
|  |     const code = useMemo( | ||||||
|  |         () => | ||||||
|  |             (databaseClients.length > 0 | ||||||
|  |                 ? importMetadataScripts?.[databaseType]?.({ | ||||||
|  |                       databaseEdition, | ||||||
|  |                       databaseClient, | ||||||
|  |                   }) | ||||||
|  |                 : importMetadataScripts?.[databaseType]?.({ | ||||||
|  |                       databaseEdition, | ||||||
|  |                   })) ?? '', | ||||||
|  |         [ | ||||||
|  |             databaseType, | ||||||
|  |             databaseEdition, | ||||||
|  |             databaseClients, | ||||||
|  |             importMetadataScripts, | ||||||
|  |             databaseClient, | ||||||
|  |         ] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     useEffect(() => { | ||||||
|  |         const loadScripts = async () => { | ||||||
|  |             const { importMetadataScripts } = await import( | ||||||
|  |                 '@/lib/data/import-metadata/scripts/scripts' | ||||||
|  |             ); | ||||||
|  |             setImportMetadataScripts(importMetadataScripts); | ||||||
|  |         }; | ||||||
|  |         loadScripts(); | ||||||
|  |     }, []); | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |         <> | ||||||
|  |             <div className="flex flex-col gap-1"> | ||||||
|  |                 <div className="flex flex-col gap-1 text-sm text-primary"> | ||||||
|  |                     <div> | ||||||
|  |                         <span className="font-medium">1.</span>{' '} | ||||||
|  |                         {t('new_diagram_dialog.import_database.step_1')} | ||||||
|  |                     </div> | ||||||
|  |                     {databaseType === DatabaseType.SQL_SERVER && ( | ||||||
|  |                         <SSMSInfo | ||||||
|  |                             open={showSSMSInfoDialog} | ||||||
|  |                             setOpen={setShowSSMSInfoDialog} | ||||||
|  |                         /> | ||||||
|  |                     )} | ||||||
|  |                 </div> | ||||||
|  |                 {databaseClients.length > 0 ? ( | ||||||
|  |                     <Tabs | ||||||
|  |                         value={!databaseClient ? 'dbclient' : databaseClient} | ||||||
|  |                         onValueChange={(value) => { | ||||||
|  |                             setDatabaseClient( | ||||||
|  |                                 value === 'dbclient' | ||||||
|  |                                     ? undefined | ||||||
|  |                                     : (value as DatabaseClient) | ||||||
|  |                             ); | ||||||
|  |                         }} | ||||||
|  |                     > | ||||||
|  |                         <div className="flex flex-1"> | ||||||
|  |                             <TabsList className="h-8 justify-start rounded-none rounded-t-sm "> | ||||||
|  |                                 <TabsTrigger | ||||||
|  |                                     value="dbclient" | ||||||
|  |                                     className="h-6 w-20" | ||||||
|  |                                 > | ||||||
|  |                                     DB Client | ||||||
|  |                                 </TabsTrigger> | ||||||
|  |  | ||||||
|  |                                 {databaseClients?.map((client) => ( | ||||||
|  |                                     <TabsTrigger | ||||||
|  |                                         key={client} | ||||||
|  |                                         value={client} | ||||||
|  |                                         className="h-6 !w-20" | ||||||
|  |                                     > | ||||||
|  |                                         {databaseClientToLabelMap[client]} | ||||||
|  |                                     </TabsTrigger> | ||||||
|  |                                 )) ?? []} | ||||||
|  |                             </TabsList> | ||||||
|  |                         </div> | ||||||
|  |                         <CodeSnippet | ||||||
|  |                             className="h-40 w-full md:h-[200px]" | ||||||
|  |                             loading={!importMetadataScripts} | ||||||
|  |                             code={minimizeQuery(code)} | ||||||
|  |                             codeToCopy={code} | ||||||
|  |                             language={databaseClient ? 'shell' : 'sql'} | ||||||
|  |                         /> | ||||||
|  |                     </Tabs> | ||||||
|  |                 ) : ( | ||||||
|  |                     <CodeSnippet | ||||||
|  |                         className="h-40 w-full flex-auto md:h-[200px]" | ||||||
|  |                         loading={!importMetadataScripts} | ||||||
|  |                         code={minimizeQuery(code)} | ||||||
|  |                         codeToCopy={code} | ||||||
|  |                         language="sql" | ||||||
|  |                     /> | ||||||
|  |                 )} | ||||||
|  |             </div> | ||||||
|  |             <div className="flex flex-col gap-1"> | ||||||
|  |                 <p className="text-sm text-primary"> | ||||||
|  |                     <span className="font-medium">2.</span>{' '} | ||||||
|  |                     {t('new_diagram_dialog.import_database.step_2')} | ||||||
|  |                 </p> | ||||||
|  |             </div> | ||||||
|  |         </> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @@ -4,32 +4,55 @@ import { | |||||||
|     HoverCardTrigger, |     HoverCardTrigger, | ||||||
| } from '@/components/hover-card/hover-card'; | } from '@/components/hover-card/hover-card'; | ||||||
| import { Label } from '@/components/label/label'; | import { Label } from '@/components/label/label'; | ||||||
| import { Info } from 'lucide-react'; | import { Info, X } from 'lucide-react'; | ||||||
| import React from 'react'; | import React, { useCallback, useEffect, useMemo } from 'react'; | ||||||
| import SSMSInstructions from '@/assets/ssms-instructions.png'; | import SSMSInstructions from '@/assets/ssms-instructions.png'; | ||||||
| import { ZoomableImage } from '@/components/zoomable-image/zoomable-image'; | import { ZoomableImage } from '@/components/zoomable-image/zoomable-image'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| 
 | 
 | ||||||
| export interface SSMSInfoProps {} | export interface SSMSInfoProps { | ||||||
|  |     open?: boolean; | ||||||
|  |     setOpen?: (open: boolean) => void; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| export const SSMSInfo = React.forwardRef< | export const SSMSInfo = React.forwardRef< | ||||||
|     React.ElementRef<typeof HoverCardTrigger>, |     React.ElementRef<typeof HoverCardTrigger>, | ||||||
|     SSMSInfoProps |     SSMSInfoProps | ||||||
| >((props, ref) => { | >(({ open: controlledOpen, setOpen: setControlledOpen }, ref) => { | ||||||
|     const [open, setOpen] = React.useState(false); |     const [open, setOpen] = React.useState(false); | ||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
|  | 
 | ||||||
|  |     useEffect(() => { | ||||||
|  |         if (controlledOpen) { | ||||||
|  |             setOpen(true); | ||||||
|  |         } | ||||||
|  |     }, [controlledOpen]); | ||||||
|  | 
 | ||||||
|  |     const closeHandler = useCallback(() => { | ||||||
|  |         setOpen(false); | ||||||
|  |         setControlledOpen?.(false); | ||||||
|  |     }, [setControlledOpen]); | ||||||
|  | 
 | ||||||
|  |     const isOpen = useMemo( | ||||||
|  |         () => open || controlledOpen, | ||||||
|  |         [open, controlledOpen] | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|     return ( |     return ( | ||||||
|         <HoverCard |         <HoverCard | ||||||
|             open={open} |             open={isOpen} | ||||||
|             onOpenChange={(isOpen) => { |             onOpenChange={(isOpen) => { | ||||||
|  |                 if (controlledOpen) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|                 setOpen(isOpen); |                 setOpen(isOpen); | ||||||
|             }} |             }} | ||||||
|         > |         > | ||||||
|             <HoverCardTrigger ref={ref} {...props} asChild> |             <HoverCardTrigger ref={ref} asChild> | ||||||
|                 <div |                 <div | ||||||
|                     className="flex flex-row items-center gap-1 text-pink-600" |                     className="flex flex-row items-center gap-1 text-pink-600" | ||||||
|                     onClick={() => { |                     onClick={() => { | ||||||
|                         setOpen(!open); |                         setOpen?.(!open); | ||||||
|                     }} |                     }} | ||||||
|                 > |                 > | ||||||
|                     <Info size={14} /> |                     <Info size={14} /> | ||||||
| @@ -41,13 +64,21 @@ export const SSMSInfo = React.forwardRef< | |||||||
|                 </div> |                 </div> | ||||||
|             </HoverCardTrigger> |             </HoverCardTrigger> | ||||||
|             <HoverCardContent className="w-80"> |             <HoverCardContent className="w-80"> | ||||||
|                 <div className="flex"> |                 <div className="flex flex-col"> | ||||||
|                     <div className="space-y-1"> |                     <div className="flex items-start justify-between"> | ||||||
|                         <h4 className="text-sm font-semibold"> |                         <h4 className="text-sm font-semibold"> | ||||||
|                             {t( |                             {t( | ||||||
|                                 'new_diagram_dialog.import_database.ssms_instructions.title' |                                 'new_diagram_dialog.import_database.ssms_instructions.title' | ||||||
|                             )} |                             )} | ||||||
|                         </h4> |                         </h4> | ||||||
|  |                         <button | ||||||
|  |                             onClick={closeHandler} | ||||||
|  |                             className="text-muted-foreground hover:text-foreground" | ||||||
|  |                         > | ||||||
|  |                             <X size={16} /> | ||||||
|  |                         </button> | ||||||
|  |                     </div> | ||||||
|  |                     <div className="space-y-1"> | ||||||
|                         <p className="text-xs text-muted-foreground"> |                         <p className="text-xs text-muted-foreground"> | ||||||
|                             <span className="font-semibold">1. </span> |                             <span className="font-semibold">1. </span> | ||||||
|                             {t( |                             {t( | ||||||
| @@ -17,6 +17,7 @@ import { CreateDiagramDialogStep } from './create-diagram-dialog-step'; | |||||||
| import { ImportDatabase } from '../common/import-database/import-database'; | import { ImportDatabase } from '../common/import-database/import-database'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||||
|  | import { sqlImportToDiagram } from '@/lib/data/sql-import'; | ||||||
|  |  | ||||||
| export interface CreateDiagramDialogProps extends BaseDialogProps {} | export interface CreateDiagramDialogProps extends BaseDialogProps {} | ||||||
|  |  | ||||||
| @@ -25,10 +26,11 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({ | |||||||
| }) => { | }) => { | ||||||
|     const { diagramId } = useChartDB(); |     const { diagramId } = useChartDB(); | ||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
|  |     const [importMethod, setImportMethod] = useState<'query' | 'ddl'>('query'); | ||||||
|     const [databaseType, setDatabaseType] = useState<DatabaseType>( |     const [databaseType, setDatabaseType] = useState<DatabaseType>( | ||||||
|         DatabaseType.GENERIC |         DatabaseType.GENERIC | ||||||
|     ); |     ); | ||||||
|     const { closeCreateDiagramDialog } = useDialog(); |     const { closeCreateDiagramDialog, openImportDBMLDialog } = useDialog(); | ||||||
|     const { updateConfig } = useConfig(); |     const { updateConfig } = useConfig(); | ||||||
|     const [scriptResult, setScriptResult] = useState(''); |     const [scriptResult, setScriptResult] = useState(''); | ||||||
|     const [databaseEdition, setDatabaseEdition] = useState< |     const [databaseEdition, setDatabaseEdition] = useState< | ||||||
| @@ -41,6 +43,11 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({ | |||||||
|     const [diagramNumber, setDiagramNumber] = useState<number>(1); |     const [diagramNumber, setDiagramNumber] = useState<number>(1); | ||||||
|     const navigate = useNavigate(); |     const navigate = useNavigate(); | ||||||
|  |  | ||||||
|  |     useEffect(() => { | ||||||
|  |         setDatabaseEdition(undefined); | ||||||
|  |         setImportMethod('query'); | ||||||
|  |     }, [databaseType]); | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         const fetchDiagrams = async () => { |         const fetchDiagrams = async () => { | ||||||
|             const diagrams = await listDiagrams(); |             const diagrams = await listDiagrams(); | ||||||
| @@ -54,29 +61,41 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({ | |||||||
|         setDatabaseType(DatabaseType.GENERIC); |         setDatabaseType(DatabaseType.GENERIC); | ||||||
|         setDatabaseEdition(undefined); |         setDatabaseEdition(undefined); | ||||||
|         setScriptResult(''); |         setScriptResult(''); | ||||||
|  |         setImportMethod('query'); | ||||||
|     }, [dialog.open]); |     }, [dialog.open]); | ||||||
|  |  | ||||||
|     const hasExistingDiagram = (diagramId ?? '').trim().length !== 0; |     const hasExistingDiagram = (diagramId ?? '').trim().length !== 0; | ||||||
|  |  | ||||||
|     const importNewDiagram = useCallback(async () => { |     const importNewDiagram = useCallback(async () => { | ||||||
|         const databaseMetadata: DatabaseMetadata = |         let diagram: Diagram | undefined; | ||||||
|             loadDatabaseMetadata(scriptResult); |  | ||||||
|  |  | ||||||
|         const diagram = await loadFromDatabaseMetadata({ |         if (importMethod === 'ddl') { | ||||||
|             databaseType, |             diagram = await sqlImportToDiagram({ | ||||||
|             databaseMetadata, |                 sqlContent: scriptResult, | ||||||
|             diagramNumber, |                 sourceDatabaseType: databaseType, | ||||||
|             databaseEdition: |                 targetDatabaseType: databaseType, | ||||||
|                 databaseEdition?.trim().length === 0 |             }); | ||||||
|                     ? undefined |         } else { | ||||||
|                     : databaseEdition, |             const databaseMetadata: DatabaseMetadata = | ||||||
|         }); |                 loadDatabaseMetadata(scriptResult); | ||||||
|  |  | ||||||
|  |             diagram = await loadFromDatabaseMetadata({ | ||||||
|  |                 databaseType, | ||||||
|  |                 databaseMetadata, | ||||||
|  |                 diagramNumber, | ||||||
|  |                 databaseEdition: | ||||||
|  |                     databaseEdition?.trim().length === 0 | ||||||
|  |                         ? undefined | ||||||
|  |                         : databaseEdition, | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         await addDiagram({ diagram }); |         await addDiagram({ diagram }); | ||||||
|         await updateConfig({ defaultDiagramId: diagram.id }); |         await updateConfig({ config: { defaultDiagramId: diagram.id } }); | ||||||
|         closeCreateDiagramDialog(); |         closeCreateDiagramDialog(); | ||||||
|         navigate(`/diagrams/${diagram.id}`); |         navigate(`/diagrams/${diagram.id}`); | ||||||
|     }, [ |     }, [ | ||||||
|  |         importMethod, | ||||||
|         databaseType, |         databaseType, | ||||||
|         addDiagram, |         addDiagram, | ||||||
|         databaseEdition, |         databaseEdition, | ||||||
| @@ -101,9 +120,13 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({ | |||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         await addDiagram({ diagram }); |         await addDiagram({ diagram }); | ||||||
|         await updateConfig({ defaultDiagramId: diagram.id }); |         await updateConfig({ config: { defaultDiagramId: diagram.id } }); | ||||||
|         closeCreateDiagramDialog(); |         closeCreateDiagramDialog(); | ||||||
|         navigate(`/diagrams/${diagram.id}`); |         navigate(`/diagrams/${diagram.id}`); | ||||||
|  |         setTimeout( | ||||||
|  |             () => openImportDBMLDialog({ withCreateEmptyDiagram: true }), | ||||||
|  |             700 | ||||||
|  |         ); | ||||||
|     }, [ |     }, [ | ||||||
|         databaseType, |         databaseType, | ||||||
|         addDiagram, |         addDiagram, | ||||||
| @@ -112,6 +135,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({ | |||||||
|         navigate, |         navigate, | ||||||
|         updateConfig, |         updateConfig, | ||||||
|         diagramNumber, |         diagramNumber, | ||||||
|  |         openImportDBMLDialog, | ||||||
|     ]); |     ]); | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
| @@ -128,7 +152,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({ | |||||||
|             }} |             }} | ||||||
|         > |         > | ||||||
|             <DialogContent |             <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} |                 showClose={hasExistingDiagram} | ||||||
|             > |             > | ||||||
|                 {step === CreateDiagramDialogStep.SELECT_DATABASE ? ( |                 {step === CreateDiagramDialogStep.SELECT_DATABASE ? ( | ||||||
| @@ -154,6 +178,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({ | |||||||
|                         } |                         } | ||||||
|                         setScriptResult={setScriptResult} |                         setScriptResult={setScriptResult} | ||||||
|                         title={t('new_diagram_dialog.import_database.title')} |                         title={t('new_diagram_dialog.import_database.title')} | ||||||
|  |                         importMethod={importMethod} | ||||||
|  |                         setImportMethod={setImportMethod} | ||||||
|                     /> |                     /> | ||||||
|                 )} |                 )} | ||||||
|             </DialogContent> |             </DialogContent> | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ const SUPPORTED_DB_TYPES: DatabaseType[] = [ | |||||||
|     DatabaseType.MARIADB, |     DatabaseType.MARIADB, | ||||||
|     DatabaseType.SQLITE, |     DatabaseType.SQLITE, | ||||||
|     DatabaseType.SQL_SERVER, |     DatabaseType.SQL_SERVER, | ||||||
|  |     DatabaseType.ORACLE, | ||||||
|     DatabaseType.COCKROACHDB, |     DatabaseType.COCKROACHDB, | ||||||
|     DatabaseType.CLICKHOUSE, |     DatabaseType.CLICKHOUSE, | ||||||
| ]; | ]; | ||||||
|   | |||||||
| @@ -22,13 +22,17 @@ import { areFieldTypesCompatible } from '@/lib/data/data-types/data-types'; | |||||||
| const ErrorMessageRelationshipFieldsNotSameType = | const ErrorMessageRelationshipFieldsNotSameType = | ||||||
|     'Relationships can only be created between fields of the same type'; |     'Relationships can only be created between fields of the same type'; | ||||||
|  |  | ||||||
| export interface CreateRelationshipDialogProps extends BaseDialogProps {} | export interface CreateRelationshipDialogProps extends BaseDialogProps { | ||||||
|  |     sourceTableId?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
| export const CreateRelationshipDialog: React.FC< | export const CreateRelationshipDialog: React.FC< | ||||||
|     CreateRelationshipDialogProps |     CreateRelationshipDialogProps | ||||||
| > = ({ dialog }) => { | > = ({ dialog, sourceTableId: preSelectedSourceTableId }) => { | ||||||
|     const { closeCreateRelationshipDialog } = useDialog(); |     const { closeCreateRelationshipDialog } = useDialog(); | ||||||
|     const [primaryTableId, setPrimaryTableId] = useState<string | undefined>(); |     const [primaryTableId, setPrimaryTableId] = useState<string | undefined>( | ||||||
|  |         preSelectedSourceTableId | ||||||
|  |     ); | ||||||
|     const [primaryFieldId, setPrimaryFieldId] = useState<string | undefined>(); |     const [primaryFieldId, setPrimaryFieldId] = useState<string | undefined>(); | ||||||
|     const [referencedTableId, setReferencedTableId] = useState< |     const [referencedTableId, setReferencedTableId] = useState< | ||||||
|         string | undefined |         string | undefined | ||||||
| @@ -43,6 +47,9 @@ export const CreateRelationshipDialog: React.FC< | |||||||
|     const [canCreateRelationship, setCanCreateRelationship] = useState(false); |     const [canCreateRelationship, setCanCreateRelationship] = useState(false); | ||||||
|     const { fitView, setEdges } = useReactFlow(); |     const { fitView, setEdges } = useReactFlow(); | ||||||
|     const { databaseType } = useChartDB(); |     const { databaseType } = useChartDB(); | ||||||
|  |     const [primaryFieldSelectOpen, setPrimaryFieldSelectOpen] = useState(false); | ||||||
|  |     const [referencedTableSelectOpen, setReferencedTableSelectOpen] = | ||||||
|  |         useState(false); | ||||||
|  |  | ||||||
|     const tableOptions = useMemo(() => { |     const tableOptions = useMemo(() => { | ||||||
|         return tables.map( |         return tables.map( | ||||||
| @@ -89,8 +96,23 @@ export const CreateRelationshipDialog: React.FC< | |||||||
|         setReferencedTableId(undefined); |         setReferencedTableId(undefined); | ||||||
|         setReferencedFieldId(undefined); |         setReferencedFieldId(undefined); | ||||||
|         setErrorMessage(''); |         setErrorMessage(''); | ||||||
|  |         setPrimaryFieldSelectOpen(false); | ||||||
|  |         setReferencedTableSelectOpen(false); | ||||||
|     }, [dialog.open]); |     }, [dialog.open]); | ||||||
|  |  | ||||||
|  |     useEffect(() => { | ||||||
|  |         if (preSelectedSourceTableId) { | ||||||
|  |             const table = getTable(preSelectedSourceTableId); | ||||||
|  |             if (table) { | ||||||
|  |                 setPrimaryTableId(preSelectedSourceTableId); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             setTimeout(() => { | ||||||
|  |                 setPrimaryFieldSelectOpen(true); | ||||||
|  |             }, 100); | ||||||
|  |         } | ||||||
|  |     }, [preSelectedSourceTableId, getTable]); | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         setCanCreateRelationship(false); |         setCanCreateRelationship(false); | ||||||
|         setErrorMessage(''); |         setErrorMessage(''); | ||||||
| @@ -223,8 +245,14 @@ export const CreateRelationshipDialog: React.FC< | |||||||
|                                     )} |                                     )} | ||||||
|                                     value={primaryTableId} |                                     value={primaryTableId} | ||||||
|                                     onChange={(value) => { |                                     onChange={(value) => { | ||||||
|                                         setPrimaryTableId(value as string); |                                         const newTableId = value as string; | ||||||
|                                         setPrimaryFieldId(undefined); |                                         setPrimaryTableId(newTableId); | ||||||
|  |                                         if ( | ||||||
|  |                                             newTableId !== | ||||||
|  |                                             preSelectedSourceTableId | ||||||
|  |                                         ) { | ||||||
|  |                                             setPrimaryFieldId(undefined); | ||||||
|  |                                         } | ||||||
|                                     }} |                                     }} | ||||||
|                                     emptyPlaceholder={t( |                                     emptyPlaceholder={t( | ||||||
|                                         'create_relationship_dialog.no_tables_found' |                                         'create_relationship_dialog.no_tables_found' | ||||||
| @@ -253,6 +281,8 @@ export const CreateRelationshipDialog: React.FC< | |||||||
|                                             'create_relationship_dialog.primary_field_placeholder' |                                             'create_relationship_dialog.primary_field_placeholder' | ||||||
|                                         )} |                                         )} | ||||||
|                                         value={primaryFieldId} |                                         value={primaryFieldId} | ||||||
|  |                                         open={primaryFieldSelectOpen} | ||||||
|  |                                         onOpenChange={setPrimaryFieldSelectOpen} | ||||||
|                                         onChange={(value) => |                                         onChange={(value) => | ||||||
|                                             setPrimaryFieldId(value as string) |                                             setPrimaryFieldId(value as string) | ||||||
|                                         } |                                         } | ||||||
| @@ -283,6 +313,8 @@ export const CreateRelationshipDialog: React.FC< | |||||||
|                                         'create_relationship_dialog.referenced_table_placeholder' |                                         'create_relationship_dialog.referenced_table_placeholder' | ||||||
|                                     )} |                                     )} | ||||||
|                                     value={referencedTableId} |                                     value={referencedTableId} | ||||||
|  |                                     open={referencedTableSelectOpen} | ||||||
|  |                                     onOpenChange={setReferencedTableSelectOpen} | ||||||
|                                     onChange={(value) => { |                                     onChange={(value) => { | ||||||
|                                         setReferencedTableId(value as string); |                                         setReferencedTableId(value as string); | ||||||
|                                         setReferencedFieldId(undefined); |                                         setReferencedFieldId(undefined); | ||||||
|   | |||||||
| @@ -15,11 +15,10 @@ import { SelectBox } from '@/components/select-box/select-box'; | |||||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { useChartDB } from '@/hooks/use-chartdb'; | import { useChartDB } from '@/hooks/use-chartdb'; | ||||||
| import { diagramToJSONOutput } from '@/lib/export-import-utils'; |  | ||||||
| import { Spinner } from '@/components/spinner/spinner'; | import { Spinner } from '@/components/spinner/spinner'; | ||||||
| import { waitFor } from '@/lib/utils'; |  | ||||||
| import { AlertCircle } from 'lucide-react'; | import { AlertCircle } from 'lucide-react'; | ||||||
| import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert'; | import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert'; | ||||||
|  | import { useExportDiagram } from '@/hooks/use-export-diagram'; | ||||||
|  |  | ||||||
| export interface ExportDiagramDialogProps extends BaseDialogProps {} | export interface ExportDiagramDialogProps extends BaseDialogProps {} | ||||||
|  |  | ||||||
| @@ -27,44 +26,27 @@ export const ExportDiagramDialog: React.FC<ExportDiagramDialogProps> = ({ | |||||||
|     dialog, |     dialog, | ||||||
| }) => { | }) => { | ||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
|     const { diagramName, currentDiagram } = useChartDB(); |     const { currentDiagram } = useChartDB(); | ||||||
|     const [isLoading, setIsLoading] = useState(false); |  | ||||||
|     const { closeExportDiagramDialog } = useDialog(); |     const { closeExportDiagramDialog } = useDialog(); | ||||||
|     const [error, setError] = useState(false); |     const [error, setError] = useState(false); | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         if (!dialog.open) return; |         if (!dialog.open) return; | ||||||
|         setIsLoading(false); |  | ||||||
|         setError(false); |         setError(false); | ||||||
|     }, [dialog.open]); |     }, [dialog.open]); | ||||||
|  |  | ||||||
|     const downloadOutput = useCallback( |     const { exportDiagram, isExporting: isLoading } = useExportDiagram(); | ||||||
|         (dataUrl: string) => { |  | ||||||
|             const a = document.createElement('a'); |  | ||||||
|             a.setAttribute('download', `ChartDB(${diagramName}).json`); |  | ||||||
|             a.setAttribute('href', dataUrl); |  | ||||||
|             a.click(); |  | ||||||
|         }, |  | ||||||
|         [diagramName] |  | ||||||
|     ); |  | ||||||
|  |  | ||||||
|     const handleExport = useCallback(async () => { |     const handleExport = useCallback(async () => { | ||||||
|         setIsLoading(true); |  | ||||||
|         await waitFor(1000); |  | ||||||
|         try { |         try { | ||||||
|             const json = diagramToJSONOutput(currentDiagram); |             await exportDiagram({ diagram: currentDiagram }); | ||||||
|             const blob = new Blob([json], { type: 'application/json' }); |  | ||||||
|             const dataUrl = URL.createObjectURL(blob); |  | ||||||
|             downloadOutput(dataUrl); |  | ||||||
|             setIsLoading(false); |  | ||||||
|             closeExportDiagramDialog(); |             closeExportDiagramDialog(); | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             setError(true); |             setError(true); | ||||||
|             setIsLoading(false); |  | ||||||
|  |  | ||||||
|             throw e; |             throw e; | ||||||
|         } |         } | ||||||
|     }, [downloadOutput, currentDiagram, closeExportDiagramDialog]); |     }, [exportDiagram, currentDiagram, closeExportDiagramDialog]); | ||||||
|  |  | ||||||
|     const outputTypeOptions: SelectBoxOption[] = useMemo( |     const outputTypeOptions: SelectBoxOption[] = useMemo( | ||||||
|         () => |         () => | ||||||
|   | |||||||
| @@ -16,11 +16,20 @@ import type { BaseDialogProps } from '../common/base-dialog-props'; | |||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import type { ImageType } from '@/context/export-image-context/export-image-context'; | import type { ImageType } from '@/context/export-image-context/export-image-context'; | ||||||
| import { useExportImage } from '@/hooks/use-export-image'; | 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 { | export interface ExportImageDialogProps extends BaseDialogProps { | ||||||
|     format: ImageType; |     format: ImageType; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | const DEFAULT_INCLUDE_PATTERN_BG = true; | ||||||
|  | const DEFAULT_TRANSPARENT = false; | ||||||
| const DEFAULT_SCALE = '2'; | const DEFAULT_SCALE = '2'; | ||||||
| export const ExportImageDialog: React.FC<ExportImageDialogProps> = ({ | export const ExportImageDialog: React.FC<ExportImageDialogProps> = ({ | ||||||
|     dialog, |     dialog, | ||||||
| @@ -28,17 +37,28 @@ export const ExportImageDialog: React.FC<ExportImageDialogProps> = ({ | |||||||
| }) => { | }) => { | ||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
|     const [scale, setScale] = useState<string>(DEFAULT_SCALE); |     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(); |     const { exportImage } = useExportImage(); | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         if (!dialog.open) return; |         if (!dialog.open) return; | ||||||
|         setScale(DEFAULT_SCALE); |         setScale(DEFAULT_SCALE); | ||||||
|  |         setIncludePatternBG(DEFAULT_INCLUDE_PATTERN_BG); | ||||||
|  |         setTransparent(DEFAULT_TRANSPARENT); | ||||||
|     }, [dialog.open]); |     }, [dialog.open]); | ||||||
|     const { closeExportImageDialog } = useDialog(); |     const { closeExportImageDialog } = useDialog(); | ||||||
|  |  | ||||||
|     const handleExport = useCallback(() => { |     const handleExport = useCallback(() => { | ||||||
|         exportImage(format, Number(scale)); |         exportImage(format, { | ||||||
|     }, [exportImage, format, scale]); |             transparent, | ||||||
|  |             includePatternBG, | ||||||
|  |             scale: Number(scale), | ||||||
|  |         }); | ||||||
|  |     }, [exportImage, format, includePatternBG, transparent, scale]); | ||||||
|  |  | ||||||
|     const scaleOptions: SelectBoxOption[] = useMemo( |     const scaleOptions: SelectBoxOption[] = useMemo( | ||||||
|         () => |         () => | ||||||
| @@ -65,15 +85,79 @@ export const ExportImageDialog: React.FC<ExportImageDialogProps> = ({ | |||||||
|                         {t('export_image_dialog.description')} |                         {t('export_image_dialog.description')} | ||||||
|                     </DialogDescription> |                     </DialogDescription> | ||||||
|                 </DialogHeader> |                 </DialogHeader> | ||||||
|                 <div className="grid gap-4 py-1"> |                 <div className="flex flex-col gap-4 py-1"> | ||||||
|                     <div className="grid w-full items-center gap-4"> |                     <SelectBox | ||||||
|                         <SelectBox |                         options={scaleOptions} | ||||||
|                             options={scaleOptions} |                         multiple={false} | ||||||
|                             multiple={false} |                         value={scale} | ||||||
|                             value={scale} |                         onChange={(value) => setScale(value as string)} | ||||||
|                             onChange={(value) => setScale(value as string)} |                     /> | ||||||
|                         /> |                     <Accordion type="single" collapsible className="w-full"> | ||||||
|                     </div> |                         <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> |                 </div> | ||||||
|                 <DialogFooter className="flex gap-1 md:justify-between"> |                 <DialogFooter className="flex gap-1 md:justify-between"> | ||||||
|                     <DialogClose asChild> |                     <DialogClose asChild> | ||||||
|   | |||||||
| @@ -20,10 +20,12 @@ import { | |||||||
| } from '@/lib/data/export-metadata/export-sql-script'; | } from '@/lib/data/export-metadata/export-sql-script'; | ||||||
| import { databaseTypeToLabelMap } from '@/lib/databases'; | import { databaseTypeToLabelMap } from '@/lib/databases'; | ||||||
| import { DatabaseType } from '@/lib/domain/database-type'; | import { DatabaseType } from '@/lib/domain/database-type'; | ||||||
|  | import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table'; | ||||||
| import { Annoyed, Sparkles } from 'lucide-react'; | import { Annoyed, Sparkles } from 'lucide-react'; | ||||||
| import React, { useCallback, useEffect, useRef } from 'react'; | import React, { useCallback, useEffect, useRef } from 'react'; | ||||||
| import { Trans, useTranslation } from 'react-i18next'; | import { Trans, useTranslation } from 'react-i18next'; | ||||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||||
|  | import type { Diagram } from '@/lib/domain/diagram'; | ||||||
|  |  | ||||||
| export interface ExportSQLDialogProps extends BaseDialogProps { | export interface ExportSQLDialogProps extends BaseDialogProps { | ||||||
|     targetDatabaseType: DatabaseType; |     targetDatabaseType: DatabaseType; | ||||||
| @@ -34,7 +36,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | |||||||
|     targetDatabaseType, |     targetDatabaseType, | ||||||
| }) => { | }) => { | ||||||
|     const { closeExportSQLDialog } = useDialog(); |     const { closeExportSQLDialog } = useDialog(); | ||||||
|     const { currentDiagram } = useChartDB(); |     const { currentDiagram, filteredSchemas } = useChartDB(); | ||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
|     const [script, setScript] = React.useState<string>(); |     const [script, setScript] = React.useState<string>(); | ||||||
|     const [error, setError] = React.useState<boolean>(false); |     const [error, setError] = React.useState<boolean>(false); | ||||||
| @@ -43,17 +45,63 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | |||||||
|     const abortControllerRef = useRef<AbortController | null>(null); |     const abortControllerRef = useRef<AbortController | null>(null); | ||||||
|  |  | ||||||
|     const exportSQLScript = useCallback(async () => { |     const exportSQLScript = useCallback(async () => { | ||||||
|  |         const filteredDiagram: Diagram = { | ||||||
|  |             ...currentDiagram, | ||||||
|  |             tables: currentDiagram.tables?.filter((table) => | ||||||
|  |                 shouldShowTablesBySchemaFilter(table, filteredSchemas) | ||||||
|  |             ), | ||||||
|  |             relationships: currentDiagram.relationships?.filter((rel) => { | ||||||
|  |                 const sourceTable = currentDiagram.tables?.find( | ||||||
|  |                     (t) => t.id === rel.sourceTableId | ||||||
|  |                 ); | ||||||
|  |                 const targetTable = currentDiagram.tables?.find( | ||||||
|  |                     (t) => t.id === rel.targetTableId | ||||||
|  |                 ); | ||||||
|  |                 return ( | ||||||
|  |                     sourceTable && | ||||||
|  |                     targetTable && | ||||||
|  |                     shouldShowTablesBySchemaFilter( | ||||||
|  |                         sourceTable, | ||||||
|  |                         filteredSchemas | ||||||
|  |                     ) && | ||||||
|  |                     shouldShowTablesBySchemaFilter(targetTable, filteredSchemas) | ||||||
|  |                 ); | ||||||
|  |             }), | ||||||
|  |             dependencies: currentDiagram.dependencies?.filter((dep) => { | ||||||
|  |                 const table = currentDiagram.tables?.find( | ||||||
|  |                     (t) => t.id === dep.tableId | ||||||
|  |                 ); | ||||||
|  |                 const dependentTable = currentDiagram.tables?.find( | ||||||
|  |                     (t) => t.id === dep.dependentTableId | ||||||
|  |                 ); | ||||||
|  |                 return ( | ||||||
|  |                     table && | ||||||
|  |                     dependentTable && | ||||||
|  |                     shouldShowTablesBySchemaFilter(table, filteredSchemas) && | ||||||
|  |                     shouldShowTablesBySchemaFilter( | ||||||
|  |                         dependentTable, | ||||||
|  |                         filteredSchemas | ||||||
|  |                     ) | ||||||
|  |                 ); | ||||||
|  |             }), | ||||||
|  |         }; | ||||||
|  |  | ||||||
|         if (targetDatabaseType === DatabaseType.GENERIC) { |         if (targetDatabaseType === DatabaseType.GENERIC) { | ||||||
|             return Promise.resolve(exportBaseSQL(currentDiagram)); |             return Promise.resolve( | ||||||
|  |                 exportBaseSQL({ | ||||||
|  |                     diagram: filteredDiagram, | ||||||
|  |                     targetDatabaseType, | ||||||
|  |                 }) | ||||||
|  |             ); | ||||||
|         } else { |         } else { | ||||||
|             return exportSQL(currentDiagram, targetDatabaseType, { |             return exportSQL(filteredDiagram, targetDatabaseType, { | ||||||
|                 stream: true, |                 stream: true, | ||||||
|                 onResultStream: (text) => |                 onResultStream: (text) => | ||||||
|                     setScript((prev) => (prev ? prev + text : text)), |                     setScript((prev) => (prev ? prev + text : text)), | ||||||
|                 signal: abortControllerRef.current?.signal, |                 signal: abortControllerRef.current?.signal, | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|     }, [targetDatabaseType, currentDiagram]); |     }, [targetDatabaseType, currentDiagram, filteredSchemas]); | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         if (!dialog.open) { |         if (!dialog.open) { | ||||||
| @@ -92,7 +140,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | |||||||
|                             components={[ |                             components={[ | ||||||
|                                 <a |                                 <a | ||||||
|                                     key={0} |                                     key={0} | ||||||
|                                     href="mailto:chartdb.io@gmail.com" |                                     href="mailto:support@chartdb.io" | ||||||
|                                     target="_blank" |                                     target="_blank" | ||||||
|                                     className="text-pink-600 hover:underline" |                                     className="text-pink-600 hover:underline" | ||||||
|                                     rel="noreferrer" |                                     rel="noreferrer" | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ import { ImportDatabase } from '../common/import-database/import-database'; | |||||||
| import type { DatabaseEdition } from '@/lib/domain/database-edition'; | import type { DatabaseEdition } from '@/lib/domain/database-edition'; | ||||||
| import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata'; | import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata'; | ||||||
| import { loadDatabaseMetadata } 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 { loadFromDatabaseMetadata } from '@/lib/domain/diagram'; | ||||||
| import { useChartDB } from '@/hooks/use-chartdb'; | import { useChartDB } from '@/hooks/use-chartdb'; | ||||||
| import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack'; | import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack'; | ||||||
| @@ -13,6 +14,7 @@ import { Trans, useTranslation } from 'react-i18next'; | |||||||
| import { useReactFlow } from '@xyflow/react'; | import { useReactFlow } from '@xyflow/react'; | ||||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||||
| import { useAlert } from '@/context/alert-context/alert-context'; | import { useAlert } from '@/context/alert-context/alert-context'; | ||||||
|  | import { sqlImportToDiagram } from '@/lib/data/sql-import'; | ||||||
|  |  | ||||||
| export interface ImportDatabaseDialogProps extends BaseDialogProps { | export interface ImportDatabaseDialogProps extends BaseDialogProps { | ||||||
|     databaseType: DatabaseType; |     databaseType: DatabaseType; | ||||||
| @@ -22,6 +24,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({ | |||||||
|     dialog, |     dialog, | ||||||
|     databaseType, |     databaseType, | ||||||
| }) => { | }) => { | ||||||
|  |     const [importMethod, setImportMethod] = useState<'query' | 'ddl'>('query'); | ||||||
|     const { closeImportDatabaseDialog } = useDialog(); |     const { closeImportDatabaseDialog } = useDialog(); | ||||||
|     const { showAlert } = useAlert(); |     const { showAlert } = useAlert(); | ||||||
|     const { |     const { | ||||||
| @@ -43,6 +46,10 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({ | |||||||
|         DatabaseEdition | undefined |         DatabaseEdition | undefined | ||||||
|     >(); |     >(); | ||||||
|  |  | ||||||
|  |     useEffect(() => { | ||||||
|  |         setDatabaseEdition(undefined); | ||||||
|  |     }, [databaseType]); | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         if (!dialog.open) return; |         if (!dialog.open) return; | ||||||
|         setDatabaseEdition(undefined); |         setDatabaseEdition(undefined); | ||||||
| @@ -50,17 +57,27 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({ | |||||||
|     }, [dialog.open]); |     }, [dialog.open]); | ||||||
|  |  | ||||||
|     const importDatabase = useCallback(async () => { |     const importDatabase = useCallback(async () => { | ||||||
|         const databaseMetadata: DatabaseMetadata = |         let diagram: Diagram | undefined; | ||||||
|             loadDatabaseMetadata(scriptResult); |  | ||||||
|  |  | ||||||
|         const diagram = await loadFromDatabaseMetadata({ |         if (importMethod === 'ddl') { | ||||||
|             databaseType, |             diagram = await sqlImportToDiagram({ | ||||||
|             databaseMetadata, |                 sqlContent: scriptResult, | ||||||
|             databaseEdition: |                 sourceDatabaseType: databaseType, | ||||||
|                 databaseEdition?.trim().length === 0 |                 targetDatabaseType: databaseType, | ||||||
|                     ? undefined |             }); | ||||||
|                     : databaseEdition, |         } else { | ||||||
|         }); |             const databaseMetadata: DatabaseMetadata = | ||||||
|  |                 loadDatabaseMetadata(scriptResult); | ||||||
|  |  | ||||||
|  |             diagram = await loadFromDatabaseMetadata({ | ||||||
|  |                 databaseType, | ||||||
|  |                 databaseMetadata, | ||||||
|  |                 databaseEdition: | ||||||
|  |                     databaseEdition?.trim().length === 0 | ||||||
|  |                         ? undefined | ||||||
|  |                         : databaseEdition, | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         const tableIdsToRemove = tables |         const tableIdsToRemove = tables | ||||||
|             .filter((table) => |             .filter((table) => | ||||||
| @@ -304,6 +321,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({ | |||||||
|  |  | ||||||
|         closeImportDatabaseDialog(); |         closeImportDatabaseDialog(); | ||||||
|     }, [ |     }, [ | ||||||
|  |         importMethod, | ||||||
|         databaseEdition, |         databaseEdition, | ||||||
|         currentDatabaseType, |         currentDatabaseType, | ||||||
|         updateDatabaseType, |         updateDatabaseType, | ||||||
| @@ -333,7 +351,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({ | |||||||
|             }} |             }} | ||||||
|         > |         > | ||||||
|             <DialogContent |             <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 |                 showClose | ||||||
|             > |             > | ||||||
|                 <ImportDatabase |                 <ImportDatabase | ||||||
| @@ -345,6 +363,8 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({ | |||||||
|                     setScriptResult={setScriptResult} |                     setScriptResult={setScriptResult} | ||||||
|                     keepDialogAfterImport |                     keepDialogAfterImport | ||||||
|                     title={t('import_database_dialog.title', { diagramName })} |                     title={t('import_database_dialog.title', { diagramName })} | ||||||
|  |                     importMethod={importMethod} | ||||||
|  |                     setImportMethod={setImportMethod} | ||||||
|                 /> |                 /> | ||||||
|             </DialogContent> |             </DialogContent> | ||||||
|         </Dialog> |         </Dialog> | ||||||
|   | |||||||
							
								
								
									
										413
									
								
								src/dialogs/import-dbml-dialog/import-dbml-dialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,413 @@ | |||||||
|  | import React, { | ||||||
|  |     useCallback, | ||||||
|  |     useEffect, | ||||||
|  |     useState, | ||||||
|  |     Suspense, | ||||||
|  |     useRef, | ||||||
|  | } from 'react'; | ||||||
|  | import * as monaco from 'monaco-editor'; | ||||||
|  | import { useDialog } from '@/hooks/use-dialog'; | ||||||
|  | import { | ||||||
|  |     Dialog, | ||||||
|  |     DialogClose, | ||||||
|  |     DialogContent, | ||||||
|  |     DialogDescription, | ||||||
|  |     DialogFooter, | ||||||
|  |     DialogHeader, | ||||||
|  |     DialogInternalContent, | ||||||
|  |     DialogTitle, | ||||||
|  | } from '@/components/dialog/dialog'; | ||||||
|  | import { Button } from '@/components/button/button'; | ||||||
|  | import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||||
|  | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { Editor } from '@/components/code-snippet/code-snippet'; | ||||||
|  | import { useTheme } from '@/hooks/use-theme'; | ||||||
|  | import { AlertCircle } from 'lucide-react'; | ||||||
|  | import { importDBMLToDiagram, sanitizeDBML } from '@/lib/dbml-import'; | ||||||
|  | import { useChartDB } from '@/hooks/use-chartdb'; | ||||||
|  | import { Parser } from '@dbml/core'; | ||||||
|  | import { useCanvas } from '@/hooks/use-canvas'; | ||||||
|  | import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language'; | ||||||
|  | import { useToast } from '@/components/toast/use-toast'; | ||||||
|  | import { Spinner } from '@/components/spinner/spinner'; | ||||||
|  | import { debounce } from '@/lib/utils'; | ||||||
|  |  | ||||||
|  | interface DBMLError { | ||||||
|  |     message: string; | ||||||
|  |     line: number; | ||||||
|  |     column: number; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function parseDBMLError(error: unknown): DBMLError | null { | ||||||
|  |     try { | ||||||
|  |         if (typeof error === 'string') { | ||||||
|  |             const parsed = JSON.parse(error); | ||||||
|  |             if (parsed.diags?.[0]) { | ||||||
|  |                 const diag = parsed.diags[0]; | ||||||
|  |                 return { | ||||||
|  |                     message: diag.message, | ||||||
|  |                     line: diag.location.start.line, | ||||||
|  |                     column: diag.location.start.column, | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  |         } else if (error && typeof error === 'object' && 'diags' in error) { | ||||||
|  |             const parsed = error as { | ||||||
|  |                 diags: Array<{ | ||||||
|  |                     message: string; | ||||||
|  |                     location: { start: { line: number; column: number } }; | ||||||
|  |                 }>; | ||||||
|  |             }; | ||||||
|  |             if (parsed.diags?.[0]) { | ||||||
|  |                 return { | ||||||
|  |                     message: parsed.diags[0].message, | ||||||
|  |                     line: parsed.diags[0].location.start.line, | ||||||
|  |                     column: parsed.diags[0].location.start.column, | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } catch (e) { | ||||||
|  |         console.error('Error parsing DBML error:', e); | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface ImportDBMLDialogProps extends BaseDialogProps { | ||||||
|  |     withCreateEmptyDiagram?: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const ImportDBMLDialog: React.FC<ImportDBMLDialogProps> = ({ | ||||||
|  |     dialog, | ||||||
|  |     withCreateEmptyDiagram, | ||||||
|  | }) => { | ||||||
|  |     const { t } = useTranslation(); | ||||||
|  |     const initialDBML = `// Use DBML to define your database structure | ||||||
|  | // Simple Blog System with Comments Example | ||||||
|  |  | ||||||
|  | Table users { | ||||||
|  |   id integer [primary key] | ||||||
|  |   name varchar | ||||||
|  |   email varchar | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Table posts { | ||||||
|  |   id integer [primary key] | ||||||
|  |   title varchar | ||||||
|  |   content text | ||||||
|  |   user_id integer | ||||||
|  |   created_at timestamp | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Table comments { | ||||||
|  |   id integer [primary key] | ||||||
|  |   content text | ||||||
|  |   post_id integer | ||||||
|  |   user_id integer | ||||||
|  |   created_at timestamp | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Relationships | ||||||
|  | Ref: posts.user_id > users.id // Each post belongs to one user | ||||||
|  | Ref: comments.post_id > posts.id // Each comment belongs to one post | ||||||
|  | Ref: comments.user_id > users.id // Each comment is written by one user`; | ||||||
|  |  | ||||||
|  |     const [dbmlContent, setDBMLContent] = useState<string>(initialDBML); | ||||||
|  |     const { closeImportDBMLDialog } = useDialog(); | ||||||
|  |     const [errorMessage, setErrorMessage] = useState<string | undefined>(); | ||||||
|  |     const { effectiveTheme } = useTheme(); | ||||||
|  |     const { toast } = useToast(); | ||||||
|  |     const { | ||||||
|  |         addTables, | ||||||
|  |         addRelationships, | ||||||
|  |         tables, | ||||||
|  |         relationships, | ||||||
|  |         removeTables, | ||||||
|  |         removeRelationships, | ||||||
|  |     } = useChartDB(); | ||||||
|  |     const { reorderTables } = useCanvas(); | ||||||
|  |     const [reorder, setReorder] = useState(false); | ||||||
|  |     const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(); | ||||||
|  |     const decorationsCollection = | ||||||
|  |         useRef<monaco.editor.IEditorDecorationsCollection>(); | ||||||
|  |  | ||||||
|  |     const handleEditorDidMount = ( | ||||||
|  |         editor: monaco.editor.IStandaloneCodeEditor | ||||||
|  |     ) => { | ||||||
|  |         editorRef.current = editor; | ||||||
|  |         decorationsCollection.current = editor.createDecorationsCollection(); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     useEffect(() => { | ||||||
|  |         if (reorder) { | ||||||
|  |             reorderTables({ | ||||||
|  |                 updateHistory: false, | ||||||
|  |             }); | ||||||
|  |             setReorder(false); | ||||||
|  |         } | ||||||
|  |     }, [reorder, reorderTables]); | ||||||
|  |  | ||||||
|  |     const highlightErrorLine = useCallback((error: DBMLError) => { | ||||||
|  |         if (!editorRef.current) return; | ||||||
|  |  | ||||||
|  |         const model = editorRef.current.getModel(); | ||||||
|  |         if (!model) return; | ||||||
|  |  | ||||||
|  |         const decorations = [ | ||||||
|  |             { | ||||||
|  |                 range: new monaco.Range( | ||||||
|  |                     error.line, | ||||||
|  |                     1, | ||||||
|  |                     error.line, | ||||||
|  |                     model.getLineMaxColumn(error.line) | ||||||
|  |                 ), | ||||||
|  |                 options: { | ||||||
|  |                     isWholeLine: true, | ||||||
|  |                     className: 'dbml-error-line', | ||||||
|  |                     glyphMarginClassName: 'dbml-error-glyph', | ||||||
|  |                     hoverMessage: { value: error.message }, | ||||||
|  |                     overviewRuler: { | ||||||
|  |                         color: '#ff0000', | ||||||
|  |                         position: monaco.editor.OverviewRulerLane.Right, | ||||||
|  |                         darkColor: '#ff0000', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |         ]; | ||||||
|  |  | ||||||
|  |         decorationsCollection.current?.set(decorations); | ||||||
|  |     }, []); | ||||||
|  |  | ||||||
|  |     const clearDecorations = useCallback(() => { | ||||||
|  |         decorationsCollection.current?.clear(); | ||||||
|  |     }, []); | ||||||
|  |  | ||||||
|  |     const validateDBML = useCallback( | ||||||
|  |         async (content: string) => { | ||||||
|  |             // Clear previous errors | ||||||
|  |             setErrorMessage(undefined); | ||||||
|  |             clearDecorations(); | ||||||
|  |  | ||||||
|  |             if (!content.trim()) return; | ||||||
|  |  | ||||||
|  |             try { | ||||||
|  |                 const sanitizedContent = sanitizeDBML(content); | ||||||
|  |                 const parser = new Parser(); | ||||||
|  |                 parser.parse(sanitizedContent, 'dbml'); | ||||||
|  |             } catch (e) { | ||||||
|  |                 const parsedError = parseDBMLError(e); | ||||||
|  |                 if (parsedError) { | ||||||
|  |                     setErrorMessage( | ||||||
|  |                         t('import_dbml_dialog.error.description') + | ||||||
|  |                             ` (1 error found - in line ${parsedError.line})` | ||||||
|  |                     ); | ||||||
|  |                     highlightErrorLine(parsedError); | ||||||
|  |                 } else { | ||||||
|  |                     setErrorMessage( | ||||||
|  |                         e instanceof Error ? e.message : JSON.stringify(e) | ||||||
|  |                     ); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         [clearDecorations, highlightErrorLine, t] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const debouncedValidateRef = useRef<((value: string) => void) | null>(null); | ||||||
|  |  | ||||||
|  |     // Set up debounced validation | ||||||
|  |     useEffect(() => { | ||||||
|  |         debouncedValidateRef.current = debounce((value: string) => { | ||||||
|  |             validateDBML(value); | ||||||
|  |         }, 500); | ||||||
|  |  | ||||||
|  |         return () => { | ||||||
|  |             debouncedValidateRef.current = null; | ||||||
|  |         }; | ||||||
|  |     }, [validateDBML]); | ||||||
|  |  | ||||||
|  |     // Trigger validation when content changes | ||||||
|  |     useEffect(() => { | ||||||
|  |         if (debouncedValidateRef.current) { | ||||||
|  |             debouncedValidateRef.current(dbmlContent); | ||||||
|  |         } | ||||||
|  |     }, [dbmlContent]); | ||||||
|  |  | ||||||
|  |     useEffect(() => { | ||||||
|  |         if (!dialog.open) { | ||||||
|  |             setErrorMessage(undefined); | ||||||
|  |             clearDecorations(); | ||||||
|  |             setDBMLContent(initialDBML); | ||||||
|  |         } | ||||||
|  |     }, [dialog.open, initialDBML, clearDecorations]); | ||||||
|  |  | ||||||
|  |     const handleImport = useCallback(async () => { | ||||||
|  |         if (!dbmlContent.trim() || errorMessage) return; | ||||||
|  |  | ||||||
|  |         try { | ||||||
|  |             // Sanitize DBML content before importing | ||||||
|  |             const sanitizedContent = sanitizeDBML(dbmlContent); | ||||||
|  |             const importedDiagram = await importDBMLToDiagram(sanitizedContent); | ||||||
|  |             const tableIdsToRemove = tables | ||||||
|  |                 .filter((table) => | ||||||
|  |                     importedDiagram.tables?.some( | ||||||
|  |                         (t) => | ||||||
|  |                             t.name === table.name && t.schema === table.schema | ||||||
|  |                     ) | ||||||
|  |                 ) | ||||||
|  |                 .map((table) => table.id); | ||||||
|  |             // Find relationships that need to be removed | ||||||
|  |             const relationshipIdsToRemove = relationships | ||||||
|  |                 .filter((relationship) => { | ||||||
|  |                     const sourceTable = tables.find( | ||||||
|  |                         (table) => table.id === relationship.sourceTableId | ||||||
|  |                     ); | ||||||
|  |                     const targetTable = tables.find( | ||||||
|  |                         (table) => table.id === relationship.targetTableId | ||||||
|  |                     ); | ||||||
|  |                     if (!sourceTable || !targetTable) return true; | ||||||
|  |                     const replacementSourceTable = importedDiagram.tables?.find( | ||||||
|  |                         (table) => | ||||||
|  |                             table.name === sourceTable.name && | ||||||
|  |                             table.schema === sourceTable.schema | ||||||
|  |                     ); | ||||||
|  |                     const replacementTargetTable = importedDiagram.tables?.find( | ||||||
|  |                         (table) => | ||||||
|  |                             table.name === targetTable.name && | ||||||
|  |                             table.schema === targetTable.schema | ||||||
|  |                     ); | ||||||
|  |                     return replacementSourceTable || replacementTargetTable; | ||||||
|  |                 }) | ||||||
|  |                 .map((relationship) => relationship.id); | ||||||
|  |  | ||||||
|  |             // Remove existing items | ||||||
|  |             await Promise.all([ | ||||||
|  |                 removeTables(tableIdsToRemove, { updateHistory: false }), | ||||||
|  |                 removeRelationships(relationshipIdsToRemove, { | ||||||
|  |                     updateHistory: false, | ||||||
|  |                 }), | ||||||
|  |             ]); | ||||||
|  |  | ||||||
|  |             // Add new items | ||||||
|  |             await Promise.all([ | ||||||
|  |                 addTables(importedDiagram.tables ?? [], { | ||||||
|  |                     updateHistory: false, | ||||||
|  |                 }), | ||||||
|  |                 addRelationships(importedDiagram.relationships ?? [], { | ||||||
|  |                     updateHistory: false, | ||||||
|  |                 }), | ||||||
|  |             ]); | ||||||
|  |             setReorder(true); | ||||||
|  |             closeImportDBMLDialog(); | ||||||
|  |         } catch (e) { | ||||||
|  |             toast({ | ||||||
|  |                 title: t('import_dbml_dialog.error.title'), | ||||||
|  |                 variant: 'destructive', | ||||||
|  |                 description: ( | ||||||
|  |                     <> | ||||||
|  |                         <div>{t('import_dbml_dialog.error.description')}</div> | ||||||
|  |                         {e instanceof Error ? e.message : JSON.stringify(e)} | ||||||
|  |                     </> | ||||||
|  |                 ), | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     }, [ | ||||||
|  |         dbmlContent, | ||||||
|  |         closeImportDBMLDialog, | ||||||
|  |         tables, | ||||||
|  |         relationships, | ||||||
|  |         removeTables, | ||||||
|  |         removeRelationships, | ||||||
|  |         addTables, | ||||||
|  |         addRelationships, | ||||||
|  |         errorMessage, | ||||||
|  |         toast, | ||||||
|  |         setReorder, | ||||||
|  |         t, | ||||||
|  |     ]); | ||||||
|  |  | ||||||
|  |     return ( | ||||||
|  |         <Dialog | ||||||
|  |             {...dialog} | ||||||
|  |             onOpenChange={(open) => { | ||||||
|  |                 if (!open) { | ||||||
|  |                     closeImportDBMLDialog(); | ||||||
|  |                 } | ||||||
|  |             }} | ||||||
|  |         > | ||||||
|  |             <DialogContent | ||||||
|  |                 className="flex h-[80vh] max-h-screen w-full flex-col md:max-w-[900px]" | ||||||
|  |                 showClose | ||||||
|  |             > | ||||||
|  |                 <DialogHeader> | ||||||
|  |                     <DialogTitle> | ||||||
|  |                         {withCreateEmptyDiagram | ||||||
|  |                             ? t('import_dbml_dialog.example_title') | ||||||
|  |                             : t('import_dbml_dialog.title')} | ||||||
|  |                     </DialogTitle> | ||||||
|  |                     <DialogDescription> | ||||||
|  |                         {t('import_dbml_dialog.description')} | ||||||
|  |                     </DialogDescription> | ||||||
|  |                 </DialogHeader> | ||||||
|  |                 <DialogInternalContent> | ||||||
|  |                     <Suspense fallback={<Spinner />}> | ||||||
|  |                         <Editor | ||||||
|  |                             value={dbmlContent} | ||||||
|  |                             onChange={(value) => setDBMLContent(value || '')} | ||||||
|  |                             language="dbml" | ||||||
|  |                             onMount={handleEditorDidMount} | ||||||
|  |                             theme={ | ||||||
|  |                                 effectiveTheme === 'dark' | ||||||
|  |                                     ? 'dbml-dark' | ||||||
|  |                                     : 'dbml-light' | ||||||
|  |                             } | ||||||
|  |                             beforeMount={setupDBMLLanguage} | ||||||
|  |                             options={{ | ||||||
|  |                                 minimap: { enabled: false }, | ||||||
|  |                                 scrollBeyondLastLine: false, | ||||||
|  |                                 automaticLayout: true, | ||||||
|  |                                 glyphMargin: true, | ||||||
|  |                                 lineNumbers: 'on', | ||||||
|  |                                 scrollbar: { | ||||||
|  |                                     vertical: 'visible', | ||||||
|  |                                     horizontal: 'visible', | ||||||
|  |                                 }, | ||||||
|  |                             }} | ||||||
|  |                             className="size-full" | ||||||
|  |                         /> | ||||||
|  |                     </Suspense> | ||||||
|  |                 </DialogInternalContent> | ||||||
|  |                 <DialogFooter> | ||||||
|  |                     <div className="flex w-full items-center justify-between"> | ||||||
|  |                         <div className="flex items-center gap-4"> | ||||||
|  |                             <DialogClose asChild> | ||||||
|  |                                 <Button variant="secondary"> | ||||||
|  |                                     {withCreateEmptyDiagram | ||||||
|  |                                         ? t('import_dbml_dialog.skip_and_empty') | ||||||
|  |                                         : t('import_dbml_dialog.cancel')} | ||||||
|  |                                 </Button> | ||||||
|  |                             </DialogClose> | ||||||
|  |                             {errorMessage ? ( | ||||||
|  |                                 <div className="flex items-center gap-1"> | ||||||
|  |                                     <AlertCircle className="size-4 text-destructive" /> | ||||||
|  |  | ||||||
|  |                                     <span className="text-xs text-destructive"> | ||||||
|  |                                         {errorMessage || | ||||||
|  |                                             t( | ||||||
|  |                                                 'import_dbml_dialog.error.description' | ||||||
|  |                                             )} | ||||||
|  |                                     </span> | ||||||
|  |                                 </div> | ||||||
|  |                             ) : null} | ||||||
|  |                         </div> | ||||||
|  |                         <Button | ||||||
|  |                             onClick={handleImport} | ||||||
|  |                             disabled={!dbmlContent.trim() || !!errorMessage} | ||||||
|  |                         > | ||||||
|  |                             {withCreateEmptyDiagram | ||||||
|  |                                 ? t('import_dbml_dialog.show_example') | ||||||
|  |                                 : t('import_dbml_dialog.import')} | ||||||
|  |                         </Button> | ||||||
|  |                     </div> | ||||||
|  |                 </DialogFooter> | ||||||
|  |             </DialogContent> | ||||||
|  |         </Dialog> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @@ -22,15 +22,19 @@ import { useConfig } from '@/hooks/use-config'; | |||||||
| import { useDialog } from '@/hooks/use-dialog'; | import { useDialog } from '@/hooks/use-dialog'; | ||||||
| import { useStorage } from '@/hooks/use-storage'; | import { useStorage } from '@/hooks/use-storage'; | ||||||
| import type { Diagram } from '@/lib/domain/diagram'; | import type { Diagram } from '@/lib/domain/diagram'; | ||||||
| import React, { useEffect, useState } from 'react'; | import React, { useCallback, useEffect, useState } from 'react'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { useNavigate } from 'react-router-dom'; | import { useNavigate } from 'react-router-dom'; | ||||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||||
|  | import { useDebounce } from '@/hooks/use-debounce'; | ||||||
|  |  | ||||||
| export interface OpenDiagramDialogProps extends BaseDialogProps {} | export interface OpenDiagramDialogProps extends BaseDialogProps { | ||||||
|  |     canClose?: boolean; | ||||||
|  | } | ||||||
|  |  | ||||||
| export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | ||||||
|     dialog, |     dialog, | ||||||
|  |     canClose = true, | ||||||
| }) => { | }) => { | ||||||
|     const { closeOpenDiagramDialog } = useDialog(); |     const { closeOpenDiagramDialog } = useDialog(); | ||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
| @@ -58,24 +62,77 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | |||||||
|         fetchDiagrams(); |         fetchDiagrams(); | ||||||
|     }, [listDiagrams, setDiagrams, dialog.open]); |     }, [listDiagrams, setDiagrams, dialog.open]); | ||||||
|  |  | ||||||
|     const openDiagram = (diagramId: string) => { |     const openDiagram = useCallback( | ||||||
|         if (diagramId) { |         (diagramId: string) => { | ||||||
|             updateConfig({ defaultDiagramId: diagramId }); |             if (diagramId) { | ||||||
|             navigate(`/diagrams/${diagramId}`); |                 updateConfig({ config: { defaultDiagramId: diagramId } }); | ||||||
|         } |                 navigate(`/diagrams/${diagramId}`); | ||||||
|     }; |             } | ||||||
|  |         }, | ||||||
|  |         [updateConfig, navigate] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const handleRowKeyDown = useCallback( | ||||||
|  |         (e: React.KeyboardEvent<HTMLTableRowElement>) => { | ||||||
|  |             const element = e.target as HTMLElement; | ||||||
|  |             const diagramId = element.getAttribute('data-diagram-id'); | ||||||
|  |             const selectionIndexAttr = element.getAttribute( | ||||||
|  |                 'data-selection-index' | ||||||
|  |             ); | ||||||
|  |  | ||||||
|  |             if (!diagramId || !selectionIndexAttr) return; | ||||||
|  |  | ||||||
|  |             const selectionIndex = parseInt(selectionIndexAttr, 10); | ||||||
|  |  | ||||||
|  |             switch (e.key) { | ||||||
|  |                 case 'Enter': | ||||||
|  |                 case ' ': | ||||||
|  |                     e.preventDefault(); | ||||||
|  |                     openDiagram(diagramId); | ||||||
|  |                     closeOpenDiagramDialog(); | ||||||
|  |                     break; | ||||||
|  |                 case 'ArrowDown': { | ||||||
|  |                     e.preventDefault(); | ||||||
|  |  | ||||||
|  |                     ( | ||||||
|  |                         document.querySelector( | ||||||
|  |                             `[data-selection-index="${selectionIndex + 1}"]` | ||||||
|  |                         ) as HTMLElement | ||||||
|  |                     )?.focus(); | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |                 case 'ArrowUp': { | ||||||
|  |                     e.preventDefault(); | ||||||
|  |  | ||||||
|  |                     ( | ||||||
|  |                         document.querySelector( | ||||||
|  |                             `[data-selection-index="${selectionIndex - 1}"]` | ||||||
|  |                         ) as HTMLElement | ||||||
|  |                     )?.focus(); | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         [openDiagram, closeOpenDiagramDialog] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const onFocusHandler = useDebounce( | ||||||
|  |         (diagramId: string) => setSelectedDiagramId(diagramId), | ||||||
|  |         50 | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <Dialog |         <Dialog | ||||||
|             {...dialog} |             {...dialog} | ||||||
|             onOpenChange={(open) => { |             onOpenChange={(open) => { | ||||||
|                 if (!open) { |                 if (!open && canClose) { | ||||||
|                     closeOpenDiagramDialog(); |                     closeOpenDiagramDialog(); | ||||||
|                 } |                 } | ||||||
|             }} |             }} | ||||||
|         > |         > | ||||||
|             <DialogContent |             <DialogContent | ||||||
|                 className="flex h-[30rem] max-h-screen flex-col overflow-y-auto md:min-w-[80vw] xl:min-w-[55vw]" |                 className="flex h-[30rem] max-h-screen flex-col overflow-y-auto md:min-w-[80vw] xl:min-w-[55vw]" | ||||||
|                 showClose |                 showClose={canClose} | ||||||
|             > |             > | ||||||
|                 <DialogHeader> |                 <DialogHeader> | ||||||
|                     <DialogTitle>{t('open_diagram_dialog.title')}</DialogTitle> |                     <DialogTitle>{t('open_diagram_dialog.title')}</DialogTitle> | ||||||
| @@ -112,10 +169,17 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | |||||||
|                                 </TableRow> |                                 </TableRow> | ||||||
|                             </TableHeader> |                             </TableHeader> | ||||||
|                             <TableBody> |                             <TableBody> | ||||||
|                                 {diagrams.map((diagram) => ( |                                 {diagrams.map((diagram, index) => ( | ||||||
|                                     <TableRow |                                     <TableRow | ||||||
|                                         key={diagram.id} |                                         key={diagram.id} | ||||||
|                                         data-state={`${selectedDiagramId === diagram.id ? 'selected' : ''}`} |                                         data-state={`${selectedDiagramId === diagram.id ? 'selected' : ''}`} | ||||||
|  |                                         data-diagram-id={diagram.id} | ||||||
|  |                                         data-selection-index={index} | ||||||
|  |                                         tabIndex={0} | ||||||
|  |                                         onFocus={() => | ||||||
|  |                                             onFocusHandler(diagram.id) | ||||||
|  |                                         } | ||||||
|  |                                         className="focus:bg-accent focus:outline-none" | ||||||
|                                         onClick={(e) => { |                                         onClick={(e) => { | ||||||
|                                             switch (e.detail) { |                                             switch (e.detail) { | ||||||
|                                                 case 1: |                                                 case 1: | ||||||
| @@ -133,6 +197,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | |||||||
|                                                     ); |                                                     ); | ||||||
|                                             } |                                             } | ||||||
|                                         }} |                                         }} | ||||||
|  |                                         onKeyDown={handleRowKeyDown} | ||||||
|                                     > |                                     > | ||||||
|                                         <TableCell className="table-cell"> |                                         <TableCell className="table-cell"> | ||||||
|                                             <div className="flex justify-center"> |                                             <div className="flex justify-center"> | ||||||
| @@ -164,11 +229,15 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | |||||||
|                 </DialogInternalContent> |                 </DialogInternalContent> | ||||||
|  |  | ||||||
|                 <DialogFooter className="flex !justify-between gap-2"> |                 <DialogFooter className="flex !justify-between gap-2"> | ||||||
|                     <DialogClose asChild> |                     {canClose ? ( | ||||||
|                         <Button type="button" variant="secondary"> |                         <DialogClose asChild> | ||||||
|                             {t('open_diagram_dialog.cancel')} |                             <Button type="button" variant="secondary"> | ||||||
|                         </Button> |                                 {t('open_diagram_dialog.cancel')} | ||||||
|                     </DialogClose> |                             </Button> | ||||||
|  |                         </DialogClose> | ||||||
|  |                     ) : ( | ||||||
|  |                         <div /> | ||||||
|  |                     )} | ||||||
|                     <DialogClose asChild> |                     <DialogClose asChild> | ||||||
|                         <Button |                         <Button | ||||||
|                             type="submit" |                             type="submit" | ||||||
|   | |||||||
| @@ -30,6 +30,14 @@ | |||||||
|         --chart-4: 43 74% 66%; |         --chart-4: 43 74% 66%; | ||||||
|         --chart-5: 27 87% 67%; |         --chart-5: 27 87% 67%; | ||||||
|         --subtitle: 215.3 19.3% 34.5%; |         --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 { |     .dark { | ||||||
| @@ -58,6 +66,14 @@ | |||||||
|         --chart-4: 280 65% 60%; |         --chart-4: 280 65% 60%; | ||||||
|         --chart-5: 340 75% 55%; |         --chart-5: 340 75% 55%; | ||||||
|         --subtitle: 212.7 26.8% 83.9%; |         --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%; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -109,6 +125,10 @@ | |||||||
|     animation: rainbow-text-simple-animation 0.5s ease-in forwards; |     animation: rainbow-text-simple-animation 0.5s ease-in forwards; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .dbml-error-line { | ||||||
|  |     background-color: rgba(255, 0, 0, 0.2) !important; | ||||||
|  | } | ||||||
|  |  | ||||||
| @keyframes rainbow-text-simple-animation-rev { | @keyframes rainbow-text-simple-animation-rev { | ||||||
|     0% { |     0% { | ||||||
|         background-size: 650%; |         background-size: 650%; | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								src/hooks/use-canvas.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | |||||||
|  | import { useContext } from 'react'; | ||||||
|  | import { canvasContext } from '@/context/canvas-context/canvas-context'; | ||||||
|  |  | ||||||
|  | export const useCanvas = () => useContext(canvasContext); | ||||||
							
								
								
									
										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; | ||||||
|  | } | ||||||
							
								
								
									
										21
									
								
								src/hooks/use-debounce.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | |||||||
|  | import { useCallback, useRef } from 'react'; | ||||||
|  |  | ||||||
|  | // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||
|  | type AnyFunction = (...args: any[]) => any; | ||||||
|  |  | ||||||
|  | export const useDebounce = <T extends AnyFunction>( | ||||||
|  |     func: T, | ||||||
|  |     delay: number | ||||||
|  | ): ((...args: Parameters<T>) => void) => { | ||||||
|  |     const inDebounce = useRef<NodeJS.Timeout>(); | ||||||
|  |  | ||||||
|  |     const debounce = useCallback( | ||||||
|  |         (...args: Parameters<T>) => { | ||||||
|  |             clearTimeout(inDebounce.current); | ||||||
|  |             inDebounce.current = setTimeout(() => func(...args), delay); | ||||||
|  |         }, | ||||||
|  |         [func, delay] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return debounce; | ||||||
|  | }; | ||||||
							
								
								
									
										40
									
								
								src/hooks/use-export-diagram.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,40 @@ | |||||||
|  | import { useCallback, useState } from 'react'; | ||||||
|  | import { useDialog } from '@/hooks/use-dialog'; | ||||||
|  | import { diagramToJSONOutput } from '@/lib/export-import-utils'; | ||||||
|  | import { waitFor } from '@/lib/utils'; | ||||||
|  | import type { Diagram } from '@/lib/domain/diagram'; | ||||||
|  |  | ||||||
|  | export const useExportDiagram = () => { | ||||||
|  |     const [isLoading, setIsLoading] = useState(false); | ||||||
|  |     const { closeExportDiagramDialog } = useDialog(); | ||||||
|  |  | ||||||
|  |     const downloadOutput = useCallback((name: string, dataUrl: string) => { | ||||||
|  |         const a = document.createElement('a'); | ||||||
|  |         a.setAttribute('download', `ChartDB(${name}).json`); | ||||||
|  |         a.setAttribute('href', dataUrl); | ||||||
|  |         a.click(); | ||||||
|  |     }, []); | ||||||
|  |  | ||||||
|  |     const handleExport = useCallback( | ||||||
|  |         async ({ diagram }: { diagram: Diagram }) => { | ||||||
|  |             setIsLoading(true); | ||||||
|  |             await waitFor(1000); | ||||||
|  |             try { | ||||||
|  |                 const json = diagramToJSONOutput(diagram); | ||||||
|  |                 const blob = new Blob([json], { type: 'application/json' }); | ||||||
|  |                 const dataUrl = URL.createObjectURL(blob); | ||||||
|  |                 downloadOutput(diagram.name, dataUrl); | ||||||
|  |                 setIsLoading(false); | ||||||
|  |                 closeExportDiagramDialog(); | ||||||
|  |             } finally { | ||||||
|  |                 setIsLoading(false); | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         [downloadOutput, closeExportDiagramDialog] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |         exportDiagram: handleExport, | ||||||
|  |         isExporting: isLoading, | ||||||
|  |     }; | ||||||
|  | }; | ||||||
							
								
								
									
										23
									
								
								src/hooks/use-mobile.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -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; | ||||||
|  | } | ||||||
| @@ -8,7 +8,7 @@ export const ar: LanguageTranslation = { | |||||||
|                 new: 'جديد', |                 new: 'جديد', | ||||||
|                 open: 'فتح', |                 open: 'فتح', | ||||||
|                 save: 'حفظ', |                 save: 'حفظ', | ||||||
|                 import_database: 'استيراد قاعدة بيانات', |                 import: 'استيراد قاعدة بيانات', | ||||||
|                 export_sql: 'SQL تصدير', |                 export_sql: 'SQL تصدير', | ||||||
|                 export_as: 'تصدير كـ', |                 export_as: 'تصدير كـ', | ||||||
|                 delete_diagram: 'حذف الرسم البياني', |                 delete_diagram: 'حذف الرسم البياني', | ||||||
| @@ -34,16 +34,15 @@ export const ar: LanguageTranslation = { | |||||||
|                 show_minimap: 'Show Mini Map', |                 show_minimap: 'Show Mini Map', | ||||||
|                 hide_minimap: 'Hide Mini Map', |                 hide_minimap: 'Hide Mini Map', | ||||||
|             }, |             }, | ||||||
|             share: { |             backup: { | ||||||
|                 share: 'مشاركة', |                 backup: 'النسخ الاحتياطي', | ||||||
|                 export_diagram: 'تصدير المخطط', |                 export_diagram: 'تصدير المخطط', | ||||||
|                 import_diagram: 'استيراد المخطط', |                 restore_diagram: 'استعادة المخطط', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'مساعدة', |                 help: 'مساعدة', | ||||||
|                 visit_website: 'ChartDB قم بزيارة', |                 docs_website: 'الوثائق', | ||||||
|                 join_discord: 'Discord انضم إلينا على', |                 join_discord: 'انضم إلينا على Discord', | ||||||
|                 schedule_a_call: '!تحدث معنا', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -124,6 +123,12 @@ export const ar: LanguageTranslation = { | |||||||
|                 add_table: 'إضافة جدول', |                 add_table: 'إضافة جدول', | ||||||
|                 filter: 'تصفية', |                 filter: 'تصفية', | ||||||
|                 collapse: 'طي الكل', |                 collapse: 'طي الكل', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No tables found matching your filter.', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 show_list: 'Show Table List', | ||||||
|  |                 show_dbml: 'Show DBML Editor', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'الحقول', |                     fields: 'الحقول', | ||||||
| @@ -144,6 +149,8 @@ export const ar: LanguageTranslation = { | |||||||
|                         comments: 'تعليقات', |                         comments: 'تعليقات', | ||||||
|                         no_comments: 'لا يوجد تعليقات', |                         no_comments: 'لا يوجد تعليقات', | ||||||
|                         delete_field: 'حذف الحقل', |                         delete_field: 'حذف الحقل', | ||||||
|  |                         // TODO: Translate | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: 'خصائص الفهرس', |                         title: 'خصائص الفهرس', | ||||||
| @@ -203,6 +210,54 @@ export const ar: LanguageTranslation = { | |||||||
|                     description: 'إنشاء اعتماد للبدء', |                     description: 'إنشاء اعتماد للبدء', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -229,7 +284,7 @@ export const ar: LanguageTranslation = { | |||||||
|                 title: 'إسترد قاعدة بياناتك', |                 title: 'إسترد قاعدة بياناتك', | ||||||
|                 database_edition: ':إصدار قاعدة البيانات', |                 database_edition: ':إصدار قاعدة البيانات', | ||||||
|                 step_1: ':قم بتشغيل هذا البرنامج النصي في قاعدة بياناتك', |                 step_1: ':قم بتشغيل هذا البرنامج النصي في قاعدة بياناتك', | ||||||
|                 step_2: ':إلصق نتيجة البرنامج النصي هنا', |                 step_2: ':إلصق نتيجة البرنامج النصي هنا →', | ||||||
|                 script_results_placeholder: '...نتيجة البرنامج النصي هنا', |                 script_results_placeholder: '...نتيجة البرنامج النصي هنا', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'SSMS تعليمات', |                     button_text: 'SSMS تعليمات', | ||||||
| @@ -323,6 +378,12 @@ export const ar: LanguageTranslation = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'إلغاء', |             cancel: 'إلغاء', | ||||||
|             export: 'تصدير', |             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: { |         new_table_schema_dialog: { | ||||||
| @@ -355,10 +416,9 @@ export const ar: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'حدث خطأ أثناء التصدير', |                 title: 'حدث خطأ أثناء التصدير', | ||||||
|                 description: |                 description: | ||||||
|                     'chartdb.io@gmail.com حدث خطأ ما. هل تحتاج إلى مساعدة؟', |                     'support@chartdb.io حدث خطأ ما. هل تحتاج إلى مساعدة؟', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         import_diagram_dialog: { |         import_diagram_dialog: { | ||||||
|             title: 'استيراد الرسم البياني', |             title: 'استيراد الرسم البياني', | ||||||
|             description: ':للرسم البياني ادناه JSON قم بلصق', |             description: ':للرسم البياني ادناه JSON قم بلصق', | ||||||
| @@ -367,7 +427,21 @@ export const ar: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'حدث خطأ أثناء الاستيراد', |                 title: 'حدث خطأ أثناء الاستيراد', | ||||||
|                 description: |                 description: | ||||||
|                     'chartdb.io@gmail.com و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني', |                     'support@chartdb.io و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني', | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |         import_dbml_dialog: { | ||||||
|  |             // TODO: Translate | ||||||
|  |             title: 'Import DBML', | ||||||
|  |             example_title: 'Import Example DBML', | ||||||
|  |             description: 'Import a database schema from DBML format.', | ||||||
|  |             import: 'Import', | ||||||
|  |             cancel: 'Cancel', | ||||||
|  |             skip_and_empty: 'Skip & Empty', | ||||||
|  |             show_example: 'Show Example', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Error', | ||||||
|  |                 description: 'Failed to parse DBML. Please check the syntax.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         relationship_type: { |         relationship_type: { | ||||||
| @@ -380,12 +454,15 @@ export const ar: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: 'جدول جديد', |             new_table: 'جدول جديد', | ||||||
|             new_relationship: 'علاقة جديدة', |             new_relationship: 'علاقة جديدة', | ||||||
|  |             // TODO: Translate | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'تعديل الجدول', |             edit_table: 'تعديل الجدول', | ||||||
|             duplicate_table: 'نسخ الجدول', |             duplicate_table: 'نسخ الجدول', | ||||||
|             delete_table: 'حذف الجدول', |             delete_table: 'حذف الجدول', | ||||||
|  |             add_relationship: 'Add Relationship', // TODO: Translate | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         snap_to_grid_tooltip: '({{key}} مغنظة الشبكة (اضغط مع الاستمرار على', |         snap_to_grid_tooltip: '({{key}} مغنظة الشبكة (اضغط مع الاستمرار على', | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const bn: LanguageTranslation = { | |||||||
|                 new: 'নতুন', |                 new: 'নতুন', | ||||||
|                 open: 'খুলুন', |                 open: 'খুলুন', | ||||||
|                 save: 'সংরক্ষণ করুন', |                 save: 'সংরক্ষণ করুন', | ||||||
|                 import_database: 'ডাটাবেস আমদানি করুন', |                 import: 'ডাটাবেস আমদানি করুন', | ||||||
|                 export_sql: 'SQL রপ্তানি করুন', |                 export_sql: 'SQL রপ্তানি করুন', | ||||||
|                 export_as: 'রূপে রপ্তানি করুন', |                 export_as: 'রূপে রপ্তানি করুন', | ||||||
|                 delete_diagram: 'ডায়াগ্রাম মুছুন', |                 delete_diagram: 'ডায়াগ্রাম মুছুন', | ||||||
| @@ -35,16 +35,15 @@ export const bn: LanguageTranslation = { | |||||||
|                 hide_minimap: 'Hide Mini Map', |                 hide_minimap: 'Hide Mini Map', | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|             share: { |             backup: { | ||||||
|                 share: 'শেয়ার করুন', |                 backup: 'ব্যাকআপ', | ||||||
|                 export_diagram: 'ডায়াগ্রাম রপ্তানি করুন', |                 export_diagram: 'ডায়াগ্রাম রপ্তানি করুন', | ||||||
|                 import_diagram: 'ডায়াগ্রাম আমদানি করুন', |                 restore_diagram: 'ডায়াগ্রাম পুনরুদ্ধার করুন', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'সাহায্য', |                 help: 'সাহায্য', | ||||||
|                 visit_website: 'ChartDB ওয়েবসাইটে যান', |                 docs_website: 'ডকুমেন্টেশন', | ||||||
|                 join_discord: 'আমাদের Discord-এ যোগ দিন', |                 join_discord: 'আমাদের Discord-এ যোগ দিন', | ||||||
|                 schedule_a_call: 'আমাদের সাথে কথা বলুন!', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -125,6 +124,12 @@ export const bn: LanguageTranslation = { | |||||||
|                 add_table: 'টেবিল যোগ করুন', |                 add_table: 'টেবিল যোগ করুন', | ||||||
|                 filter: 'ফিল্টার', |                 filter: 'ফিল্টার', | ||||||
|                 collapse: 'সব ভাঁজ করুন', |                 collapse: 'সব ভাঁজ করুন', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No tables found matching your filter.', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 show_list: 'Show Table List', | ||||||
|  |                 show_dbml: 'Show DBML Editor', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'ফিল্ড', |                     fields: 'ফিল্ড', | ||||||
| @@ -145,6 +150,8 @@ export const bn: LanguageTranslation = { | |||||||
|                         comments: 'মন্তব্য', |                         comments: 'মন্তব্য', | ||||||
|                         no_comments: 'কোনো মন্তব্য নেই', |                         no_comments: 'কোনো মন্তব্য নেই', | ||||||
|                         delete_field: 'ফিল্ড মুছুন', |                         delete_field: 'ফিল্ড মুছুন', | ||||||
|  |                         // TODO: Translate | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: 'ইনডেক্স কর্ম', |                         title: 'ইনডেক্স কর্ম', | ||||||
| @@ -204,6 +211,53 @@ export const bn: LanguageTranslation = { | |||||||
|                     description: 'এই অংশে কোনো নির্ভরতা উপলব্ধ নেই।', |                     description: 'এই অংশে কোনো নির্ভরতা উপলব্ধ নেই।', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -230,7 +284,7 @@ export const bn: LanguageTranslation = { | |||||||
|                 title: 'আপনার ডাটাবেস আমদানি করুন', |                 title: 'আপনার ডাটাবেস আমদানি করুন', | ||||||
|                 database_edition: 'ডাটাবেস সংস্করণ:', |                 database_edition: 'ডাটাবেস সংস্করণ:', | ||||||
|                 step_1: 'আপনার ডাটাবেসে এই স্ক্রিপ্ট চালান:', |                 step_1: 'আপনার ডাটাবেসে এই স্ক্রিপ্ট চালান:', | ||||||
|                 step_2: 'স্ক্রিপ্টের ফলাফল এখানে পেস্ট করুন:', |                 step_2: 'স্ক্রিপ্টের ফলাফল এখানে পেস্ট করুন →', | ||||||
|                 script_results_placeholder: 'স্ক্রিপ্টের ফলাফল এখানে...', |                 script_results_placeholder: 'স্ক্রিপ্টের ফলাফল এখানে...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'SSMS নির্দেশনা', |                     button_text: 'SSMS নির্দেশনা', | ||||||
| @@ -324,6 +378,12 @@ export const bn: LanguageTranslation = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'বাতিল করুন', |             cancel: 'বাতিল করুন', | ||||||
|             export: 'রপ্তানি করুন', |             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: { |         new_table_schema_dialog: { | ||||||
| @@ -358,7 +418,7 @@ export const bn: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'চিত্র রপ্তানিতে ত্রুটি', |                 title: 'চিত্র রপ্তানিতে ত্রুটি', | ||||||
|                 description: |                 description: | ||||||
|                     'কিছু ভুল হয়েছে। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।', |                     'কিছু ভুল হয়েছে। সাহায্যের প্রয়োজন? support@chartdb.io-এ যোগাযোগ করুন।', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -370,7 +430,21 @@ export const bn: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'চিত্র আমদানিতে ত্রুটি', |                 title: 'চিত্র আমদানিতে ত্রুটি', | ||||||
|                 description: |                 description: | ||||||
|                     'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।', |                     'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে 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', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Error', | ||||||
|  |                 description: 'Failed to parse DBML. Please check the syntax.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         relationship_type: { |         relationship_type: { | ||||||
| @@ -383,12 +457,15 @@ export const bn: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: 'নতুন টেবিল', |             new_table: 'নতুন টেবিল', | ||||||
|             new_relationship: 'নতুন সম্পর্ক', |             new_relationship: 'নতুন সম্পর্ক', | ||||||
|  |             // TODO: Translate | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'টেবিল সম্পাদনা করুন', |             edit_table: 'টেবিল সম্পাদনা করুন', | ||||||
|             duplicate_table: 'টেবিল নকল করুন', |             duplicate_table: 'টেবিল নকল করুন', | ||||||
|             delete_table: 'টেবিল মুছে ফেলুন', |             delete_table: 'টেবিল মুছে ফেলুন', | ||||||
|  |             add_relationship: 'Add Relationship', // TODO: Translate | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         snap_to_grid_tooltip: 'গ্রিডে স্ন্যাপ করুন (অবস্থান {{key}})', |         snap_to_grid_tooltip: 'গ্রিডে স্ন্যাপ করুন (অবস্থান {{key}})', | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const de: LanguageTranslation = { | |||||||
|                 new: 'Neu', |                 new: 'Neu', | ||||||
|                 open: 'Öffnen', |                 open: 'Öffnen', | ||||||
|                 save: 'Speichern', |                 save: 'Speichern', | ||||||
|                 import_database: 'Datenbank importieren', |                 import: 'Datenbank importieren', | ||||||
|                 export_sql: 'SQL exportieren', |                 export_sql: 'SQL exportieren', | ||||||
|                 export_as: 'Exportieren als', |                 export_as: 'Exportieren als', | ||||||
|                 delete_diagram: 'Diagramm löschen', |                 delete_diagram: 'Diagramm löschen', | ||||||
| @@ -35,16 +35,15 @@ export const de: LanguageTranslation = { | |||||||
|                 hide_minimap: 'Hide Mini Map', |                 hide_minimap: 'Hide Mini Map', | ||||||
|             }, |             }, | ||||||
|             // TODO: Translate |             // TODO: Translate | ||||||
|             share: { |             backup: { | ||||||
|                 share: 'Share', |                 backup: 'Backup', | ||||||
|                 export_diagram: 'Export Diagram', |                 export_diagram: 'Export Diagram', | ||||||
|                 import_diagram: 'Import Diagram', |                 restore_diagram: 'Restore Diagram', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'Hilfe', |                 help: 'Hilfe', | ||||||
|                 visit_website: 'ChartDB Webseite', |                 docs_website: 'Dokumentation', | ||||||
|                 join_discord: 'Auf Discord beitreten', |                 join_discord: 'Auf Discord beitreten', | ||||||
|                 schedule_a_call: 'Gespräch vereinbaren', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -126,6 +125,12 @@ export const de: LanguageTranslation = { | |||||||
|                 add_table: 'Tabelle hinzufügen', |                 add_table: 'Tabelle hinzufügen', | ||||||
|                 filter: 'Filter', |                 filter: 'Filter', | ||||||
|                 collapse: 'Alle einklappen', |                 collapse: 'Alle einklappen', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No tables found matching your filter.', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 show_list: 'Show Table List', | ||||||
|  |                 show_dbml: 'Show DBML Editor', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'Felder', |                     fields: 'Felder', | ||||||
| @@ -146,6 +151,8 @@ export const de: LanguageTranslation = { | |||||||
|                         comments: 'Kommentare', |                         comments: 'Kommentare', | ||||||
|                         no_comments: 'Keine Kommentare', |                         no_comments: 'Keine Kommentare', | ||||||
|                         delete_field: 'Feld löschen', |                         delete_field: 'Feld löschen', | ||||||
|  |                         // TODO: Translate | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: 'Indexattribute', |                         title: 'Indexattribute', | ||||||
| @@ -206,6 +213,53 @@ export const de: LanguageTranslation = { | |||||||
|                     description: 'Erstellen Sie eine Ansicht, um zu beginnen', |                     description: 'Erstellen Sie eine Ansicht, um zu beginnen', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -232,7 +286,7 @@ export const de: LanguageTranslation = { | |||||||
|                 title: 'Datenbank importieren', |                 title: 'Datenbank importieren', | ||||||
|                 database_edition: 'Datenbank Edition:', |                 database_edition: 'Datenbank Edition:', | ||||||
|                 step_1: 'Führen Sie dieses Skript in Ihrer Datenbank aus:', |                 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...', |                 script_results_placeholder: 'Skriptergebnisse hier...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'SSMS Anweisungen', |                     button_text: 'SSMS Anweisungen', | ||||||
| @@ -327,6 +381,12 @@ export const de: LanguageTranslation = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'Abbrechen', |             cancel: 'Abbrechen', | ||||||
|             export: 'Exportieren', |             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: { |         new_table_schema_dialog: { | ||||||
| @@ -361,7 +421,7 @@ export const de: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error exporting diagram', |                 title: 'Error exporting diagram', | ||||||
|                 description: |                 description: | ||||||
|                     'Something went wrong. Need help? chartdb.io@gmail.com', |                     'Something went wrong. Need help? support@chartdb.io', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         // TODO: Translate |         // TODO: Translate | ||||||
| @@ -373,7 +433,21 @@ export const de: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error importing diagram', |                 title: 'Error importing diagram', | ||||||
|                 description: |                 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 | ||||||
|  |         import_dbml_dialog: { | ||||||
|  |             example_title: 'Import Example DBML', | ||||||
|  |             title: 'Import DBML', | ||||||
|  |             description: 'Import a database schema from DBML format.', | ||||||
|  |             import: 'Import', | ||||||
|  |             cancel: 'Cancel', | ||||||
|  |             skip_and_empty: 'Skip & Empty', | ||||||
|  |             show_example: 'Show Example', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Error', | ||||||
|  |                 description: 'Failed to parse DBML. Please check the syntax.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         relationship_type: { |         relationship_type: { | ||||||
| @@ -386,12 +460,15 @@ export const de: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: 'Neue Tabelle', |             new_table: 'Neue Tabelle', | ||||||
|             new_relationship: 'Neue Beziehung', |             new_relationship: 'Neue Beziehung', | ||||||
|  |             // TODO: Translate | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'Tabelle bearbeiten', |             edit_table: 'Tabelle bearbeiten', | ||||||
|             duplicate_table: 'Duplicate Table', // TODO: Translate |             duplicate_table: 'Duplicate Table', // TODO: Translate | ||||||
|             delete_table: 'Tabelle löschen', |             delete_table: 'Tabelle löschen', | ||||||
|  |             add_relationship: 'Add Relationship', // TODO: Translate | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         // TODO: Add translations |         // TODO: Add translations | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const en = { | |||||||
|                 new: 'New', |                 new: 'New', | ||||||
|                 open: 'Open', |                 open: 'Open', | ||||||
|                 save: 'Save', |                 save: 'Save', | ||||||
|                 import_database: 'Import Database', |                 import: 'Import', | ||||||
|                 export_sql: 'Export SQL', |                 export_sql: 'Export SQL', | ||||||
|                 export_as: 'Export as', |                 export_as: 'Export as', | ||||||
|                 delete_diagram: 'Delete Diagram', |                 delete_diagram: 'Delete Diagram', | ||||||
| @@ -33,16 +33,15 @@ export const en = { | |||||||
|                 show_minimap: 'Show Mini Map', |                 show_minimap: 'Show Mini Map', | ||||||
|                 hide_minimap: 'Hide Mini Map', |                 hide_minimap: 'Hide Mini Map', | ||||||
|             }, |             }, | ||||||
|             share: { |             backup: { | ||||||
|                 share: 'Share', |                 backup: 'Backup', | ||||||
|                 export_diagram: 'Export Diagram', |                 export_diagram: 'Export Diagram', | ||||||
|                 import_diagram: 'Import Diagram', |                 restore_diagram: 'Restore Diagram', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'Help', |                 help: 'Help', | ||||||
|                 visit_website: 'Visit ChartDB', |                 docs_website: 'Docs', | ||||||
|                 join_discord: 'Join us on Discord', |                 join_discord: 'Join us on Discord', | ||||||
|                 schedule_a_call: 'Talk with us!', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -123,6 +122,10 @@ export const en = { | |||||||
|                 add_table: 'Add Table', |                 add_table: 'Add Table', | ||||||
|                 filter: 'Filter', |                 filter: 'Filter', | ||||||
|                 collapse: 'Collapse All', |                 collapse: 'Collapse All', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No tables found matching your filter.', | ||||||
|  |                 show_list: 'Show Table List', | ||||||
|  |                 show_dbml: 'Show DBML Editor', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'Fields', |                     fields: 'Fields', | ||||||
| @@ -140,6 +143,7 @@ export const en = { | |||||||
|                     field_actions: { |                     field_actions: { | ||||||
|                         title: 'Field Attributes', |                         title: 'Field Attributes', | ||||||
|                         unique: 'Unique', |                         unique: 'Unique', | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                         comments: 'Comments', |                         comments: 'Comments', | ||||||
|                         no_comments: 'No comments', |                         no_comments: 'No comments', | ||||||
|                         delete_field: 'Delete Field', |                         delete_field: 'Delete Field', | ||||||
| @@ -202,6 +206,52 @@ export const en = { | |||||||
|                     description: 'Create a view to get started', |                     description: 'Create a view to get started', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |  | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -228,7 +278,7 @@ export const en = { | |||||||
|                 title: 'Import your Database', |                 title: 'Import your Database', | ||||||
|                 database_edition: 'Database Edition:', |                 database_edition: 'Database Edition:', | ||||||
|                 step_1: 'Run this script in your database:', |                 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...', |                 script_results_placeholder: 'Script results here...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'SSMS Instructions', |                     button_text: 'SSMS Instructions', | ||||||
| @@ -322,6 +372,11 @@ export const en = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'Cancel', |             cancel: 'Cancel', | ||||||
|             export: 'Export', |             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: { |         new_table_schema_dialog: { | ||||||
| @@ -355,19 +410,33 @@ export const en = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error exporting diagram', |                 title: 'Error exporting diagram', | ||||||
|                 description: |                 description: | ||||||
|                     'Something went wrong. Need help? chartdb.io@gmail.com', |                     'Something went wrong. Need help? support@chartdb.io', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         import_diagram_dialog: { |         import_diagram_dialog: { | ||||||
|             title: 'Import Diagram', |             title: 'Import Diagram', | ||||||
|             description: 'Paste the diagram JSON below:', |             description: 'Import a diagram from a JSON file.', | ||||||
|             cancel: 'Cancel', |             cancel: 'Cancel', | ||||||
|             import: 'Import', |             import: 'Import', | ||||||
|             error: { |             error: { | ||||||
|                 title: 'Error importing diagram', |                 title: 'Error importing diagram', | ||||||
|                 description: |                 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', | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |  | ||||||
|  |         import_dbml_dialog: { | ||||||
|  |             example_title: 'Import Example DBML', | ||||||
|  |             title: 'Import DBML', | ||||||
|  |             description: 'Import a database schema from DBML format.', | ||||||
|  |             import: 'Import', | ||||||
|  |             cancel: 'Cancel', | ||||||
|  |             skip_and_empty: 'Skip & Empty', | ||||||
|  |             show_example: 'Show Example', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Error importing DBML', | ||||||
|  |                 description: 'Failed to parse DBML. Please check the syntax.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         relationship_type: { |         relationship_type: { | ||||||
| @@ -380,12 +449,14 @@ export const en = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: 'New Table', |             new_table: 'New Table', | ||||||
|             new_relationship: 'New Relationship', |             new_relationship: 'New Relationship', | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'Edit Table', |             edit_table: 'Edit Table', | ||||||
|             duplicate_table: 'Duplicate Table', |             duplicate_table: 'Duplicate Table', | ||||||
|             delete_table: 'Delete Table', |             delete_table: 'Delete Table', | ||||||
|  |             add_relationship: 'Add Relationship', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})', |         snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})', | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const es: LanguageTranslation = { | |||||||
|                 new: 'Nuevo', |                 new: 'Nuevo', | ||||||
|                 open: 'Abrir', |                 open: 'Abrir', | ||||||
|                 save: 'Guardar', |                 save: 'Guardar', | ||||||
|                 import_database: 'Importar Base de Datos', |                 import: 'Importar Base de Datos', | ||||||
|                 export_sql: 'Exportar SQL', |                 export_sql: 'Exportar SQL', | ||||||
|                 export_as: 'Exportar como', |                 export_as: 'Exportar como', | ||||||
|                 delete_diagram: 'Eliminar Diagrama', |                 delete_diagram: 'Eliminar Diagrama', | ||||||
| @@ -34,17 +34,15 @@ export const es: LanguageTranslation = { | |||||||
|                 show_minimap: 'Show Mini Map', |                 show_minimap: 'Show Mini Map', | ||||||
|                 hide_minimap: 'Hide Mini Map', |                 hide_minimap: 'Hide Mini Map', | ||||||
|             }, |             }, | ||||||
|             // TODO: Translate |             backup: { | ||||||
|             share: { |                 backup: 'Respaldo', | ||||||
|                 share: 'Share', |                 export_diagram: 'Exportar Diagrama', | ||||||
|                 export_diagram: 'Export Diagram', |                 restore_diagram: 'Restaurar Diagrama', | ||||||
|                 import_diagram: 'Import Diagram', |  | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'Ayuda', |                 help: 'Ayuda', | ||||||
|                 visit_website: 'Visitar ChartDB', |                 docs_website: 'Documentación', | ||||||
|                 join_discord: 'Únete a nosotros en Discord', |                 join_discord: 'Únete a nosotros en Discord', | ||||||
|                 schedule_a_call: '¡Habla con nosotros!', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -116,6 +114,12 @@ export const es: LanguageTranslation = { | |||||||
|                 add_table: 'Agregar Tabla', |                 add_table: 'Agregar Tabla', | ||||||
|                 filter: 'Filtrar', |                 filter: 'Filtrar', | ||||||
|                 collapse: 'Colapsar Todo', |                 collapse: 'Colapsar Todo', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No tables found matching your filter.', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 show_list: 'Show Table List', | ||||||
|  |                 show_dbml: 'Show DBML Editor', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'Campos', |                     fields: 'Campos', | ||||||
| @@ -136,6 +140,8 @@ export const es: LanguageTranslation = { | |||||||
|                         comments: 'Comentarios', |                         comments: 'Comentarios', | ||||||
|                         no_comments: 'Sin comentarios', |                         no_comments: 'Sin comentarios', | ||||||
|                         delete_field: 'Eliminar Campo', |                         delete_field: 'Eliminar Campo', | ||||||
|  |                         // TODO: Translate | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: 'Atributos del Índice', |                         title: 'Atributos del Índice', | ||||||
| @@ -195,6 +201,53 @@ export const es: LanguageTranslation = { | |||||||
|                     description: 'Crea una vista para comenzar', |                     description: 'Crea una vista para comenzar', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -221,7 +274,7 @@ export const es: LanguageTranslation = { | |||||||
|                 title: 'Importa tu Base de Datos', |                 title: 'Importa tu Base de Datos', | ||||||
|                 database_edition: 'Edición de Base de Datos:', |                 database_edition: 'Edición de Base de Datos:', | ||||||
|                 step_1: 'Ejecuta este script en tu 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í...', |                 script_results_placeholder: 'Resultados del script aquí...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'Instrucciones SSMS', |                     button_text: 'Instrucciones SSMS', | ||||||
| @@ -317,6 +370,12 @@ export const es: LanguageTranslation = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'Cancelar', |             cancel: 'Cancelar', | ||||||
|             export: 'Exportar', |             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: { |         new_table_schema_dialog: { | ||||||
| @@ -360,7 +419,7 @@ export const es: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error exporting diagram', |                 title: 'Error exporting diagram', | ||||||
|                 description: |                 description: | ||||||
|                     'Something went wrong. Need help? chartdb.io@gmail.com', |                     'Something went wrong. Need help? support@chartdb.io', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         // TODO: Translate |         // TODO: Translate | ||||||
| @@ -372,7 +431,21 @@ export const es: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error importing diagram', |                 title: 'Error importing diagram', | ||||||
|                 description: |                 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 | ||||||
|  |         import_dbml_dialog: { | ||||||
|  |             example_title: 'Import Example DBML', | ||||||
|  |             title: 'Import DBML', | ||||||
|  |             description: 'Import a database schema from DBML format.', | ||||||
|  |             import: 'Import', | ||||||
|  |             cancel: 'Cancel', | ||||||
|  |             skip_and_empty: 'Skip & Empty', | ||||||
|  |             show_example: 'Show Example', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Error', | ||||||
|  |                 description: 'Failed to parse DBML. Please check the syntax.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         relationship_type: { |         relationship_type: { | ||||||
| @@ -385,12 +458,15 @@ export const es: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: 'Nueva Tabla', |             new_table: 'Nueva Tabla', | ||||||
|             new_relationship: 'Nueva Relación', |             new_relationship: 'Nueva Relación', | ||||||
|  |             // TODO: Translate | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'Editar Tabla', |             edit_table: 'Editar Tabla', | ||||||
|             duplicate_table: 'Duplicate Table', // TODO: Translate |             duplicate_table: 'Duplicate Table', // TODO: Translate | ||||||
|             delete_table: 'Eliminar Tabla', |             delete_table: 'Eliminar Tabla', | ||||||
|  |             add_relationship: 'Add Relationship', // TODO: Translate | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         // TODO: Add translations |         // TODO: Add translations | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const fr: LanguageTranslation = { | |||||||
|                 new: 'Nouveau', |                 new: 'Nouveau', | ||||||
|                 open: 'Ouvrir', |                 open: 'Ouvrir', | ||||||
|                 save: 'Enregistrer', |                 save: 'Enregistrer', | ||||||
|                 import_database: 'Importer Base de Données', |                 import: 'Importer Base de Données', | ||||||
|                 export_sql: 'Exporter SQL', |                 export_sql: 'Exporter SQL', | ||||||
|                 export_as: 'Exporter en tant que', |                 export_as: 'Exporter en tant que', | ||||||
|                 delete_diagram: 'Supprimer le Diagramme', |                 delete_diagram: 'Supprimer le Diagramme', | ||||||
| @@ -30,20 +30,18 @@ export const fr: LanguageTranslation = { | |||||||
|                 theme: 'Thème', |                 theme: 'Thème', | ||||||
|                 show_dependencies: 'Afficher les Dépendances', |                 show_dependencies: 'Afficher les Dépendances', | ||||||
|                 hide_dependencies: 'Masquer les Dépendances', |                 hide_dependencies: 'Masquer les Dépendances', | ||||||
|                 // TODO: Translate |                 show_minimap: 'Afficher la Mini Carte', | ||||||
|                 show_minimap: 'Show Mini Map', |                 hide_minimap: 'Masquer la Mini Carte', | ||||||
|                 hide_minimap: 'Hide Mini Map', |  | ||||||
|             }, |             }, | ||||||
|             share: { |             backup: { | ||||||
|                 share: 'Partage', |                 backup: 'Sauvegarde', | ||||||
|                 export_diagram: 'Exporter le diagramme', |                 export_diagram: 'Exporter le diagramme', | ||||||
|                 import_diagram: 'Importer un diagramme', |                 restore_diagram: 'Restaurer le diagramme', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'Aide', |                 help: 'Aide', | ||||||
|                 visit_website: 'Visitez ChartDB', |                 docs_website: 'Documentation', | ||||||
|                 join_discord: 'Rejoignez-nous sur Discord', |                 join_discord: 'Rejoignez-nous sur Discord', | ||||||
|                 schedule_a_call: 'Parlez avec nous !', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -101,9 +99,8 @@ export const fr: LanguageTranslation = { | |||||||
|         clear: 'Effacer', |         clear: 'Effacer', | ||||||
|         show_more: 'Afficher Plus', |         show_more: 'Afficher Plus', | ||||||
|         show_less: 'Afficher Moins', |         show_less: 'Afficher Moins', | ||||||
|         // TODO: Translate |         copy_to_clipboard: 'Copier dans le presse-papiers', | ||||||
|         copy_to_clipboard: 'Copy to Clipboard', |         copied: 'Copié !', | ||||||
|         copied: 'Copied!', |  | ||||||
|  |  | ||||||
|         side_panel: { |         side_panel: { | ||||||
|             schema: 'Schéma:', |             schema: 'Schéma:', | ||||||
| @@ -116,6 +113,11 @@ export const fr: LanguageTranslation = { | |||||||
|                 add_table: 'Ajouter une Table', |                 add_table: 'Ajouter une Table', | ||||||
|                 filter: 'Filtrer', |                 filter: 'Filtrer', | ||||||
|                 collapse: 'Réduire Tout', |                 collapse: 'Réduire Tout', | ||||||
|  |                 clear: 'Effacer le Filtre', | ||||||
|  |                 no_results: | ||||||
|  |                     'Aucune table trouvée correspondant à votre filtre.', | ||||||
|  |                 show_list: 'Afficher la Liste des Tableaux', | ||||||
|  |                 show_dbml: "Afficher l'éditeur DBML", | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'Champs', |                     fields: 'Champs', | ||||||
| @@ -136,6 +138,8 @@ export const fr: LanguageTranslation = { | |||||||
|                         comments: 'Commentaires', |                         comments: 'Commentaires', | ||||||
|                         no_comments: 'Pas de commentaires', |                         no_comments: 'Pas de commentaires', | ||||||
|                         delete_field: 'Supprimer le Champ', |                         delete_field: 'Supprimer le Champ', | ||||||
|  |                         // TODO: Translate | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: "Attributs de l'Index", |                         title: "Attributs de l'Index", | ||||||
| @@ -147,7 +151,7 @@ export const fr: LanguageTranslation = { | |||||||
|                         title: 'Actions de la Table', |                         title: 'Actions de la Table', | ||||||
|                         add_field: 'Ajouter un Champ', |                         add_field: 'Ajouter un Champ', | ||||||
|                         add_index: 'Ajouter un Index', |                         add_index: 'Ajouter un Index', | ||||||
|                         duplicate_table: 'Duplicate Table', // TODO: Translate |                         duplicate_table: 'Tableau dupliqué', | ||||||
|                         delete_table: 'Supprimer la Table', |                         delete_table: 'Supprimer la Table', | ||||||
|                         change_schema: 'Changer le Schéma', |                         change_schema: 'Changer le Schéma', | ||||||
|                     }, |                     }, | ||||||
| @@ -195,6 +199,53 @@ export const fr: LanguageTranslation = { | |||||||
|                     description: 'Créez une vue pour commencer', |                     description: 'Créez une vue pour commencer', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -221,7 +272,7 @@ export const fr: LanguageTranslation = { | |||||||
|                 title: 'Importer votre Base de Données', |                 title: 'Importer votre Base de Données', | ||||||
|                 database_edition: 'Édition de la 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_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...', |                 script_results_placeholder: 'Résultats du script ici...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'Instructions SSMS', |                     button_text: 'Instructions SSMS', | ||||||
| @@ -230,14 +281,12 @@ export const fr: LanguageTranslation = { | |||||||
|                     step_2: 'Si vous utilisez "Résultats en Grille", changez le nombre maximum de caractères récupérés pour les données non-XML (définir à 9999999).', |                     step_2: 'Si vous utilisez "Résultats en Grille", changez le nombre maximum de caractères récupérés pour les données non-XML (définir à 9999999).', | ||||||
|                 }, |                 }, | ||||||
|                 instructions_link: "Besoin d'aide ? Regardez comment", |                 instructions_link: "Besoin d'aide ? Regardez comment", | ||||||
|                 // TODO: Translate |                 check_script_result: 'Vérifier le résultat du Script', | ||||||
|                 check_script_result: 'Check Script Result', |  | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|             cancel: 'Annuler', |             cancel: 'Annuler', | ||||||
|             back: 'Retour', |             back: 'Retour', | ||||||
|             // TODO: Translate |             import_from_file: "Importer à partir d'un fichier", | ||||||
|             import_from_file: 'Import from File', |  | ||||||
|             empty_diagram: 'Diagramme vide', |             empty_diagram: 'Diagramme vide', | ||||||
|             continue: 'Continuer', |             continue: 'Continuer', | ||||||
|             import: 'Importer', |             import: 'Importer', | ||||||
| @@ -284,6 +333,12 @@ export const fr: LanguageTranslation = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'Annuler', |             cancel: 'Annuler', | ||||||
|             export: 'Exporter', |             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: { |         multiple_schemas_alert: { | ||||||
| @@ -352,29 +407,42 @@ export const fr: LanguageTranslation = { | |||||||
|                 cancel: 'Annuler', |                 cancel: 'Annuler', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         // TODO: Translate |  | ||||||
|         export_diagram_dialog: { |         export_diagram_dialog: { | ||||||
|             title: 'Export Diagram', |             title: 'Exporter le Diagramme', | ||||||
|             description: 'Choose the format for export:', |             description: "Sélectionner le format d'exportation :", | ||||||
|             format_json: 'JSON', |             format_json: 'JSON', | ||||||
|             cancel: 'Cancel', |             cancel: 'Annuler', | ||||||
|             export: 'Export', |             export: 'Exporter', | ||||||
|             error: { |             error: { | ||||||
|                 title: 'Error exporting diagram', |                 title: "Erreur lors de l'exportation du diagramme", | ||||||
|                 description: |                 description: | ||||||
|                     'Something went wrong. Need help? chartdb.io@gmail.com', |                     "Une erreur s'est produite. Besoin d'aide ? support@chartdb.io", | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         // TODO: Translate |  | ||||||
|         import_diagram_dialog: { |         import_diagram_dialog: { | ||||||
|             title: 'Import Diagram', |             title: 'Importer un diagramme', | ||||||
|             description: 'Paste the diagram JSON below:', |             description: 'Coller le diagramme au format JSON ci-dessous :', | ||||||
|             cancel: 'Cancel', |             cancel: 'Annuler', | ||||||
|             import: 'Import', |             import: 'Exporter', | ||||||
|             error: { |             error: { | ||||||
|                 title: 'Error importing diagram', |                 title: "Erreur lors de l'exportation du diagramme", | ||||||
|                 description: |                 description: | ||||||
|                     'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com', |                     "Le diagramme JSON n'est pas valide. Veuillez vérifier le JSON et réessayer. Besoin d'aide ? support@chartdb.io", | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |         import_dbml_dialog: { | ||||||
|  |             example_title: "Exemple d'importation DBML", | ||||||
|  |             title: 'Import DBML', | ||||||
|  |             description: | ||||||
|  |                 'Importer un schéma de base de données à partir du format DBML.', | ||||||
|  |             import: 'Importer', | ||||||
|  |             cancel: 'Annuler', | ||||||
|  |             skip_and_empty: 'Passer et vider', | ||||||
|  |             show_example: 'Afficher un exemple', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Erreur', | ||||||
|  |                 description: | ||||||
|  |                     "Erreur d'analyse du DBML. Veuillez vérifier la syntaxe.", | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         relationship_type: { |         relationship_type: { | ||||||
| @@ -387,16 +455,19 @@ export const fr: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: 'Nouvelle Table', |             new_table: 'Nouvelle Table', | ||||||
|             new_relationship: 'Nouvelle Relation', |             new_relationship: 'Nouvelle Relation', | ||||||
|  |             // TODO: Translate | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'Éditer la Table', |             edit_table: 'Éditer la Table', | ||||||
|             duplicate_table: 'Duplicate Table', // TODO: Translate |             duplicate_table: 'Tableau Dupliqué', | ||||||
|             delete_table: 'Supprimer la Table', |             delete_table: 'Supprimer la Table', | ||||||
|  |             add_relationship: 'Ajouter une Relation', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         // TODO: Add translations |         snap_to_grid_tooltip: | ||||||
|         snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})', |             'Aligner sur la grille (maintenir la touche {{key}})', | ||||||
|  |  | ||||||
|         tool_tips: { |         tool_tips: { | ||||||
|             double_click_to_edit: 'Double-cliquez pour modifier', |             double_click_to_edit: 'Double-cliquez pour modifier', | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const gu: LanguageTranslation = { | |||||||
|                 new: 'નવું', |                 new: 'નવું', | ||||||
|                 open: 'ખોલો', |                 open: 'ખોલો', | ||||||
|                 save: 'સાચવો', |                 save: 'સાચવો', | ||||||
|                 import_database: 'ડેટાબેસ આયાત કરો', |                 import: 'ડેટાબેસ આયાત કરો', | ||||||
|                 export_sql: 'SQL નિકાસ કરો', |                 export_sql: 'SQL નિકાસ કરો', | ||||||
|                 export_as: 'રૂપે નિકાસ કરો', |                 export_as: 'રૂપે નિકાસ કરો', | ||||||
|                 delete_diagram: 'ડાયાગ્રામ કાઢી નાખો', |                 delete_diagram: 'ડાયાગ્રામ કાઢી નાખો', | ||||||
| @@ -35,16 +35,15 @@ export const gu: LanguageTranslation = { | |||||||
|                 hide_minimap: 'Hide Mini Map', |                 hide_minimap: 'Hide Mini Map', | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|             share: { |             backup: { | ||||||
|                 share: 'શેર કરો', |                 backup: 'બેકઅપ', | ||||||
|                 export_diagram: 'ડાયાગ્રામ નિકાસ કરો', |                 export_diagram: 'ડાયાગ્રામ નિકાસ કરો', | ||||||
|                 import_diagram: 'ડાયાગ્રામ આયાત કરો', |                 restore_diagram: 'ડાયાગ્રામ પુનઃસ્થાપિત કરો', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'મદદ', |                 help: 'મદદ', | ||||||
|                 visit_website: 'ChartDB વેબસાઇટ પર જાઓ', |                 docs_website: 'દસ્તાવેજીકરણ', | ||||||
|                 join_discord: 'અમારા Discordમાં જોડાઓ', |                 join_discord: 'અમારા Discordમાં જોડાઓ', | ||||||
|                 schedule_a_call: 'અમારી સાથે વાત કરો!', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -125,6 +124,12 @@ export const gu: LanguageTranslation = { | |||||||
|                 add_table: 'ટેબલ ઉમેરો', |                 add_table: 'ટેબલ ઉમેરો', | ||||||
|                 filter: 'ફિલ્ટર', |                 filter: 'ફિલ્ટર', | ||||||
|                 collapse: 'બધાને સકુચિત કરો', |                 collapse: 'બધાને સકુચિત કરો', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No tables found matching your filter.', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 show_list: 'Show Table List', | ||||||
|  |                 show_dbml: 'Show DBML Editor', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'ફીલ્ડ્સ', |                     fields: 'ફીલ્ડ્સ', | ||||||
| @@ -146,6 +151,8 @@ export const gu: LanguageTranslation = { | |||||||
|                         comments: 'ટિપ્પણીઓ', |                         comments: 'ટિપ્પણીઓ', | ||||||
|                         no_comments: 'કોઈ ટિપ્પણીઓ નથી', |                         no_comments: 'કોઈ ટિપ્પણીઓ નથી', | ||||||
|                         delete_field: 'ફીલ્ડ કાઢી નાખો', |                         delete_field: 'ફીલ્ડ કાઢી નાખો', | ||||||
|  |                         // TODO: Translate | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: 'ઇન્ડેક્સ લક્ષણો', |                         title: 'ઇન્ડેક્સ લક્ષણો', | ||||||
| @@ -205,6 +212,53 @@ export const gu: LanguageTranslation = { | |||||||
|                     description: 'આ વિભાગમાં કોઈ નિર્ભરતા ઉપલબ્ધ નથી.', |                     description: 'આ વિભાગમાં કોઈ નિર્ભરતા ઉપલબ્ધ નથી.', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -230,7 +284,7 @@ export const gu: LanguageTranslation = { | |||||||
|                 title: 'તમારું ડેટાબેસ આયાત કરો', |                 title: 'તમારું ડેટાબેસ આયાત કરો', | ||||||
|                 database_edition: 'ડેટાબેસ આવૃત્તિ:', |                 database_edition: 'ડેટાબેસ આવૃત્તિ:', | ||||||
|                 step_1: 'તમારા ડેટાબેસમાં આ સ્ક્રિપ્ટ ચલાવો:', |                 step_1: 'તમારા ડેટાબેસમાં આ સ્ક્રિપ્ટ ચલાવો:', | ||||||
|                 step_2: 'સ્ક્રિપ્ટનો પરિણામ અહીં પેસ્ટ કરો:', |                 step_2: 'સ્ક્રિપ્ટનો પરિણામ અહીં પેસ્ટ કરો →', | ||||||
|                 script_results_placeholder: 'સ્ક્રિપ્ટના પરિણામ અહીં...', |                 script_results_placeholder: 'સ્ક્રિપ્ટના પરિણામ અહીં...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'SSMS સૂચનાઓ', |                     button_text: 'SSMS સૂચનાઓ', | ||||||
| @@ -324,6 +378,12 @@ export const gu: LanguageTranslation = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'રદ કરો', |             cancel: 'રદ કરો', | ||||||
|             export: 'નિકાસ કરો', |             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: { |         new_table_schema_dialog: { | ||||||
| @@ -358,7 +418,7 @@ export const gu: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'ડાયાગ્રામ નિકાસમાં ભૂલ', |                 title: 'ડાયાગ્રામ નિકાસમાં ભૂલ', | ||||||
|                 description: |                 description: | ||||||
|                     'કશુક તો ખોટું થયું. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.', |                     'કશુક તો ખોટું થયું. મદદ જોઈએ? support@chartdb.io પર સંપર્ક કરો.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -370,7 +430,21 @@ export const gu: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'ડાયાગ્રામ આયાતમાં ભૂલ', |                 title: 'ડાયાગ્રામ આયાતમાં ભૂલ', | ||||||
|                 description: |                 description: | ||||||
|                     'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.', |                     'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને 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', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Error', | ||||||
|  |                 description: 'Failed to parse DBML. Please check the syntax.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         relationship_type: { |         relationship_type: { | ||||||
| @@ -383,12 +457,15 @@ export const gu: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: 'નવું ટેબલ', |             new_table: 'નવું ટેબલ', | ||||||
|             new_relationship: 'નવો સંબંધ', |             new_relationship: 'નવો સંબંધ', | ||||||
|  |             // TODO: Translate | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'ટેબલ સંપાદિત કરો', |             edit_table: 'ટેબલ સંપાદિત કરો', | ||||||
|             duplicate_table: 'ટેબલ નકલ કરો', |             duplicate_table: 'ટેબલ નકલ કરો', | ||||||
|             delete_table: 'ટેબલ કાઢી નાખો', |             delete_table: 'ટેબલ કાઢી નાખો', | ||||||
|  |             add_relationship: 'Add Relationship', // TODO: Translate | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         snap_to_grid_tooltip: 'ગ્રિડ પર સ્નેપ કરો (જમાવટ {{key}})', |         snap_to_grid_tooltip: 'ગ્રિડ પર સ્નેપ કરો (જમાવટ {{key}})', | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const hi: LanguageTranslation = { | |||||||
|                 new: 'नया', |                 new: 'नया', | ||||||
|                 open: 'खोलें', |                 open: 'खोलें', | ||||||
|                 save: 'सहेजें', |                 save: 'सहेजें', | ||||||
|                 import_database: 'डेटाबेस आयात करें', |                 import: 'डेटाबेस आयात करें', | ||||||
|                 export_sql: 'SQL निर्यात करें', |                 export_sql: 'SQL निर्यात करें', | ||||||
|                 export_as: 'के रूप में निर्यात करें', |                 export_as: 'के रूप में निर्यात करें', | ||||||
|                 delete_diagram: 'आरेख हटाएँ', |                 delete_diagram: 'आरेख हटाएँ', | ||||||
| @@ -34,17 +34,15 @@ export const hi: LanguageTranslation = { | |||||||
|                 show_minimap: 'Show Mini Map', |                 show_minimap: 'Show Mini Map', | ||||||
|                 hide_minimap: 'Hide Mini Map', |                 hide_minimap: 'Hide Mini Map', | ||||||
|             }, |             }, | ||||||
|             // TODO: Translate |             backup: { | ||||||
|             share: { |                 backup: 'बैकअप', | ||||||
|                 share: 'Share', |                 export_diagram: 'आरेख निर्यात करें', | ||||||
|                 export_diagram: 'Export Diagram', |                 restore_diagram: 'आरेख पुनर्स्थापित करें', | ||||||
|                 import_diagram: 'Import Diagram', |  | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'मदद', |                 help: 'मदद', | ||||||
|                 visit_website: 'ChartDB वेबसाइट पर जाएँ', |                 docs_website: 'દસ્તાવેજીકરણ', | ||||||
|                 join_discord: 'हमसे Discord पर जुड़ें', |                 join_discord: 'हमसे Discord पर जुड़ें', | ||||||
|                 schedule_a_call: 'हमसे बात करें!', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -126,6 +124,12 @@ export const hi: LanguageTranslation = { | |||||||
|                 add_table: 'तालिका जोड़ें', |                 add_table: 'तालिका जोड़ें', | ||||||
|                 filter: 'फ़िल्टर', |                 filter: 'फ़िल्टर', | ||||||
|                 collapse: 'सभी को संक्षिप्त करें', |                 collapse: 'सभी को संक्षिप्त करें', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No tables found matching your filter.', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 show_list: 'Show Table List', | ||||||
|  |                 show_dbml: 'Show DBML Editor', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'फ़ील्ड्स', |                     fields: 'फ़ील्ड्स', | ||||||
| @@ -146,6 +150,8 @@ export const hi: LanguageTranslation = { | |||||||
|                         comments: 'टिप्पणियाँ', |                         comments: 'टिप्पणियाँ', | ||||||
|                         no_comments: 'कोई टिप्पणी नहीं', |                         no_comments: 'कोई टिप्पणी नहीं', | ||||||
|                         delete_field: 'फ़ील्ड हटाएँ', |                         delete_field: 'फ़ील्ड हटाएँ', | ||||||
|  |                         // TODO: Translate | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: 'सूचकांक विशेषताएँ', |                         title: 'सूचकांक विशेषताएँ', | ||||||
| @@ -206,6 +212,53 @@ export const hi: LanguageTranslation = { | |||||||
|                     description: 'इस अनुभाग में कोई निर्भरता उपलब्ध नहीं है।', |                     description: 'इस अनुभाग में कोई निर्भरता उपलब्ध नहीं है।', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -232,7 +285,7 @@ export const hi: LanguageTranslation = { | |||||||
|                 title: 'अपना डेटाबेस आयात करें', |                 title: 'अपना डेटाबेस आयात करें', | ||||||
|                 database_edition: 'डेटाबेस संस्करण:', |                 database_edition: 'डेटाबेस संस्करण:', | ||||||
|                 step_1: 'अपने डेटाबेस में यह स्क्रिप्ट चलाएँ:', |                 step_1: 'अपने डेटाबेस में यह स्क्रिप्ट चलाएँ:', | ||||||
|                 step_2: 'यहाँ स्क्रिप्ट का परिणाम पेस्ट करें:', |                 step_2: 'यहाँ स्क्रिप्ट का परिणाम पेस्ट करें →', | ||||||
|                 script_results_placeholder: 'स्क्रिप्ट के परिणाम यहाँ...', |                 script_results_placeholder: 'स्क्रिप्ट के परिणाम यहाँ...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'SSMS निर्देश', |                     button_text: 'SSMS निर्देश', | ||||||
| @@ -328,6 +381,12 @@ export const hi: LanguageTranslation = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'रद्द करें', |             cancel: 'रद्द करें', | ||||||
|             export: 'निर्यात करें', |             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: { |         new_table_schema_dialog: { | ||||||
| @@ -362,7 +421,7 @@ export const hi: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error exporting diagram', |                 title: 'Error exporting diagram', | ||||||
|                 description: |                 description: | ||||||
|                     'Something went wrong. Need help? chartdb.io@gmail.com', |                     'Something went wrong. Need help? support@chartdb.io', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         // TODO: Translate |         // TODO: Translate | ||||||
| @@ -374,7 +433,21 @@ export const hi: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error importing diagram', |                 title: 'Error importing diagram', | ||||||
|                 description: |                 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 | ||||||
|  |         import_dbml_dialog: { | ||||||
|  |             example_title: 'Import Example DBML', | ||||||
|  |             title: 'Import DBML', | ||||||
|  |             description: 'Import a database schema from DBML format.', | ||||||
|  |             import: 'Import', | ||||||
|  |             cancel: 'Cancel', | ||||||
|  |             skip_and_empty: 'Skip & Empty', | ||||||
|  |             show_example: 'Show Example', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Error', | ||||||
|  |                 description: 'Failed to parse DBML. Please check the syntax.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         relationship_type: { |         relationship_type: { | ||||||
| @@ -387,12 +460,15 @@ export const hi: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: 'नई तालिका', |             new_table: 'नई तालिका', | ||||||
|             new_relationship: 'नया संबंध', |             new_relationship: 'नया संबंध', | ||||||
|  |             // TODO: Translate | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'तालिका संपादित करें', |             edit_table: 'तालिका संपादित करें', | ||||||
|             duplicate_table: 'Duplicate Table', // TODO: Translate |             duplicate_table: 'Duplicate Table', // TODO: Translate | ||||||
|             delete_table: 'तालिका हटाएँ', |             delete_table: 'तालिका हटाएँ', | ||||||
|  |             add_relationship: 'Add Relationship', // TODO: Translate | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         // TODO: Add translations |         // TODO: Add translations | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const id_ID: LanguageTranslation = { | |||||||
|                 new: 'Buat Baru', |                 new: 'Buat Baru', | ||||||
|                 open: 'Buka', |                 open: 'Buka', | ||||||
|                 save: 'Simpan', |                 save: 'Simpan', | ||||||
|                 import_database: 'Impor Database', |                 import: 'Impor Database', | ||||||
|                 export_sql: 'Ekspor SQL', |                 export_sql: 'Ekspor SQL', | ||||||
|                 export_as: 'Ekspor Sebagai', |                 export_as: 'Ekspor Sebagai', | ||||||
|                 delete_diagram: 'Hapus Diagram', |                 delete_diagram: 'Hapus Diagram', | ||||||
| @@ -34,16 +34,15 @@ export const id_ID: LanguageTranslation = { | |||||||
|                 show_minimap: 'Show Mini Map', |                 show_minimap: 'Show Mini Map', | ||||||
|                 hide_minimap: 'Hide Mini Map', |                 hide_minimap: 'Hide Mini Map', | ||||||
|             }, |             }, | ||||||
|             share: { |             backup: { | ||||||
|                 share: 'Bagikan', |                 backup: 'Cadangan', | ||||||
|                 export_diagram: 'Ekspor Diagram', |                 export_diagram: 'Ekspor Diagram', | ||||||
|                 import_diagram: 'Impor Diagram', |                 restore_diagram: 'Pulihkan Diagram', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'Bantuan', |                 help: 'Bantuan', | ||||||
|                 visit_website: 'Kunjungi ChartDB', |                 docs_website: 'Dokumentasi', | ||||||
|                 join_discord: 'Bergabunglah di Discord kami', |                 join_discord: 'Bergabunglah di Discord kami', | ||||||
|                 schedule_a_call: 'Berbicara dengan kami!', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -124,6 +123,12 @@ export const id_ID: LanguageTranslation = { | |||||||
|                 add_table: 'Tambah Tabel', |                 add_table: 'Tambah Tabel', | ||||||
|                 filter: 'Saring', |                 filter: 'Saring', | ||||||
|                 collapse: 'Lipat Semua', |                 collapse: 'Lipat Semua', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No tables found matching your filter.', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 show_list: 'Show Table List', | ||||||
|  |                 show_dbml: 'Show DBML Editor', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'Kolom', |                     fields: 'Kolom', | ||||||
| @@ -144,6 +149,8 @@ export const id_ID: LanguageTranslation = { | |||||||
|                         comments: 'Komentar', |                         comments: 'Komentar', | ||||||
|                         no_comments: 'Tidak ada komentar', |                         no_comments: 'Tidak ada komentar', | ||||||
|                         delete_field: 'Hapus Kolom', |                         delete_field: 'Hapus Kolom', | ||||||
|  |                         // TODO: Translate | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: 'Atribut Indeks', |                         title: 'Atribut Indeks', | ||||||
| @@ -203,6 +210,53 @@ export const id_ID: LanguageTranslation = { | |||||||
|                     description: 'Buat tampilan untuk memulai', |                     description: 'Buat tampilan untuk memulai', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -229,7 +283,7 @@ export const id_ID: LanguageTranslation = { | |||||||
|                 title: 'Impor Database Anda', |                 title: 'Impor Database Anda', | ||||||
|                 database_edition: 'Edisi Database:', |                 database_edition: 'Edisi Database:', | ||||||
|                 step_1: 'Jalankan skrip ini di database Anda:', |                 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...', |                 script_results_placeholder: 'Hasil skrip di sini...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'Instruksi SSMS', |                     button_text: 'Instruksi SSMS', | ||||||
| @@ -322,6 +376,12 @@ export const id_ID: LanguageTranslation = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'Batal', |             cancel: 'Batal', | ||||||
|             export: 'Ekspor', |             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: { |         new_table_schema_dialog: { | ||||||
| @@ -356,7 +416,7 @@ export const id_ID: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error ekspor diagram', |                 title: 'Error ekspor diagram', | ||||||
|                 description: |                 description: | ||||||
|                     'Sesuatu yang salah. Butuh bantuan? chartdb.io@gmail.com', |                     'Sesuatu yang salah. Butuh bantuan? support@chartdb.io', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -368,7 +428,21 @@ export const id_ID: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error impor diagram', |                 title: 'Error impor diagram', | ||||||
|                 description: |                 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 | ||||||
|  |         import_dbml_dialog: { | ||||||
|  |             example_title: 'Import Example DBML', | ||||||
|  |             title: 'Import DBML', | ||||||
|  |             description: 'Import a database schema from DBML format.', | ||||||
|  |             import: 'Import', | ||||||
|  |             cancel: 'Cancel', | ||||||
|  |             skip_and_empty: 'Skip & Empty', | ||||||
|  |             show_example: 'Show Example', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Error', | ||||||
|  |                 description: 'Failed to parse DBML. Please check the syntax.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -382,12 +456,15 @@ export const id_ID: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: 'Tabel Baru', |             new_table: 'Tabel Baru', | ||||||
|             new_relationship: 'Hubungan Baru', |             new_relationship: 'Hubungan Baru', | ||||||
|  |             // TODO: Translate | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'Ubah Tabel', |             edit_table: 'Ubah Tabel', | ||||||
|             delete_table: 'Hapus Tabel', |             delete_table: 'Hapus Tabel', | ||||||
|             duplicate_table: 'Duplikat Tabel', |             duplicate_table: 'Duplikat Tabel', | ||||||
|  |             add_relationship: 'Add Relationship', // TODO: Translate | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         snap_to_grid_tooltip: 'Snap ke Kisi (Tahan {{key}})', |         snap_to_grid_tooltip: 'Snap ke Kisi (Tahan {{key}})', | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const ja: LanguageTranslation = { | |||||||
|                 new: '新規', |                 new: '新規', | ||||||
|                 open: '開く', |                 open: '開く', | ||||||
|                 save: '保存', |                 save: '保存', | ||||||
|                 import_database: 'データベースをインポート', |                 import: 'データベースをインポート', | ||||||
|                 export_sql: 'SQLをエクスポート', |                 export_sql: 'SQLをエクスポート', | ||||||
|                 export_as: '形式を指定してエクスポート', |                 export_as: '形式を指定してエクスポート', | ||||||
|                 delete_diagram: 'ダイアグラムを削除', |                 delete_diagram: 'ダイアグラムを削除', | ||||||
| @@ -36,16 +36,15 @@ export const ja: LanguageTranslation = { | |||||||
|                 hide_minimap: 'Hide Mini Map', |                 hide_minimap: 'Hide Mini Map', | ||||||
|             }, |             }, | ||||||
|             // TODO: Translate |             // TODO: Translate | ||||||
|             share: { |             backup: { | ||||||
|                 share: 'Share', |                 backup: 'Backup', | ||||||
|                 export_diagram: 'Export Diagram', |                 export_diagram: 'Export Diagram', | ||||||
|                 import_diagram: 'Import Diagram', |                 restore_diagram: 'Restore Diagram', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'ヘルプ', |                 help: 'ヘルプ', | ||||||
|                 visit_website: 'ChartDBにアクセス', |                 docs_website: 'ドキュメント', | ||||||
|                 join_discord: 'Discordに参加', |                 join_discord: 'Discordに参加', | ||||||
|                 schedule_a_call: '話しかけてください!', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -128,6 +127,12 @@ export const ja: LanguageTranslation = { | |||||||
|                 add_table: 'テーブルを追加', |                 add_table: 'テーブルを追加', | ||||||
|                 filter: 'フィルタ', |                 filter: 'フィルタ', | ||||||
|                 collapse: 'すべて折りたたむ', |                 collapse: 'すべて折りたたむ', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No tables found matching your filter.', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 show_list: 'Show Table List', | ||||||
|  |                 show_dbml: 'Show DBML Editor', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'フィールド', |                     fields: 'フィールド', | ||||||
| @@ -148,6 +153,8 @@ export const ja: LanguageTranslation = { | |||||||
|                         comments: 'コメント', |                         comments: 'コメント', | ||||||
|                         no_comments: 'コメントがありません', |                         no_comments: 'コメントがありません', | ||||||
|                         delete_field: 'フィールドを削除', |                         delete_field: 'フィールドを削除', | ||||||
|  |                         // TODO: Translate | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: 'インデックス属性', |                         title: 'インデックス属性', | ||||||
| @@ -209,6 +216,53 @@ export const ja: LanguageTranslation = { | |||||||
|                     description: 'Create a view to get started', |                     description: 'Create a view to get started', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -235,7 +289,7 @@ export const ja: LanguageTranslation = { | |||||||
|                 title: 'データベースをインポート', |                 title: 'データベースをインポート', | ||||||
|                 database_edition: 'データベースエディション:', |                 database_edition: 'データベースエディション:', | ||||||
|                 step_1: 'このスクリプトをデータベースで実行してください:', |                 step_1: 'このスクリプトをデータベースで実行してください:', | ||||||
|                 step_2: 'ここにスクリプトの結果を貼り付けてください:', |                 step_2: 'ここにスクリプトの結果を貼り付けてください →', | ||||||
|                 script_results_placeholder: 'ここにスクリプトの結果...', |                 script_results_placeholder: 'ここにスクリプトの結果...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'SSMSの手順', |                     button_text: 'SSMSの手順', | ||||||
| @@ -331,6 +385,12 @@ export const ja: LanguageTranslation = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'キャンセル', |             cancel: 'キャンセル', | ||||||
|             export: 'エクスポート', |             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: { |         new_table_schema_dialog: { | ||||||
| @@ -365,7 +425,7 @@ export const ja: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error exporting diagram', |                 title: 'Error exporting diagram', | ||||||
|                 description: |                 description: | ||||||
|                     'Something went wrong. Need help? chartdb.io@gmail.com', |                     'Something went wrong. Need help? support@chartdb.io', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         // TODO: Translate |         // TODO: Translate | ||||||
| @@ -377,7 +437,21 @@ export const ja: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error importing diagram', |                 title: 'Error importing diagram', | ||||||
|                 description: |                 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 | ||||||
|  |         import_dbml_dialog: { | ||||||
|  |             example_title: 'Import Example DBML', | ||||||
|  |             title: 'Import DBML', | ||||||
|  |             description: 'Import a database schema from DBML format.', | ||||||
|  |             import: 'Import', | ||||||
|  |             cancel: 'Cancel', | ||||||
|  |             skip_and_empty: 'Skip & Empty', | ||||||
|  |             show_example: 'Show Example', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Error', | ||||||
|  |                 description: 'Failed to parse DBML. Please check the syntax.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         relationship_type: { |         relationship_type: { | ||||||
| @@ -390,12 +464,15 @@ export const ja: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: '新しいテーブル', |             new_table: '新しいテーブル', | ||||||
|             new_relationship: '新しいリレーションシップ', |             new_relationship: '新しいリレーションシップ', | ||||||
|  |             // TODO: Translate | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'テーブルを編集', |             edit_table: 'テーブルを編集', | ||||||
|             duplicate_table: 'Duplicate Table', // TODO: Translate |             duplicate_table: 'Duplicate Table', // TODO: Translate | ||||||
|             delete_table: 'テーブルを削除', |             delete_table: 'テーブルを削除', | ||||||
|  |             add_relationship: 'Add Relationship', // TODO: Translate | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         // TODO: Add translations |         // TODO: Add translations | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const ko_KR: LanguageTranslation = { | |||||||
|                 new: '새 다이어그램', |                 new: '새 다이어그램', | ||||||
|                 open: '열기', |                 open: '열기', | ||||||
|                 save: '저장', |                 save: '저장', | ||||||
|                 import_database: '데이터베이스 가져오기', |                 import: '데이터베이스 가져오기', | ||||||
|                 export_sql: 'SQL로 저장', |                 export_sql: 'SQL로 저장', | ||||||
|                 export_as: '다른 형식으로 저장', |                 export_as: '다른 형식으로 저장', | ||||||
|                 delete_diagram: '다이어그램 삭제', |                 delete_diagram: '다이어그램 삭제', | ||||||
| @@ -34,16 +34,15 @@ export const ko_KR: LanguageTranslation = { | |||||||
|                 show_minimap: 'Show Mini Map', |                 show_minimap: 'Show Mini Map', | ||||||
|                 hide_minimap: 'Hide Mini Map', |                 hide_minimap: 'Hide Mini Map', | ||||||
|             }, |             }, | ||||||
|             share: { |             backup: { | ||||||
|                 share: '공유', |                 backup: '백업', | ||||||
|                 export_diagram: '다이어그램 내보내기', |                 export_diagram: '다이어그램 내보내기', | ||||||
|                 import_diagram: '다이어그램 가져오기', |                 restore_diagram: '다이어그램 복구', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: '도움말', |                 help: '도움말', | ||||||
|                 visit_website: 'ChartDB 사이트 방문', |                 docs_website: '선적 서류 비치', | ||||||
|                 join_discord: 'Discord 가입', |                 join_discord: 'Discord 가입', | ||||||
|                 schedule_a_call: 'Talk with us!', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -124,6 +123,12 @@ export const ko_KR: LanguageTranslation = { | |||||||
|                 add_table: '테이블 추가', |                 add_table: '테이블 추가', | ||||||
|                 filter: '필터', |                 filter: '필터', | ||||||
|                 collapse: '모두 접기', |                 collapse: '모두 접기', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No tables found matching your filter.', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 show_list: 'Show Table List', | ||||||
|  |                 show_dbml: 'Show DBML Editor', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: '필드', |                     fields: '필드', | ||||||
| @@ -144,6 +149,8 @@ export const ko_KR: LanguageTranslation = { | |||||||
|                         comments: '주석', |                         comments: '주석', | ||||||
|                         no_comments: '주석 없음', |                         no_comments: '주석 없음', | ||||||
|                         delete_field: '필드 삭제', |                         delete_field: '필드 삭제', | ||||||
|  |                         // TODO: Translate | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: '인덱스 속성', |                         title: '인덱스 속성', | ||||||
| @@ -203,6 +210,53 @@ export const ko_KR: LanguageTranslation = { | |||||||
|                     description: '뷰 테이블을 만들어 시작하세요.', |                     description: '뷰 테이블을 만들어 시작하세요.', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -229,7 +283,7 @@ export const ko_KR: LanguageTranslation = { | |||||||
|                 title: '당신의 데이터베이스를 가져오세요', |                 title: '당신의 데이터베이스를 가져오세요', | ||||||
|                 database_edition: '데이터베이스 세부 종류:', |                 database_edition: '데이터베이스 세부 종류:', | ||||||
|                 step_1: '데이터베이스에서 아래의 SQL을 실행해주세요:', |                 step_1: '데이터베이스에서 아래의 SQL을 실행해주세요:', | ||||||
|                 step_2: '이곳에 결과를 붙여넣어주세요:', |                 step_2: '이곳에 결과를 붙여넣어주세요 →', | ||||||
|                 script_results_placeholder: '이곳에 스크립트 결과를 입력...', |                 script_results_placeholder: '이곳에 스크립트 결과를 입력...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'SSMS을 사용하시는 경우', |                     button_text: 'SSMS을 사용하시는 경우', | ||||||
| @@ -322,6 +376,12 @@ export const ko_KR: LanguageTranslation = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: '취소', |             cancel: '취소', | ||||||
|             export: '내보내기', |             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: { |         new_table_schema_dialog: { | ||||||
| @@ -355,7 +415,7 @@ export const ko_KR: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: '다이어그램 내보내기 오류', |                 title: '다이어그램 내보내기 오류', | ||||||
|                 description: |                 description: | ||||||
|                     '무언가 문제가 발생하였습니다. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.', |                     '무언가 문제가 발생하였습니다. 도움이 필요하신 경우 support@chartdb.io으로 연락해주세요.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         import_diagram_dialog: { |         import_diagram_dialog: { | ||||||
| @@ -366,7 +426,21 @@ export const ko_KR: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: '다이어그램 가져오기 오류', |                 title: '다이어그램 가져오기 오류', | ||||||
|                 description: |                 description: | ||||||
|                     '다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.', |                     '다이어그램 JSON이 유효하지 않습니다. 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', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Error', | ||||||
|  |                 description: 'Failed to parse DBML. Please check the syntax.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         relationship_type: { |         relationship_type: { | ||||||
| @@ -379,12 +453,15 @@ export const ko_KR: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: '새 테이블', |             new_table: '새 테이블', | ||||||
|             new_relationship: '새 연관관계', |             new_relationship: '새 연관관계', | ||||||
|  |             // TODO: Translate | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: '테이블 수정', |             edit_table: '테이블 수정', | ||||||
|             duplicate_table: '테이블 복제', |             duplicate_table: '테이블 복제', | ||||||
|             delete_table: '테이블 삭제', |             delete_table: '테이블 삭제', | ||||||
|  |             add_relationship: 'Add Relationship', // TODO: Translate | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         snap_to_grid_tooltip: '그리드에 맞추기 ({{key}}를 누른채 유지)', |         snap_to_grid_tooltip: '그리드에 맞추기 ({{key}}를 누른채 유지)', | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const mr: LanguageTranslation = { | |||||||
|                 new: 'नवीन', |                 new: 'नवीन', | ||||||
|                 open: 'उघडा', |                 open: 'उघडा', | ||||||
|                 save: 'जतन करा', |                 save: 'जतन करा', | ||||||
|                 import_database: 'डेटाबेस इम्पोर्ट करा', |                 import: 'डेटाबेस इम्पोर्ट करा', | ||||||
|                 export_sql: 'SQL एक्स्पोर्ट करा', |                 export_sql: 'SQL एक्स्पोर्ट करा', | ||||||
|                 export_as: 'म्हणून एक्स्पोर्ट करा', |                 export_as: 'म्हणून एक्स्पोर्ट करा', | ||||||
|                 delete_diagram: 'आरेख हटवा', |                 delete_diagram: 'आरेख हटवा', | ||||||
| @@ -34,17 +34,16 @@ export const mr: LanguageTranslation = { | |||||||
|                 show_minimap: 'Show Mini Map', |                 show_minimap: 'Show Mini Map', | ||||||
|                 hide_minimap: 'Hide Mini Map', |                 hide_minimap: 'Hide Mini Map', | ||||||
|             }, |             }, | ||||||
|             share: { |             backup: { | ||||||
|                 // TODO: Add translations |                 // TODO: Add translations | ||||||
|                 share: 'Share', |                 backup: 'Backup', | ||||||
|                 export_diagram: 'Export Diagram', |                 export_diagram: 'Export Diagram', | ||||||
|                 import_diagram: 'Import Diagram', |                 restore_diagram: 'Restore Diagram', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'मदत', |                 help: 'मदत', | ||||||
|                 visit_website: 'ChartDB ला भेट द्या', |                 docs_website: 'दस्तऐवजीकरण', | ||||||
|                 join_discord: 'आमच्या डिस्कॉर्डमध्ये सामील व्हा', |                 join_discord: 'आमच्या डिस्कॉर्डमध्ये सामील व्हा', | ||||||
|                 schedule_a_call: 'आमच्याशी बोला!', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -127,6 +126,12 @@ export const mr: LanguageTranslation = { | |||||||
|                 add_table: 'टेबल जोडा', |                 add_table: 'टेबल जोडा', | ||||||
|                 filter: 'फिल्टर', |                 filter: 'फिल्टर', | ||||||
|                 collapse: 'सर्व संकुचित करा', |                 collapse: 'सर्व संकुचित करा', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No tables found matching your filter.', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 show_list: 'Show Table List', | ||||||
|  |                 show_dbml: 'Show DBML Editor', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'फील्ड्स', |                     fields: 'फील्ड्स', | ||||||
| @@ -147,6 +152,8 @@ export const mr: LanguageTranslation = { | |||||||
|                         comments: 'टिप्पण्या', |                         comments: 'टिप्पण्या', | ||||||
|                         no_comments: 'कोणत्याही टिप्पणी नाहीत', |                         no_comments: 'कोणत्याही टिप्पणी नाहीत', | ||||||
|                         delete_field: 'फील्ड हटवा', |                         delete_field: 'फील्ड हटवा', | ||||||
|  |                         // TODO: Translate | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: 'इंडेक्स गुणधर्म', |                         title: 'इंडेक्स गुणधर्म', | ||||||
| @@ -208,6 +215,53 @@ export const mr: LanguageTranslation = { | |||||||
|                     description: 'सुरू करण्यासाठी एक दृश्य तयार करा', |                     description: 'सुरू करण्यासाठी एक दृश्य तयार करा', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -234,7 +288,7 @@ export const mr: LanguageTranslation = { | |||||||
|                 title: 'तुमचा डेटाबेस आयात करा', |                 title: 'तुमचा डेटाबेस आयात करा', | ||||||
|                 database_edition: 'डेटाबेस संस्करण:', |                 database_edition: 'डेटाबेस संस्करण:', | ||||||
|                 step_1: 'तुमच्या डेटाबेसमध्ये हा स्क्रिप्ट चालवा:', |                 step_1: 'तुमच्या डेटाबेसमध्ये हा स्क्रिप्ट चालवा:', | ||||||
|                 step_2: 'स्क्रिप्टचा परिणाम येथे पेस्ट करा:', |                 step_2: 'स्क्रिप्टचा परिणाम येथे पेस्ट करा →', | ||||||
|                 script_results_placeholder: 'स्क्रिप्ट परिणाम येथे...', |                 script_results_placeholder: 'स्क्रिप्ट परिणाम येथे...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'SSMS सूचना', |                     button_text: 'SSMS सूचना', | ||||||
| @@ -330,6 +384,12 @@ export const mr: LanguageTranslation = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'रद्द करा', |             cancel: 'रद्द करा', | ||||||
|             export: 'निर्यात करा', |             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: { |         new_table_schema_dialog: { | ||||||
| @@ -365,7 +425,7 @@ export const mr: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error exporting diagram', |                 title: 'Error exporting diagram', | ||||||
|                 description: |                 description: | ||||||
|                     'Something went wrong. Need help? chartdb.io@gmail.com', |                     'Something went wrong. Need help? support@chartdb.io', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -378,7 +438,21 @@ export const mr: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error importing diagram', |                 title: 'Error importing diagram', | ||||||
|                 description: |                 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 | ||||||
|  |         import_dbml_dialog: { | ||||||
|  |             example_title: 'Import Example DBML', | ||||||
|  |             title: 'Import DBML', | ||||||
|  |             description: 'Import a database schema from DBML format.', | ||||||
|  |             import: 'Import', | ||||||
|  |             cancel: 'Cancel', | ||||||
|  |             skip_and_empty: 'Skip & Empty', | ||||||
|  |             show_example: 'Show Example', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Error', | ||||||
|  |                 description: 'Failed to parse DBML. Please check the syntax.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -392,13 +466,15 @@ export const mr: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: 'नवीन टेबल', |             new_table: 'नवीन टेबल', | ||||||
|             new_relationship: 'नवीन रिलेशनशिप', |             new_relationship: 'नवीन रिलेशनशिप', | ||||||
|  |             // TODO: Translate | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'टेबल संपादित करा', |             edit_table: 'टेबल संपादित करा', | ||||||
|             delete_table: 'टेबल हटवा', |             delete_table: 'टेबल हटवा', | ||||||
|             // TODO: Add translations |             duplicate_table: 'Duplicate Table', // TODO: Translate | ||||||
|             duplicate_table: 'Duplicate Table', |             add_relationship: 'Add Relationship', // TODO: Translate | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         // TODO: Add translations |         // TODO: Add translations | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const ne: LanguageTranslation = { | |||||||
|                 new: 'नयाँ', |                 new: 'नयाँ', | ||||||
|                 open: 'खोल्नुहोस्', |                 open: 'खोल्नुहोस्', | ||||||
|                 save: 'सुरक्षित गर्नुहोस्', |                 save: 'सुरक्षित गर्नुहोस्', | ||||||
|                 import_database: 'डाटाबेस आयात गर्नुहोस्', |                 import: 'डाटाबेस आयात गर्नुहोस्', | ||||||
|                 export_sql: 'SQL निर्यात गर्नुहोस्', |                 export_sql: 'SQL निर्यात गर्नुहोस्', | ||||||
|                 export_as: 'निर्यात गर्नुहोस्', |                 export_as: 'निर्यात गर्नुहोस्', | ||||||
|                 delete_diagram: 'डायाग्राम हटाउनुहोस्', |                 delete_diagram: 'डायाग्राम हटाउनुहोस्', | ||||||
| @@ -34,16 +34,16 @@ export const ne: LanguageTranslation = { | |||||||
|                 show_minimap: 'Show Mini Map', |                 show_minimap: 'Show Mini Map', | ||||||
|                 hide_minimap: 'Hide Mini Map', |                 hide_minimap: 'Hide Mini Map', | ||||||
|             }, |             }, | ||||||
|             share: { |             // TODO: Translate | ||||||
|                 share: 'शेयर गर्नुहोस्', |             backup: { | ||||||
|                 export_diagram: 'डायाग्राम निर्यात गर्नुहोस्', |                 backup: 'Backup', | ||||||
|                 import_diagram: 'डायाग्राम आयात गर्नुहोस्', |                 export_diagram: 'Export Diagram', | ||||||
|  |                 restore_diagram: 'Restore Diagram', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'मद्दत', |                 help: 'मद्दत', | ||||||
|                 visit_website: 'वेबसाइटमा जानुहोस्', |                 docs_website: 'कागजात', | ||||||
|                 join_discord: 'डिस्कोर्डमा सामिल हुनुहोस्', |                 join_discord: 'डिस्कोर्डमा सामिल हुनुहोस्', | ||||||
|                 schedule_a_call: 'कल अनुसूची गर्नुहोस्', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -124,6 +124,12 @@ export const ne: LanguageTranslation = { | |||||||
|                 add_table: 'तालिका थप्नुहोस्', |                 add_table: 'तालिका थप्नुहोस्', | ||||||
|                 filter: 'फिल्टर', |                 filter: 'फिल्टर', | ||||||
|                 collapse: 'सबै लुकाउनुहोस्', |                 collapse: 'सबै लुकाउनुहोस्', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No tables found matching your filter.', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 show_list: 'Show Table List', | ||||||
|  |                 show_dbml: 'Show DBML Editor', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'क्षेत्रहरू', |                     fields: 'क्षेत्रहरू', | ||||||
| @@ -144,6 +150,8 @@ export const ne: LanguageTranslation = { | |||||||
|                         comments: 'टिप्पणीहरू', |                         comments: 'टिप्पणीहरू', | ||||||
|                         no_comments: 'कुनै टिप्पणीहरू छैनन्', |                         no_comments: 'कुनै टिप्पणीहरू छैनन्', | ||||||
|                         delete_field: 'क्षेत्र हटाउनुहोस्', |                         delete_field: 'क्षेत्र हटाउनुहोस्', | ||||||
|  |                         // TODO: Translate | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: 'सूचक विशेषताहरू', |                         title: 'सूचक विशेषताहरू', | ||||||
| @@ -204,6 +212,53 @@ export const ne: LanguageTranslation = { | |||||||
|                         'डिपेन्डेन्सीहरू देखाउनका लागि एक व्यू बनाउनुहोस्', |                         'डिपेन्डेन्सीहरू देखाउनका लागि एक व्यू बनाउनुहोस्', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -231,7 +286,7 @@ export const ne: LanguageTranslation = { | |||||||
|                 title: 'तपाईंको डाटाबेस आयात गर्नुहोस्', |                 title: 'तपाईंको डाटाबेस आयात गर्नुहोस्', | ||||||
|                 database_edition: 'डाटाबेस संस्करण:', |                 database_edition: 'डाटाबेस संस्करण:', | ||||||
|                 step_1: 'तपाईंको डाटाबेसमा यो स्क्रिप्ट चलाउनुहोस्:', |                 step_1: 'तपाईंको डाटाबेसमा यो स्क्रिप्ट चलाउनुहोस्:', | ||||||
|                 step_2: 'यो स्क्रिप्ट परिणाम यहाँ पेस्ट गर्नुहोस्:', |                 step_2: 'यो स्क्रिप्ट परिणाम यहाँ पेस्ट गर्नुहोस् →', | ||||||
|                 script_results_placeholder: 'स्क्रिप्ट परिणाम यहाँ...', |                 script_results_placeholder: 'स्क्रिप्ट परिणाम यहाँ...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'SSMS निर्देशन', |                     button_text: 'SSMS निर्देशन', | ||||||
| @@ -326,6 +381,12 @@ export const ne: LanguageTranslation = { | |||||||
|             scale_4x: '४x', |             scale_4x: '४x', | ||||||
|             cancel: 'रद्द गर्नुहोस्', |             cancel: 'रद्द गर्नुहोस्', | ||||||
|             export: 'निर्यात गर्नुहोस्', |             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: { |         new_table_schema_dialog: { | ||||||
| @@ -359,7 +420,7 @@ export const ne: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error exporting diagram', |                 title: 'Error exporting diagram', | ||||||
|                 description: |                 description: | ||||||
|                     'Something went wrong. Need help? chartdb.io@gmail.com', |                     'Something went wrong. Need help? support@chartdb.io', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -371,7 +432,21 @@ export const ne: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'डायाग्राम आयात गर्दा समस्या आयो', |                 title: 'डायाग्राम आयात गर्दा समस्या आयो', | ||||||
|                 description: |                 description: | ||||||
|                     'डायाग्राम JSON अमान्य छ। कृपया JSON जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्। मद्दत चाहिन्छ? chartdb.io@gmail.com मा सम्पर्क गर्नुहोस्', |                     'डायाग्राम JSON अमान्य छ। कृपया 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', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Error', | ||||||
|  |                 description: 'Failed to parse DBML. Please check the syntax.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -385,12 +460,15 @@ export const ne: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: 'नयाँ तालिका', |             new_table: 'नयाँ तालिका', | ||||||
|             new_relationship: 'नयाँ सम्बन्ध', |             new_relationship: 'नयाँ सम्बन्ध', | ||||||
|  |             // TODO: Translate | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'तालिका सम्पादन गर्नुहोस्', |             edit_table: 'तालिका सम्पादन गर्नुहोस्', | ||||||
|             duplicate_table: 'तालिका नक्कली गर्नुहोस्', |             duplicate_table: 'तालिका नक्कली गर्नुहोस्', | ||||||
|             delete_table: 'तालिका हटाउनुहोस्', |             delete_table: 'तालिका हटाउनुहोस्', | ||||||
|  |             add_relationship: 'Add Relationship', // TODO: Translate | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         snap_to_grid_tooltip: 'ग्रिडमा स्न्याप गर्नुहोस् ({{key}} थिच्नुहोस)', |         snap_to_grid_tooltip: 'ग्रिडमा स्न्याप गर्नुहोस् ({{key}} थिच्नुहोस)', | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const pt_BR: LanguageTranslation = { | |||||||
|                 new: 'Novo', |                 new: 'Novo', | ||||||
|                 open: 'Abrir', |                 open: 'Abrir', | ||||||
|                 save: 'Salvar', |                 save: 'Salvar', | ||||||
|                 import_database: 'Importar Banco de Dados', |                 import: 'Importar Banco de Dados', | ||||||
|                 export_sql: 'Exportar SQL', |                 export_sql: 'Exportar SQL', | ||||||
|                 export_as: 'Exportar como', |                 export_as: 'Exportar como', | ||||||
|                 delete_diagram: 'Excluir Diagrama', |                 delete_diagram: 'Excluir Diagrama', | ||||||
| @@ -35,16 +35,15 @@ export const pt_BR: LanguageTranslation = { | |||||||
|                 hide_minimap: 'Hide Mini Map', |                 hide_minimap: 'Hide Mini Map', | ||||||
|             }, |             }, | ||||||
|             // TODO: Translate |             // TODO: Translate | ||||||
|             share: { |             backup: { | ||||||
|                 share: 'Share', |                 backup: 'Backup', | ||||||
|                 export_diagram: 'Export Diagram', |                 export_diagram: 'Exportar Diagrama', | ||||||
|                 import_diagram: 'Import Diagram', |                 restore_diagram: 'Restaurar Diagrama', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'Ajuda', |                 help: 'Ajuda', | ||||||
|                 visit_website: 'Visitar ChartDB', |                 docs_website: 'Documentação', | ||||||
|                 join_discord: 'Junte-se a nós no Discord', |                 join_discord: 'Junte-se a nós no Discord', | ||||||
|                 schedule_a_call: 'Fale Conosco!', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -125,6 +124,12 @@ export const pt_BR: LanguageTranslation = { | |||||||
|                 add_table: 'Adicionar Tabela', |                 add_table: 'Adicionar Tabela', | ||||||
|                 filter: 'Filtrar', |                 filter: 'Filtrar', | ||||||
|                 collapse: 'Colapsar Todas', |                 collapse: 'Colapsar Todas', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No tables found matching your filter.', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 show_list: 'Show Table List', | ||||||
|  |                 show_dbml: 'Show DBML Editor', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'Campos', |                     fields: 'Campos', | ||||||
| @@ -145,6 +150,8 @@ export const pt_BR: LanguageTranslation = { | |||||||
|                         comments: 'Comentários', |                         comments: 'Comentários', | ||||||
|                         no_comments: 'Sem comentários', |                         no_comments: 'Sem comentários', | ||||||
|                         delete_field: 'Excluir Campo', |                         delete_field: 'Excluir Campo', | ||||||
|  |                         // TODO: Translate | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: 'Atributos do Índice', |                         title: 'Atributos do Índice', | ||||||
| @@ -204,6 +211,53 @@ export const pt_BR: LanguageTranslation = { | |||||||
|                     description: 'Crie uma visualização para começar', |                     description: 'Crie uma visualização para começar', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -230,7 +284,7 @@ export const pt_BR: LanguageTranslation = { | |||||||
|                 title: 'Importe seu Banco de Dados', |                 title: 'Importe seu Banco de Dados', | ||||||
|                 database_edition: 'Edição do Banco de Dados:', |                 database_edition: 'Edição do Banco de Dados:', | ||||||
|                 step_1: 'Execute este script no seu 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...', |                 script_results_placeholder: 'Resultados do script aqui...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'Instruções do SSMS', |                     button_text: 'Instruções do SSMS', | ||||||
| @@ -325,6 +379,12 @@ export const pt_BR: LanguageTranslation = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'Cancelar', |             cancel: 'Cancelar', | ||||||
|             export: 'Exportar', |             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: { |         new_table_schema_dialog: { | ||||||
| @@ -359,7 +419,7 @@ export const pt_BR: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error exporting diagram', |                 title: 'Error exporting diagram', | ||||||
|                 description: |                 description: | ||||||
|                     'Something went wrong. Need help? chartdb.io@gmail.com', |                     'Something went wrong. Need help? support@chartdb.io', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         // TODO: Translate |         // TODO: Translate | ||||||
| @@ -371,7 +431,21 @@ export const pt_BR: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error importing diagram', |                 title: 'Error importing diagram', | ||||||
|                 description: |                 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 | ||||||
|  |         import_dbml_dialog: { | ||||||
|  |             example_title: 'Import Example DBML', | ||||||
|  |             title: 'Import DBML', | ||||||
|  |             description: 'Import a database schema from DBML format.', | ||||||
|  |             import: 'Import', | ||||||
|  |             cancel: 'Cancel', | ||||||
|  |             skip_and_empty: 'Skip & Empty', | ||||||
|  |             show_example: 'Show Example', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Error', | ||||||
|  |                 description: 'Failed to parse DBML. Please check the syntax.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         relationship_type: { |         relationship_type: { | ||||||
| @@ -384,12 +458,15 @@ export const pt_BR: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: 'Nova Tabela', |             new_table: 'Nova Tabela', | ||||||
|             new_relationship: 'Novo Relacionamento', |             new_relationship: 'Novo Relacionamento', | ||||||
|  |             // TODO: Translate | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'Editar Tabela', |             edit_table: 'Editar Tabela', | ||||||
|             duplicate_table: 'Duplicate Table', // TODO: Translate |             duplicate_table: 'Duplicate Table', // TODO: Translate | ||||||
|             delete_table: 'Excluir Tabela', |             delete_table: 'Excluir Tabela', | ||||||
|  |             add_relationship: 'Add Relationship', // TODO: Translate | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         // TODO: Add translations |         // TODO: Add translations | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const ru: LanguageTranslation = { | |||||||
|                 new: 'Создать', |                 new: 'Создать', | ||||||
|                 open: 'Открыть', |                 open: 'Открыть', | ||||||
|                 save: 'Сохранить', |                 save: 'Сохранить', | ||||||
|                 import_database: 'Импортировать базу данных', |                 import: 'Импортировать базу данных', | ||||||
|                 export_sql: 'Экспорт SQL', |                 export_sql: 'Экспорт SQL', | ||||||
|                 export_as: 'Экспортировать как', |                 export_as: 'Экспортировать как', | ||||||
|                 delete_diagram: 'Удалить диаграмму', |                 delete_diagram: 'Удалить диаграмму', | ||||||
| @@ -24,26 +24,24 @@ export const ru: LanguageTranslation = { | |||||||
|                 view: 'Вид', |                 view: 'Вид', | ||||||
|                 show_sidebar: 'Показать боковую панель', |                 show_sidebar: 'Показать боковую панель', | ||||||
|                 hide_sidebar: 'Скрыть боковую панель', |                 hide_sidebar: 'Скрыть боковую панель', | ||||||
|                 hide_cardinality: 'Скрыть множественность связи', |                 hide_cardinality: 'Скрыть виды связи', | ||||||
|                 show_cardinality: 'Показать множественность связи', |                 show_cardinality: 'Показать виды связи', | ||||||
|                 zoom_on_scroll: 'Увеличение при прокрутке', |                 zoom_on_scroll: 'Увеличение при прокрутке', | ||||||
|                 theme: 'Тема', |                 theme: 'Тема', | ||||||
|                 show_dependencies: 'Показать зависимости', |                 show_dependencies: 'Показать зависимости', | ||||||
|                 hide_dependencies: 'Скрыть зависимости', |                 hide_dependencies: 'Скрыть зависимости', | ||||||
|                 // TODO: Translate |                 show_minimap: 'Показать мини-карту', | ||||||
|                 show_minimap: 'Show Mini Map', |                 hide_minimap: 'Скрыть мини-карту', | ||||||
|                 hide_minimap: 'Hide Mini Map', |  | ||||||
|             }, |             }, | ||||||
|             share: { |             backup: { | ||||||
|                 share: 'Поделиться', |                 backup: 'Бэкап', | ||||||
|                 export_diagram: 'Экспорт кода диаграммы', |                 export_diagram: 'Экспорт диаграммы', | ||||||
|                 import_diagram: 'Импорт кода диаграммы', |                 restore_diagram: 'Восстановить диаграмму', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'Помощь', |                 help: 'Помощь', | ||||||
|                 visit_website: 'Перейти на сайт ChartDB', |                 docs_website: 'Документация', | ||||||
|                 join_discord: 'Присоединиться к сообществу в Discord', |                 join_discord: 'Присоединиться к сообществу в Discord', | ||||||
|                 schedule_a_call: 'Поговорите с нами!', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -123,11 +121,17 @@ export const ru: LanguageTranslation = { | |||||||
|                 add_table: 'Добавить таблицу', |                 add_table: 'Добавить таблицу', | ||||||
|                 filter: 'Фильтр', |                 filter: 'Фильтр', | ||||||
|                 collapse: 'Свернуть все', |                 collapse: 'Свернуть все', | ||||||
|  |                 clear: 'Очистить фильтр', | ||||||
|  |  | ||||||
|  |                 no_results: | ||||||
|  |                     'Таблицы не найдены, соответствующие вашему фильтру.', | ||||||
|  |                 show_list: 'Переключиться на список таблиц', | ||||||
|  |                 show_dbml: 'Переключиться на редактор DBML', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'Поля', |                     fields: 'Поля', | ||||||
|                     nullable: 'Может содержать NULL?', |                     nullable: 'Может быть NULL?', | ||||||
|                     primary_key: 'Первичный ключ,', |                     primary_key: 'Первичный ключ', | ||||||
|                     indexes: 'Индексы', |                     indexes: 'Индексы', | ||||||
|                     comments: 'Комментарии', |                     comments: 'Комментарии', | ||||||
|                     no_comments: 'Нет комментария', |                     no_comments: 'Нет комментария', | ||||||
| @@ -143,6 +147,7 @@ export const ru: LanguageTranslation = { | |||||||
|                         comments: 'Комментарии', |                         comments: 'Комментарии', | ||||||
|                         no_comments: 'Нет комментария', |                         no_comments: 'Нет комментария', | ||||||
|                         delete_field: 'Удалить поле', |                         delete_field: 'Удалить поле', | ||||||
|  |                         character_length: 'Макс. длина', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: 'Атрибуты индекса', |                         title: 'Атрибуты индекса', | ||||||
| @@ -155,7 +160,7 @@ export const ru: LanguageTranslation = { | |||||||
|                         change_schema: 'Изменить схему', |                         change_schema: 'Изменить схему', | ||||||
|                         add_field: 'Добавить поле', |                         add_field: 'Добавить поле', | ||||||
|                         add_index: 'Добавить индекс', |                         add_index: 'Добавить индекс', | ||||||
|                         duplicate_table: 'Duplicate Table', // TODO: Translate |                         duplicate_table: 'Создать копию', | ||||||
|                         delete_table: 'Удалить таблицу', |                         delete_table: 'Удалить таблицу', | ||||||
|                     }, |                     }, | ||||||
|                 }, |                 }, | ||||||
| @@ -172,7 +177,7 @@ export const ru: LanguageTranslation = { | |||||||
|                 relationship: { |                 relationship: { | ||||||
|                     primary: 'Основная таблица', |                     primary: 'Основная таблица', | ||||||
|                     foreign: 'Справочная таблица', |                     foreign: 'Справочная таблица', | ||||||
|                     cardinality: 'Тип множественности связи', |                     cardinality: 'Тип множественной связи', | ||||||
|                     delete_relationship: 'Удалить', |                     delete_relationship: 'Удалить', | ||||||
|                     relationship_actions: { |                     relationship_actions: { | ||||||
|                         title: 'Действия', |                         title: 'Действия', | ||||||
| @@ -202,6 +207,54 @@ export const ru: LanguageTranslation = { | |||||||
|                     description: 'Создайте представление, чтобы начать', |                     description: 'Создайте представление, чтобы начать', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Области', | ||||||
|  |                 add_area: 'Добавить область', | ||||||
|  |                 filter: 'Фильтр', | ||||||
|  |                 clear: 'Очистить фильтр', | ||||||
|  |  | ||||||
|  |                 no_results: | ||||||
|  |                     'Области не найдены, соответствующие вашему фильтру.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Действия', | ||||||
|  |                         edit_name: 'Изменить название', | ||||||
|  |                         delete_area: 'Удалить область', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'Нет областей', | ||||||
|  |                     description: 'Создайте область, чтобы начать', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -228,7 +281,7 @@ export const ru: LanguageTranslation = { | |||||||
|                 title: 'Импортируйте свою базу данных', |                 title: 'Импортируйте свою базу данных', | ||||||
|                 database_edition: 'Версия базы данных:', |                 database_edition: 'Версия базы данных:', | ||||||
|                 step_1: 'Запустите этот скрипт в своей базе данных:', |                 step_1: 'Запустите этот скрипт в своей базе данных:', | ||||||
|                 step_2: 'Вставьте вывод скрипта сюда:', |                 step_2: 'Вставьте вывод скрипта сюда →', | ||||||
|                 script_results_placeholder: 'Вывод скрипта здесь...', |                 script_results_placeholder: 'Вывод скрипта здесь...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'SSMS Инструкции', |                     button_text: 'SSMS Инструкции', | ||||||
| @@ -323,6 +376,12 @@ export const ru: LanguageTranslation = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'Отменить', |             cancel: 'Отменить', | ||||||
|             export: 'Экспортировать', |             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: { |         new_table_schema_dialog: { | ||||||
| @@ -356,7 +415,7 @@ export const ru: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Ошибка экспортирования диаграммы', |                 title: 'Ошибка экспортирования диаграммы', | ||||||
|                 description: |                 description: | ||||||
|                     'Что-то пошло не так. Если вам нужна помощь, напишите нам: chartdb.io@gmail.com', |                     'Что-то пошло не так. Если вам нужна помощь, напишите нам: support@chartdb.io', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         import_diagram_dialog: { |         import_diagram_dialog: { | ||||||
| @@ -367,7 +426,22 @@ export const ru: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Ошибка при импорте диаграммы', |                 title: 'Ошибка при импорте диаграммы', | ||||||
|                 description: |                 description: | ||||||
|                     'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: chartdb.io@gmail.com', |                     'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: support@chartdb.io', | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |         import_dbml_dialog: { | ||||||
|  |             example_title: 'Импорт DBML', | ||||||
|  |             title: 'Импортировать DBML', | ||||||
|  |             description: 'Импортировать схему базы данных из DBML формата.', | ||||||
|  |             import: 'Импортировать', | ||||||
|  |             cancel: 'Отмена', | ||||||
|  |             skip_and_empty: 'Продолжить с пустой диаграммой', | ||||||
|  |             show_example: 'Использовать эту схему', | ||||||
|  |  | ||||||
|  |             error: { | ||||||
|  |                 title: 'Ошибка', | ||||||
|  |                 description: | ||||||
|  |                     'Ошибка парсинга DBML. Пожалуйста проверьте синтаксис.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         relationship_type: { |         relationship_type: { | ||||||
| @@ -380,12 +454,14 @@ export const ru: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: 'Создать таблицу', |             new_table: 'Создать таблицу', | ||||||
|             new_relationship: 'Создать отношение', |             new_relationship: 'Создать отношение', | ||||||
|  |             new_area: 'Новая область', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'Изменить таблицу', |             edit_table: 'Изменить таблицу', | ||||||
|             duplicate_table: 'Duplicate Table', // TODO: Translate |             duplicate_table: 'Создать копию', | ||||||
|             delete_table: 'Удалить таблицу', |             delete_table: 'Удалить таблицу', | ||||||
|  |             add_relationship: 'Добавить связь', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         copy_to_clipboard: 'Скопировать в буфер обмена', |         copy_to_clipboard: 'Скопировать в буфер обмена', | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const te: LanguageTranslation = { | |||||||
|                 new: 'కొత్తది', |                 new: 'కొత్తది', | ||||||
|                 open: 'తెరవు', |                 open: 'తెరవు', | ||||||
|                 save: 'సేవ్', |                 save: 'సేవ్', | ||||||
|                 import_database: 'డేటాబేస్ను దిగుమతి చేసుకోండి', |                 import: 'డేటాబేస్ను దిగుమతి చేసుకోండి', | ||||||
|                 export_sql: 'SQL ఎగుమతి', |                 export_sql: 'SQL ఎగుమతి', | ||||||
|                 export_as: 'వగా ఎగుమతి చేయండి', |                 export_as: 'వగా ఎగుమతి చేయండి', | ||||||
|                 delete_diagram: 'చిత్రాన్ని తొలగించండి', |                 delete_diagram: 'చిత్రాన్ని తొలగించండి', | ||||||
| @@ -35,16 +35,15 @@ export const te: LanguageTranslation = { | |||||||
|                 hide_minimap: 'Hide Mini Map', |                 hide_minimap: 'Hide Mini Map', | ||||||
|             }, |             }, | ||||||
|             // TODO: Translate |             // TODO: Translate | ||||||
|             share: { |             backup: { | ||||||
|                 share: 'Share', |                 backup: 'Backup', | ||||||
|                 export_diagram: 'Export Diagram', |                 export_diagram: 'Export Diagram', | ||||||
|                 import_diagram: 'Import Diagram', |                 restore_diagram: 'Restore Diagram', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'సహాయం', |                 help: 'సహాయం', | ||||||
|                 visit_website: 'ChartDB సందర్శించండి', |                 docs_website: 'డాక్యుమెంటేషన్', | ||||||
|                 join_discord: 'డిస్కార్డ్లో మా నుంచి చేరండి', |                 join_discord: 'డిస్కార్డ్లో మా నుంచి చేరండి', | ||||||
|                 schedule_a_call: 'మాతో మాట్లాడండి!', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -125,6 +124,12 @@ export const te: LanguageTranslation = { | |||||||
|                 add_table: 'పట్టికను జోడించు', |                 add_table: 'పట్టికను జోడించు', | ||||||
|                 filter: 'ఫిల్టర్', |                 filter: 'ఫిల్టర్', | ||||||
|                 collapse: 'అన్ని కూల్ చేయి', |                 collapse: 'అన్ని కూల్ చేయి', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No tables found matching your filter.', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 show_list: 'Show Table List', | ||||||
|  |                 show_dbml: 'Show DBML Editor', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'ఫీల్డులు', |                     fields: 'ఫీల్డులు', | ||||||
| @@ -145,6 +150,8 @@ export const te: LanguageTranslation = { | |||||||
|                         comments: 'వ్యాఖ్యలు', |                         comments: 'వ్యాఖ్యలు', | ||||||
|                         no_comments: 'వ్యాఖ్యలు లేవు', |                         no_comments: 'వ్యాఖ్యలు లేవు', | ||||||
|                         delete_field: 'ఫీల్డ్ తొలగించు', |                         delete_field: 'ఫీల్డ్ తొలగించు', | ||||||
|  |                         // TODO: Translate | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: 'ఇండెక్స్ గుణాలు', |                         title: 'ఇండెక్స్ గుణాలు', | ||||||
| @@ -205,6 +212,53 @@ export const te: LanguageTranslation = { | |||||||
|                     description: 'ప్రారంభించడానికి ఒక వీక్షణ సృష్టించండి', |                     description: 'ప్రారంభించడానికి ఒక వీక్షణ సృష్టించండి', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         toolbar: { |         toolbar: { | ||||||
| @@ -231,7 +285,7 @@ export const te: LanguageTranslation = { | |||||||
|                 title: 'మీ డేటాబేస్ను దిగుమతి చేసుకోండి', |                 title: 'మీ డేటాబేస్ను దిగుమతి చేసుకోండి', | ||||||
|                 database_edition: 'డేటాబేస్ ఎడిషన్:', |                 database_edition: 'డేటాబేస్ ఎడిషన్:', | ||||||
|                 step_1: 'ఈ స్క్రిప్ట్ను మీ డేటాబేస్లో అమలు చేయండి:', |                 step_1: 'ఈ స్క్రిప్ట్ను మీ డేటాబేస్లో అమలు చేయండి:', | ||||||
|                 step_2: 'స్క్రిప్ట్ ఫలితాన్ని ఇక్కడ పేస్ట్ చేయండి:', |                 step_2: 'స్క్రిప్ట్ ఫలితాన్ని ఇక్కడ పేస్ట్ చేయండి →', | ||||||
|                 script_results_placeholder: 'స్క్రిప్ట్ ఫలితాలు ఇక్కడ...', |                 script_results_placeholder: 'స్క్రిప్ట్ ఫలితాలు ఇక్కడ...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'SSMS సూచనల్ని చూపించు', |                     button_text: 'SSMS సూచనల్ని చూపించు', | ||||||
| @@ -326,6 +380,12 @@ export const te: LanguageTranslation = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'రద్దు', |             cancel: 'రద్దు', | ||||||
|             export: 'ఎగుమతి', |             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: { |         new_table_schema_dialog: { | ||||||
| @@ -361,7 +421,7 @@ export const te: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error exporting diagram', |                 title: 'Error exporting diagram', | ||||||
|                 description: |                 description: | ||||||
|                     'Something went wrong. Need help? chartdb.io@gmail.com', |                     'Something went wrong. Need help? support@chartdb.io', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -374,7 +434,21 @@ export const te: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error importing diagram', |                 title: 'Error importing diagram', | ||||||
|                 description: |                 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 | ||||||
|  |         import_dbml_dialog: { | ||||||
|  |             example_title: 'Import Example DBML', | ||||||
|  |             title: 'Import DBML', | ||||||
|  |             description: 'Import a database schema from DBML format.', | ||||||
|  |             import: 'Import', | ||||||
|  |             cancel: 'Cancel', | ||||||
|  |             skip_and_empty: 'Skip & Empty', | ||||||
|  |             show_example: 'Show Example', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Error', | ||||||
|  |                 description: 'Failed to parse DBML. Please check the syntax.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -388,13 +462,15 @@ export const te: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: 'కొత్త పట్టిక', |             new_table: 'కొత్త పట్టిక', | ||||||
|             new_relationship: 'కొత్త సంబంధం', |             new_relationship: 'కొత్త సంబంధం', | ||||||
|  |             // TODO: Translate | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'పట్టికను సవరించు', |             edit_table: 'పట్టికను సవరించు', | ||||||
|             // TODO: Translate |             duplicate_table: 'Duplicate Table', // TODO: Translate | ||||||
|             duplicate_table: 'Duplicate Table', |  | ||||||
|             delete_table: 'పట్టికను తొలగించు', |             delete_table: 'పట్టికను తొలగించు', | ||||||
|  |             add_relationship: 'Add Relationship', // TODO: Translate | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         // TODO: Translate |         // TODO: Translate | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ export const tr: LanguageTranslation = { | |||||||
|                 new: 'Yeni', |                 new: 'Yeni', | ||||||
|                 open: 'Aç', |                 open: 'Aç', | ||||||
|                 save: 'Kaydet', |                 save: 'Kaydet', | ||||||
|                 import_database: 'Veritabanı İçe Aktar', |                 import: 'Veritabanı İçe Aktar', | ||||||
|                 export_sql: 'SQL Olarak Dışa Aktar', |                 export_sql: 'SQL Olarak Dışa Aktar', | ||||||
|                 export_as: 'Olarak Dışa Aktar', |                 export_as: 'Olarak Dışa Aktar', | ||||||
|                 delete_diagram: 'Diyagramı Sil', |                 delete_diagram: 'Diyagramı Sil', | ||||||
| @@ -35,16 +35,15 @@ export const tr: LanguageTranslation = { | |||||||
|                 hide_minimap: 'Hide Mini Map', |                 hide_minimap: 'Hide Mini Map', | ||||||
|             }, |             }, | ||||||
|             // TODO: Translate |             // TODO: Translate | ||||||
|             share: { |             backup: { | ||||||
|                 share: 'Share', |                 backup: 'Backup', | ||||||
|                 export_diagram: 'Export Diagram', |                 export_diagram: 'Export Diagram', | ||||||
|                 import_diagram: 'Import Diagram', |                 restore_diagram: 'Restore Diagram', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'Yardım', |                 help: 'Yardım', | ||||||
|                 visit_website: "ChartDB'yi Ziyaret Et", |                 docs_website: 'Belgeleme', | ||||||
|                 join_discord: "Discord'a Katıl", |                 join_discord: "Discord'a Katıl", | ||||||
|                 schedule_a_call: 'Bize Ulaş!', |  | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
| @@ -124,6 +123,13 @@ export const tr: LanguageTranslation = { | |||||||
|                 add_table: 'Tablo Ekle', |                 add_table: 'Tablo Ekle', | ||||||
|                 filter: 'Filtrele', |                 filter: 'Filtrele', | ||||||
|                 collapse: 'Hepsini Daralt', |                 collapse: 'Hepsini Daralt', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No tables found matching your filter.', | ||||||
|  |                 // TODO: Translate | ||||||
|  |                 show_list: 'Show Table List', | ||||||
|  |                 show_dbml: 'Show DBML Editor', | ||||||
|  |  | ||||||
|                 table: { |                 table: { | ||||||
|                     fields: 'Alanlar', |                     fields: 'Alanlar', | ||||||
|                     nullable: 'Boş Bırakılabilir?', |                     nullable: 'Boş Bırakılabilir?', | ||||||
| @@ -143,6 +149,8 @@ export const tr: LanguageTranslation = { | |||||||
|                         comments: 'Yorumlar', |                         comments: 'Yorumlar', | ||||||
|                         no_comments: 'Yorum yok', |                         no_comments: 'Yorum yok', | ||||||
|                         delete_field: 'Alanı Sil', |                         delete_field: 'Alanı Sil', | ||||||
|  |                         // TODO: Translate | ||||||
|  |                         character_length: 'Max Length', | ||||||
|                     }, |                     }, | ||||||
|                     index_actions: { |                     index_actions: { | ||||||
|                         title: 'İndeks Özellikleri', |                         title: 'İndeks Özellikleri', | ||||||
| @@ -203,6 +211,53 @@ export const tr: LanguageTranslation = { | |||||||
|                     description: 'Başlamak için bir görünüm oluşturun', |                     description: 'Başlamak için bir görünüm oluşturun', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  |             // TODO: Translate | ||||||
|  |             areas_section: { | ||||||
|  |                 areas: 'Areas', | ||||||
|  |                 add_area: 'Add Area', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No areas found matching your filter.', | ||||||
|  |  | ||||||
|  |                 area: { | ||||||
|  |                     area_actions: { | ||||||
|  |                         title: 'Area Actions', | ||||||
|  |                         edit_name: 'Edit Name', | ||||||
|  |                         delete_area: 'Delete Area', | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No areas', | ||||||
|  |                     description: 'Create an area to get started', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             custom_types_section: { | ||||||
|  |                 custom_types: 'Custom Types', | ||||||
|  |                 filter: 'Filter', | ||||||
|  |                 clear: 'Clear Filter', | ||||||
|  |                 no_results: 'No custom types found matching your filter.', | ||||||
|  |                 empty_state: { | ||||||
|  |                     title: 'No custom types', | ||||||
|  |                     description: | ||||||
|  |                         'Custom types will appear here when they are available in your database', | ||||||
|  |                 }, | ||||||
|  |                 custom_type: { | ||||||
|  |                     kind: 'Kind', | ||||||
|  |                     enum_values: 'Enum Values', | ||||||
|  |                     composite_fields: 'Fields', | ||||||
|  |                     no_fields: 'No fields defined', | ||||||
|  |                     field_name_placeholder: 'Field name', | ||||||
|  |                     field_type_placeholder: 'Select type', | ||||||
|  |                     add_field: 'Add Field', | ||||||
|  |                     custom_type_actions: { | ||||||
|  |                         title: 'Actions', | ||||||
|  |                         delete_custom_type: 'Delete', | ||||||
|  |                     }, | ||||||
|  |                     delete_custom_type: 'Delete Type', | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|         }, |         }, | ||||||
|         toolbar: { |         toolbar: { | ||||||
|             zoom_in: 'Yakınlaştır', |             zoom_in: 'Yakınlaştır', | ||||||
| @@ -226,7 +281,7 @@ export const tr: LanguageTranslation = { | |||||||
|                 title: 'Veritabanını İçe Aktar', |                 title: 'Veritabanını İçe Aktar', | ||||||
|                 database_edition: 'Veritabanı Sürümü:', |                 database_edition: 'Veritabanı Sürümü:', | ||||||
|                 step_1: 'Bu komut dosyasını veritabanınızda çalıştırın:', |                 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...', |                 script_results_placeholder: 'Komut dosyası sonuçları burada...', | ||||||
|                 ssms_instructions: { |                 ssms_instructions: { | ||||||
|                     button_text: 'SSMS Talimatları', |                     button_text: 'SSMS Talimatları', | ||||||
| @@ -317,6 +372,12 @@ export const tr: LanguageTranslation = { | |||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'İptal', |             cancel: 'İptal', | ||||||
|             export: 'Dışa Aktar', |             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: { |         new_table_schema_dialog: { | ||||||
|             title: 'Şema Seç', |             title: 'Şema Seç', | ||||||
| @@ -348,7 +409,7 @@ export const tr: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error exporting diagram', |                 title: 'Error exporting diagram', | ||||||
|                 description: |                 description: | ||||||
|                     'Something went wrong. Need help? chartdb.io@gmail.com', |                     'Something went wrong. Need help? support@chartdb.io', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         // TODO: Translate |         // TODO: Translate | ||||||
| @@ -360,7 +421,21 @@ export const tr: LanguageTranslation = { | |||||||
|             error: { |             error: { | ||||||
|                 title: 'Error importing diagram', |                 title: 'Error importing diagram', | ||||||
|                 description: |                 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 | ||||||
|  |         import_dbml_dialog: { | ||||||
|  |             example_title: 'Import Example DBML', | ||||||
|  |             title: 'Import DBML', | ||||||
|  |             description: 'Import a database schema from DBML format.', | ||||||
|  |             import: 'Import', | ||||||
|  |             cancel: 'Cancel', | ||||||
|  |             skip_and_empty: 'Skip & Empty', | ||||||
|  |             show_example: 'Show Example', | ||||||
|  |             error: { | ||||||
|  |                 title: 'Error', | ||||||
|  |                 description: 'Failed to parse DBML. Please check the syntax.', | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         relationship_type: { |         relationship_type: { | ||||||
| @@ -372,12 +447,14 @@ export const tr: LanguageTranslation = { | |||||||
|         canvas_context_menu: { |         canvas_context_menu: { | ||||||
|             new_table: 'Yeni Tablo', |             new_table: 'Yeni Tablo', | ||||||
|             new_relationship: 'Yeni İlişki', |             new_relationship: 'Yeni İlişki', | ||||||
|  |             // TODO: Translate | ||||||
|  |             new_area: 'New Area', | ||||||
|         }, |         }, | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'Tabloyu Düzenle', |             edit_table: 'Tabloyu Düzenle', | ||||||
|             delete_table: 'Tabloyu Sil', |             delete_table: 'Tabloyu Sil', | ||||||
|             // TODO: Translate |             duplicate_table: 'Duplicate Table', // TODO: Translate | ||||||
|             duplicate_table: 'Duplicate Table', |             add_relationship: 'Add Relationship', // TODO: Translate | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         // TODO: Translate |         // TODO: Translate | ||||||
|   | |||||||