Compare commits
	
		
			161 Commits
		
	
	
		
			v1.6.1
			...
			jf/fix_fk_
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					1377bd524b | ||
| 
						 | 
					0d9f57a9c9 | ||
| 
						 | 
					b7dbe54c83 | ||
| 
						 | 
					43d1dfff71 | ||
| 
						 | 
					9949a46ee3 | ||
| 
						 | 
					dfbcf05b2f | ||
| 
						 | 
					f56fab9876 | ||
| 
						 | 
					c9ea7da092 | ||
| 
						 | 
					22d46e1e90 | ||
| 
						 | 
					6af94afc56 | ||
| 
						 | 
					f7f92903de | ||
| 
						 | 
					b35e17526b | ||
| 
						 | 
					bf32c08d37 | ||
| 
						 | 
					5d337409d6 | ||
| 
						 | 
					67f5ac303e | ||
| 
						 | 
					578546a171 | ||
| 
						 | 
					aa0b629a3e | ||
| 
						 | 
					69beaa0a83 | ||
| 
						 | 
					4fcc49d49a | ||
| 
						 | 
					d15985e399 | ||
| 
						 | 
					d429128e65 | ||
| 
						 | 
					2fce8326b6 | ||
| 
						 | 
					433c68a33d | ||
| 
						 | 
					58acb65f12 | ||
| 
						 | 
					7978955819 | ||
| 
						 | 
					c6118e0cdb | ||
| 
						 | 
					7d063b905f | ||
| 
						 | 
					e0ff198c3f | ||
| 
						 | 
					8b86e1c229 | ||
| 
						 | 
					24be28a662 | ||
| 
						 | 
					c6788b4917 | ||
| 
						 | 
					4a52bf02e6 | ||
| 
						 | 
					08b627cb8c | ||
| 
						 | 
					73f542adad | ||
| 
						 | 
					0d11b0c55a | ||
| 
						 | 
					5b9d2bd1e3 | ||
| 
						 | 
					cf1e141837 | ||
| 
						 | 
					3894a22174 | ||
| 
						 | 
					cad155e655 | ||
| 
						 | 
					4477b1ca1f | ||
| 
						 | 
					cd443466c7 | ||
| 
						 | 
					18012ddab1 | ||
| 
						 | 
					beb015194f | ||
| 
						 | 
					c3904d9fdd | ||
| 
						 | 
					aee5779983 | ||
| 
						 | 
					765a1c4354 | ||
| 
						 | 
					86840a8822 | ||
| 
						 | 
					487fb2d5c1 | ||
| 
						 | 
					54d5e96a6d | ||
| 
						 | 
					481ad3c844 | ||
| 
						 | 
					0ce85cf76b | ||
| 
						 | 
					5849e4586c | ||
| 
						 | 
					34c0a7163f | ||
| 
						 | 
					89e3ceab00 | ||
| 
						 | 
					5a5e64abef | ||
| 
						 | 
					2368e0d263 | ||
| 
						 | 
					547149da44 | ||
| 
						 | 
					a1144bbf76 | ||
| 
						 | 
					6b8d637b75 | ||
| 
						 | 
					fd47eb7f4b | ||
| 
						 | 
					7db86dcf8c | ||
| 
						 | 
					e75323c16e | ||
| 
						 | 
					97d01d7201 | ||
| 
						 | 
					90b42a4bb7 | ||
| 
						 | 
					fbf2fe919c | ||
| 
						 | 
					d3ddf7c51e | ||
| 
						 | 
					5759241573 | ||
| 
						 | 
					3747abbc3b | ||
| 
						 | 
					226e6cf1ce | ||
| 
						 | 
					1778abb683 | ||
| 
						 | 
					90a20dd1b0 | ||
| 
						 | 
					21c9129e14 | ||
| 
						 | 
					19d2d0bddd | ||
| 
						 | 
					83c43332d4 | ||
| 
						 | 
					3a1b8d1db1 | ||
| 
						 | 
					46426e27b4 | ||
| 
						 | 
					9402822fa3 | ||
| 
						 | 
					651fe361fc | ||
| 
						 | 
					aee1713aec | ||
| 
						 | 
					ecfa14829b | ||
| 
						 | 
					92e3ec785c | ||
| 
						 | 
					8102f19f79 | ||
| 
						 | 
					840a00ebcd | ||
| 
						 | 
					181f96d250 | ||
| 
						 | 
					ce2389f135 | ||
| 
						 | 
					f15dc77f33 | ||
| 
						 | 
					caa81c24a6 | ||
| 
						 | 
					e3cb62788c | ||
| 
						 | 
					fc46cbb893 | ||
| 
						 | 
					d94a71e9e1 | ||
| 
						 | 
					cf81253535 | ||
| 
						 | 
					25c4b42538 | ||
| 
						 | 
					f7a6e0cb5e | ||
| 
						 | 
					85275e5dd6 | ||
| 
						 | 
					4e5b467ce5 | ||
| 
						 | 
					874aa5ab75 | ||
| 
						 | 
					0940d72d5d | ||
| 
						 | 
					0d1739d70f | ||
| 
						 | 
					60fe0843ac | ||
| 
						 | 
					794f226209 | ||
| 
						 | 
					2fbf3476b8 | ||
| 
						 | 
					897ac60a82 | ||
| 
						 | 
					18f228ca1d | ||
| 
						 | 
					14de30b7aa | ||
| 
						 | 
					3faa39e787 | ||
| 
						 | 
					63b5ba0bb9 | ||
| 
						 | 
					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 | 
							
								
								
									
										3
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -25,3 +25,6 @@ jobs:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      - name: Build
 | 
					      - name: Build
 | 
				
			||||||
        run: npm run build
 | 
					        run: npm run build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      - name: Run tests
 | 
				
			||||||
 | 
					        run: npm run test:ci
 | 
				
			||||||
							
								
								
									
										33
									
								
								.github/workflows/cla.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					name: "CLA Assistant"
 | 
				
			||||||
 | 
					on:
 | 
				
			||||||
 | 
					  issue_comment:
 | 
				
			||||||
 | 
					    types: [created]
 | 
				
			||||||
 | 
					  pull_request_target:
 | 
				
			||||||
 | 
					    types: [opened,closed,synchronize]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					permissions:
 | 
				
			||||||
 | 
					  actions: write
 | 
				
			||||||
 | 
					  contents: write # this can be 'read' if the signatures are in remote repository
 | 
				
			||||||
 | 
					  pull-requests: write
 | 
				
			||||||
 | 
					  statuses: write
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jobs:
 | 
				
			||||||
 | 
					  CLAAssistant:
 | 
				
			||||||
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					    steps:
 | 
				
			||||||
 | 
					      - name: "CLA Assistant"
 | 
				
			||||||
 | 
					        if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
 | 
				
			||||||
 | 
					        # Beta Release
 | 
				
			||||||
 | 
					        uses: contributor-assistant/github-action@v2.6.1
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
				
			||||||
 | 
					          PERSONAL_ACCESS_TOKEN: ${{ secrets.CHARTDB_CLA_SIGNATURES_PAT }}
 | 
				
			||||||
 | 
					        with:
 | 
				
			||||||
 | 
					          remote-organization-name: 'chartdb'
 | 
				
			||||||
 | 
					          remote-repository-name: 'cla-signatures'
 | 
				
			||||||
 | 
					          path-to-signatures: 'signatures/version1/cla.json'
 | 
				
			||||||
 | 
					          path-to-document: 'https://github.com/chartdb/chartdb/blob/main/CLA.md'
 | 
				
			||||||
 | 
					          # branch should not be protected
 | 
				
			||||||
 | 
					          branch: 'main'
 | 
				
			||||||
 | 
					          allowlist: 
 | 
				
			||||||
							
								
								
									
										9
									
								
								.github/workflows/publish.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -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 }}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										205
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						@@ -1,5 +1,210 @@
 | 
				
			|||||||
# Changelog
 | 
					# Changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [1.13.2](https://github.com/chartdb/chartdb/compare/v1.13.1...v1.13.2) (2025-07-06)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Bug Fixes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* add DISABLE_ANALYTICS flag to opt-out of Fathom analytics ([#750](https://github.com/chartdb/chartdb/issues/750)) ([aa0b629](https://github.com/chartdb/chartdb/commit/aa0b629a3eaf8e8b60473ea3f28f769270c7714c))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [1.13.1](https://github.com/chartdb/chartdb/compare/v1.13.0...v1.13.1) (2025-07-04)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Bug Fixes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* **custom_types:** fix display custom types in select box ([#737](https://github.com/chartdb/chartdb/issues/737)) ([24be28a](https://github.com/chartdb/chartdb/commit/24be28a662c48fc5bc62e76446b9669d83d7d3e0))
 | 
				
			||||||
 | 
					* **dbml-editor:** for some cases that the dbml had issues ([#739](https://github.com/chartdb/chartdb/issues/739)) ([e0ff198](https://github.com/chartdb/chartdb/commit/e0ff198c3fd416498dac5680bb323ec88c54b65c))
 | 
				
			||||||
 | 
					* **dbml:** Filter duplicate tables at diagram level before export dbml ([#746](https://github.com/chartdb/chartdb/issues/746)) ([d429128](https://github.com/chartdb/chartdb/commit/d429128e65aa28c500eac2487356e4869506e948))
 | 
				
			||||||
 | 
					* **export-sql:** conditionally show generic option and reorder by diagram type ([#708](https://github.com/chartdb/chartdb/issues/708)) ([c6118e0](https://github.com/chartdb/chartdb/commit/c6118e0cdb0e5caaf73447d33db2fde1a98efe60))
 | 
				
			||||||
 | 
					* general performance improvements on canvas ([#751](https://github.com/chartdb/chartdb/issues/751)) ([4fcc49d](https://github.com/chartdb/chartdb/commit/4fcc49d49a76a4b886ffd6cf0b40cf2fc49952ec))
 | 
				
			||||||
 | 
					* **import-database:** for custom types query to import supabase & timescale ([#745](https://github.com/chartdb/chartdb/issues/745)) ([2fce832](https://github.com/chartdb/chartdb/commit/2fce8326b67b751d38dd34f409fea574449d0298))
 | 
				
			||||||
 | 
					* **import-db:** fix mariadb import ([#740](https://github.com/chartdb/chartdb/issues/740)) ([7d063b9](https://github.com/chartdb/chartdb/commit/7d063b905f19f51501468bd0bd794a25cf65e1be))
 | 
				
			||||||
 | 
					* **performance:** improve storage provider performance ([#734](https://github.com/chartdb/chartdb/issues/734)) ([c6788b4](https://github.com/chartdb/chartdb/commit/c6788b49173d9cce23571daeb460285cb7cffb11))
 | 
				
			||||||
 | 
					* resolve unresponsive cursor and input glitches when editing field comments ([#749](https://github.com/chartdb/chartdb/issues/749)) ([d15985e](https://github.com/chartdb/chartdb/commit/d15985e3999a0cd54213b2fb08c55d48a1b8b3b2))
 | 
				
			||||||
 | 
					* **table name:** updates table name value when its updated from canvas/sidebar ([#716](https://github.com/chartdb/chartdb/issues/716)) ([8b86e1c](https://github.com/chartdb/chartdb/commit/8b86e1c22992aaadcce7ad5fc1d267c5a57a99f0))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [1.13.0](https://github.com/chartdb/chartdb/compare/v1.12.0...v1.13.0) (2025-05-28)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* **custom-types:** add enums and composite types for Postgres ([#714](https://github.com/chartdb/chartdb/issues/714)) ([c3904d9](https://github.com/chartdb/chartdb/commit/c3904d9fdd63ef5b76a44e73582d592f2c418687))
 | 
				
			||||||
 | 
					* **export-sql:** add custom types to export sql script ([#720](https://github.com/chartdb/chartdb/issues/720)) ([cad155e](https://github.com/chartdb/chartdb/commit/cad155e6550f171b8faecbfdff27032798ecea43))
 | 
				
			||||||
 | 
					* **oracle:** support oracle in ChartDB ([#709](https://github.com/chartdb/chartdb/issues/709)) ([765a1c4](https://github.com/chartdb/chartdb/commit/765a1c43547a29bd3428c942c7afb56f63aaf046))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Bug Fixes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* **canvas:** prevent canvas blink and lag on field edit ([#723](https://github.com/chartdb/chartdb/issues/723)) ([cd44346](https://github.com/chartdb/chartdb/commit/cd443466c7952f1cdc3739645c12130b9231e3a1))
 | 
				
			||||||
 | 
					* **canvas:** prevent canvas blink and lag on primary field edit ([#725](https://github.com/chartdb/chartdb/issues/725)) ([4477b1c](https://github.com/chartdb/chartdb/commit/4477b1ca1fe6b282b604739a23e31181acd4d7bc))
 | 
				
			||||||
 | 
					* **custom_types:** fix custom types on storage provider ([#721](https://github.com/chartdb/chartdb/issues/721)) ([beb0151](https://github.com/chartdb/chartdb/commit/beb015194f917c0ba644458410162d2b7599918c))
 | 
				
			||||||
 | 
					* **custom_types:** fix custom types on storage provider ([#722](https://github.com/chartdb/chartdb/issues/722)) ([18012dd](https://github.com/chartdb/chartdb/commit/18012ddab1718bcce3432aea626adf6fc9be25d9))
 | 
				
			||||||
 | 
					* **custom-types:** fetch directly via the smart-query the custom types ([#729](https://github.com/chartdb/chartdb/issues/729)) ([cf1e141](https://github.com/chartdb/chartdb/commit/cf1e141837eda77d717ad87489ce9946b688e226))
 | 
				
			||||||
 | 
					* **dbml-editor:** export comments with schema if existsed ([#728](https://github.com/chartdb/chartdb/issues/728)) ([73f542a](https://github.com/chartdb/chartdb/commit/73f542adad2d66a1e84fc656a0c34d9b1f39f33c))
 | 
				
			||||||
 | 
					* **dbml-editor:** fix export dbml - to show enums ([#724](https://github.com/chartdb/chartdb/issues/724)) ([3894a22](https://github.com/chartdb/chartdb/commit/3894a221745d32c13160bedcb1bcf53d89897698))
 | 
				
			||||||
 | 
					* **import-database:** remove the default fetch from import database ([#718](https://github.com/chartdb/chartdb/issues/718)) ([0d11b0c](https://github.com/chartdb/chartdb/commit/0d11b0c55a94a12a764785cfdcf2ba10437241d6))
 | 
				
			||||||
 | 
					* **menu:** add oracle to import menu ([#713](https://github.com/chartdb/chartdb/issues/713)) ([aee5779](https://github.com/chartdb/chartdb/commit/aee577998342eb4a2b05b3e03181992a435712d8))
 | 
				
			||||||
 | 
					* **relationship:** fix creating of relationships ([#732](https://github.com/chartdb/chartdb/issues/732)) ([08b627c](https://github.com/chartdb/chartdb/commit/08b627cb8ca8fdf08d8ed2ff7e89104887deffb7))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [1.12.0](https://github.com/chartdb/chartdb/compare/v1.11.0...v1.12.0) (2025-05-20)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* **areas:** implement area to enable logical diagram arrangement ([#661](https://github.com/chartdb/chartdb/issues/661)) ([92e3ec7](https://github.com/chartdb/chartdb/commit/92e3ec785c91f7f19881c6d9d0692257af4651bc))
 | 
				
			||||||
 | 
					* **examples:** update examples to have areas ([#677](https://github.com/chartdb/chartdb/issues/677)) ([21c9129](https://github.com/chartdb/chartdb/commit/21c9129e14670c744950cd43a5cbdd4b7d47c639))
 | 
				
			||||||
 | 
					* **image-export:** add transparent and pattern export image toggles ([#671](https://github.com/chartdb/chartdb/issues/671)) ([6b8d637](https://github.com/chartdb/chartdb/commit/6b8d637b757b94630ecd7521b4a2c99634afae69))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Bug Fixes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* add sorting based on how common the datatype on side-panel ([#651](https://github.com/chartdb/chartdb/issues/651)) ([3a1b8d1](https://github.com/chartdb/chartdb/commit/3a1b8d1db13d8dd7cb6cbe5ef8c5a60faccfeae5))
 | 
				
			||||||
 | 
					* **canvas:** disable edit area name on read only ([#666](https://github.com/chartdb/chartdb/issues/666)) ([9402822](https://github.com/chartdb/chartdb/commit/9402822fa31f8cd94fe7971277839ee5425e29bf))
 | 
				
			||||||
 | 
					* **canvas:** read only mode ([#665](https://github.com/chartdb/chartdb/issues/665)) ([651fe36](https://github.com/chartdb/chartdb/commit/651fe361fce61fe0577d2593f268131e9ca359d0))
 | 
				
			||||||
 | 
					* **clone:** add areas to clone diagram ([#664](https://github.com/chartdb/chartdb/issues/664)) ([aee1713](https://github.com/chartdb/chartdb/commit/aee1713aecdd5e54228a16cbc3c4fc184661c56b))
 | 
				
			||||||
 | 
					* **dbml-editor:** add inline refs mode + fix issues with DBML syntax ([#687](https://github.com/chartdb/chartdb/issues/687)) ([fbf2fe9](https://github.com/chartdb/chartdb/commit/fbf2fe919c2168c715f8231c0246753b19635f14))
 | 
				
			||||||
 | 
					* **dbml-editor:** remove invalid fields before showing DBML + warning ([#683](https://github.com/chartdb/chartdb/issues/683)) ([5759241](https://github.com/chartdb/chartdb/commit/5759241573db204183c92599588d59f4aadaeafb))
 | 
				
			||||||
 | 
					* **ddl-import:** fix datatypes when importing via ddl ([#696](https://github.com/chartdb/chartdb/issues/696)) ([a1144bb](https://github.com/chartdb/chartdb/commit/a1144bbf761a0daedd546b5d9b92300be59e0157))
 | 
				
			||||||
 | 
					* **ddl:** inline fks ddl script ([#701](https://github.com/chartdb/chartdb/issues/701)) ([5849e45](https://github.com/chartdb/chartdb/commit/5849e4586c7c2a7cd86bd064df8916b130fc6234))
 | 
				
			||||||
 | 
					* **dependencies:** hide icon when diagram has no dependencies ([#684](https://github.com/chartdb/chartdb/issues/684)) ([547149d](https://github.com/chartdb/chartdb/commit/547149da44db6d3d1e36d619d475fe52ff83a472))
 | 
				
			||||||
 | 
					* **examples:** add loader ([#678](https://github.com/chartdb/chartdb/issues/678)) ([90a20dd](https://github.com/chartdb/chartdb/commit/90a20dd1b0277c4aee848fae5ed7a8347c5ba77d))
 | 
				
			||||||
 | 
					* **examples:** fix clone examples ([#679](https://github.com/chartdb/chartdb/issues/679)) ([1778abb](https://github.com/chartdb/chartdb/commit/1778abb683d575af244edcd9a11f8d03f903f719))
 | 
				
			||||||
 | 
					* **expanded-table:** persist expanded state across renders ([#707](https://github.com/chartdb/chartdb/issues/707)) ([54d5e96](https://github.com/chartdb/chartdb/commit/54d5e96a6db1e3abd52229a89ac503ff31885386))
 | 
				
			||||||
 | 
					* **export image:** Fix usage of advanced options accordion ([#703](https://github.com/chartdb/chartdb/issues/703)) ([0ce85cf](https://github.com/chartdb/chartdb/commit/0ce85cf76b733f441f661608278c0db3122c5074))
 | 
				
			||||||
 | 
					* **import-database:** auto detect when user try to import ddl script ([#698](https://github.com/chartdb/chartdb/issues/698)) ([5a5e64a](https://github.com/chartdb/chartdb/commit/5a5e64abef510cff28b3d8972520d0b9df29b024))
 | 
				
			||||||
 | 
					* **import-database:** remove view_definition when importing via query ([#702](https://github.com/chartdb/chartdb/issues/702)) ([481ad3c](https://github.com/chartdb/chartdb/commit/481ad3c8449f469bf2b4418e4cdcc5b5608dfd36))
 | 
				
			||||||
 | 
					* **import-json:** for broken json imports ([#697](https://github.com/chartdb/chartdb/issues/697)) ([2368e0d](https://github.com/chartdb/chartdb/commit/2368e0d2639021c4a11a8e5131d6af44fb6a47db))
 | 
				
			||||||
 | 
					* **import-json:** simplify import script for fixing invalid JSON ([#681](https://github.com/chartdb/chartdb/issues/681)) ([226e6cf](https://github.com/chartdb/chartdb/commit/226e6cf1ce4d2edcfbee6a4de7ab0bc0cfeb17fe))
 | 
				
			||||||
 | 
					* **import:** dbml and query - senetize before import ([#699](https://github.com/chartdb/chartdb/issues/699)) ([34c0a71](https://github.com/chartdb/chartdb/commit/34c0a7163f47bde7ddfaa8f044341e3c971b7e03))
 | 
				
			||||||
 | 
					* **navbar:** open diagram directly from diagram icon ([#694](https://github.com/chartdb/chartdb/issues/694)) ([7db86dc](https://github.com/chartdb/chartdb/commit/7db86dcf8c97d34b056e4b5b85a0dda0438322ea))
 | 
				
			||||||
 | 
					* **performance:** Only render visible ([#672](https://github.com/chartdb/chartdb/issues/672)) ([83c4333](https://github.com/chartdb/chartdb/commit/83c43332d497e9fc148a18b9cb4d9ecc85e44183))
 | 
				
			||||||
 | 
					* **performance:** update field only when changed ([#685](https://github.com/chartdb/chartdb/issues/685)) ([d3ddf7c](https://github.com/chartdb/chartdb/commit/d3ddf7c51eaa4b9cddb961defd52d423f39f281d))
 | 
				
			||||||
 | 
					* **postgres:** fix import of postgres fks ([#700](https://github.com/chartdb/chartdb/issues/700)) ([89e3cea](https://github.com/chartdb/chartdb/commit/89e3ceab00defaabc079e165fc90e92ca00722cf))
 | 
				
			||||||
 | 
					* **schema:** add areas to diagram schema ([#663](https://github.com/chartdb/chartdb/issues/663)) ([ecfa148](https://github.com/chartdb/chartdb/commit/ecfa14829bcb1b813c7b154b4bd59f24e3032d8f))
 | 
				
			||||||
 | 
					* **sql-script:** change ddl to be sql-script ([#710](https://github.com/chartdb/chartdb/issues/710)) ([487fb2d](https://github.com/chartdb/chartdb/commit/487fb2d5c17b70ac54aa17af9a2ac9aded6b40ba))
 | 
				
			||||||
 | 
					* **table:** enhance field focus behavior to include table hover state ([#676](https://github.com/chartdb/chartdb/issues/676)) ([19d2d0b](https://github.com/chartdb/chartdb/commit/19d2d0bddd3a464995b79e97e6caf6e652836081))
 | 
				
			||||||
 | 
					* **translations:** Add some translations for ru-RU language ([#690](https://github.com/chartdb/chartdb/issues/690)) ([97d01d7](https://github.com/chartdb/chartdb/commit/97d01d72014e473c42348c9ebcbe7a0b973d31aa))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [1.11.0](https://github.com/chartdb/chartdb/compare/v1.10.0...v1.11.0) (2025-04-17)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* add sidebar footer help buttons ([#650](https://github.com/chartdb/chartdb/issues/650)) ([fc46cbb](https://github.com/chartdb/chartdb/commit/fc46cbb8933761c7bac3604664f7de812f6f5b6b))
 | 
				
			||||||
 | 
					* **import-sql:** import postgresql via SQL (DDL script) ([#639](https://github.com/chartdb/chartdb/issues/639)) ([f7a6e0c](https://github.com/chartdb/chartdb/commit/f7a6e0cb5e4921dd9540739f9da269858e7ca7be))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Bug Fixes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* **import:** display query result formatted ([#644](https://github.com/chartdb/chartdb/issues/644)) ([caa81c2](https://github.com/chartdb/chartdb/commit/caa81c24a6535bc87129c38622aac5a62a6d479d))
 | 
				
			||||||
 | 
					* **import:** strict parse of database metadata ([#635](https://github.com/chartdb/chartdb/issues/635)) ([0940d72](https://github.com/chartdb/chartdb/commit/0940d72d5d3726650213257639f24ba47e729854))
 | 
				
			||||||
 | 
					* **mobile:** fix create diagram modal on mobile ([#646](https://github.com/chartdb/chartdb/issues/646)) ([25c4b42](https://github.com/chartdb/chartdb/commit/25c4b4253849575d7a781ed197281e2a35e7184a))
 | 
				
			||||||
 | 
					* **mysql-ddl:** update the script to import - for create fks ([#642](https://github.com/chartdb/chartdb/issues/642)) ([cf81253](https://github.com/chartdb/chartdb/commit/cf81253535ca5a3b8a65add78287c1bdb283a1c7))
 | 
				
			||||||
 | 
					* **performance:** Import deps dynamically ([#652](https://github.com/chartdb/chartdb/issues/652)) ([e3cb627](https://github.com/chartdb/chartdb/commit/e3cb62788c13f149e35e1a5020191bd43d14b52f))
 | 
				
			||||||
 | 
					* remove unused links from help menu ([#623](https://github.com/chartdb/chartdb/issues/623)) ([85275e5](https://github.com/chartdb/chartdb/commit/85275e5dd6e7845f06f682eeceda7932fc87e875))
 | 
				
			||||||
 | 
					* **sidebar:** turn sidebar to responsive for mobile ([#658](https://github.com/chartdb/chartdb/issues/658)) ([ce2389f](https://github.com/chartdb/chartdb/commit/ce2389f135d399d82c9848335d31174bac8a3791))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [1.10.0](https://github.com/chartdb/chartdb/compare/v1.9.0...v1.10.0) (2025-03-25)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Features
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* **cloudflare-d1:** add support to cloudflare-d1 + wrangler cli ([#632](https://github.com/chartdb/chartdb/issues/632)) ([794f226](https://github.com/chartdb/chartdb/commit/794f2262092fbe36e27e92220221ed98cb51ae37))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Bug Fixes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* **dbml-editor:** dealing with dbml editor for non-generic db-type ([#624](https://github.com/chartdb/chartdb/issues/624)) ([14de30b](https://github.com/chartdb/chartdb/commit/14de30b7aaa0ccaca8372f0213b692266d53f0de))
 | 
				
			||||||
 | 
					* **export-sql:** move from AI sql-export for MySQL&MariaDB to deterministic script ([#628](https://github.com/chartdb/chartdb/issues/628)) ([2fbf347](https://github.com/chartdb/chartdb/commit/2fbf3476b87f1177af17de8242a74d195dae5f35))
 | 
				
			||||||
 | 
					* **export-sql:** move from AI sql-export for postgres to deterministic script ([#626](https://github.com/chartdb/chartdb/issues/626)) ([18f228c](https://github.com/chartdb/chartdb/commit/18f228ca1d5a6c6056cb7c3bfc24d04ec470edf1))
 | 
				
			||||||
 | 
					* **export-sql:** move from AI sql-export for sqlite to deterministic script ([#627](https://github.com/chartdb/chartdb/issues/627)) ([897ac60](https://github.com/chartdb/chartdb/commit/897ac60a829a00e9453d670cceeb2282e9e93f1c))
 | 
				
			||||||
 | 
					* **sidebar:** add sidebar for diagram objects ([#618](https://github.com/chartdb/chartdb/issues/618)) ([63b5ba0](https://github.com/chartdb/chartdb/commit/63b5ba0bb9934c4e5c5d0d1b6f995afbbd3acf36))
 | 
				
			||||||
 | 
					* **sidebar:** opens sidepanel in case its closed and click on sidebar ([#620](https://github.com/chartdb/chartdb/issues/620)) ([3faa39e](https://github.com/chartdb/chartdb/commit/3faa39e7875d836dfe526d94a10f8aed070ac1c1))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## [1.9.0](https://github.com/chartdb/chartdb/compare/v1.8.1...v1.9.0) (2025-03-13)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 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)
 | 
					## [1.6.1](https://github.com/chartdb/chartdb/compare/v1.6.0...v1.6.1) (2025-01-26)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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,10 @@
 | 
				
			|||||||
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_CHARTDB_CLOUD
 | 
				
			||||||
 | 
					ARG VITE_DISABLE_ANALYTICS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /usr/src/app
 | 
					WORKDIR /usr/src/app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -10,9 +14,14 @@ 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_CHARTDB_CLOUD=${VITE_HIDE_CHARTDB_CLOUD}" >> .env && \
 | 
				
			||||||
 | 
					    echo "VITE_DISABLE_ANALYTICS=${VITE_DISABLE_ANALYTICS}" >> .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 +29,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"]
 | 
				
			||||||
							
								
								
									
										41
									
								
								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>
 | 
				
			||||||
@@ -53,7 +53,7 @@ Instantly visualize your database schema with a single **"Smart Query."** Custom
 | 
				
			|||||||
  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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -67,7 +67,7 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif
 | 
				
			|||||||
- ✅ 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -91,24 +91,53 @@ 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
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> **Privacy Note:** ChartDB includes privacy-focused analytics via Fathom Analytics. You can disable this by adding `-e DISABLE_ANALYTICS=true` to the run command or `--build-arg VITE_DISABLE_ANALYTICS=true` when building.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> **Note:** You must configure either Option 1 (OpenAI API key) OR Option 2 (Custom endpoint and model name) for AI capabilities to work. Do not mix the two options.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Open your browser and navigate to `http://localhost:8080`.
 | 
					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)
 | 
				
			||||||
@@ -122,7 +151,7 @@ Open your browser and navigate to `http://localhost:8080`.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
- [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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,9 @@
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    "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,13 @@ 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_CHARTDB_CLOUD: \"$HIDE_CHARTDB_CLOUD\",
 | 
				
			||||||
 | 
					            DISABLE_ANALYTICS: \"$DISABLE_ANALYTICS\"
 | 
				
			||||||
 | 
					        };";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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_CHARTDB_CLOUD} ${DISABLE_ANALYTICS}' < /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;"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										20
									
								
								index.html
									
									
									
									
									
								
							
							
						
						@@ -13,11 +13,21 @@
 | 
				
			|||||||
            rel="stylesheet"
 | 
					            rel="stylesheet"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
        <script src="/config.js"></script>
 | 
					        <script src="/config.js"></script>
 | 
				
			||||||
        <script
 | 
					        <script>
 | 
				
			||||||
            src="https://cdn.usefathom.com/script.js"
 | 
					            // Load analytics only if not disabled
 | 
				
			||||||
            data-site="PRHIVBNN"
 | 
					            (function() {
 | 
				
			||||||
            defer
 | 
					                const disableAnalytics = (window.env && window.env.DISABLE_ANALYTICS === 'true') ||
 | 
				
			||||||
        ></script>
 | 
					                                        (typeof process !== 'undefined' && process.env && process.env.VITE_DISABLE_ANALYTICS === 'true');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (!disableAnalytics) {
 | 
				
			||||||
 | 
					                    const script = document.createElement('script');
 | 
				
			||||||
 | 
					                    script.src = 'https://cdn.usefathom.com/script.js';
 | 
				
			||||||
 | 
					                    script.setAttribute('data-site', 'PRHIVBNN');
 | 
				
			||||||
 | 
					                    script.defer = true;
 | 
				
			||||||
 | 
					                    document.head.appendChild(script);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            })();
 | 
				
			||||||
 | 
					        </script>
 | 
				
			||||||
    </head>
 | 
					    </head>
 | 
				
			||||||
    <body>
 | 
					    <body>
 | 
				
			||||||
        <div id="root"></div>
 | 
					        <div id="root"></div>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5992
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										32
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -1,7 +1,7 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    "name": "chartdb",
 | 
					    "name": "chartdb",
 | 
				
			||||||
    "private": true,
 | 
					    "private": true,
 | 
				
			||||||
    "version": "1.6.1",
 | 
					    "version": "1.13.2",
 | 
				
			||||||
    "type": "module",
 | 
					    "type": "module",
 | 
				
			||||||
    "scripts": {
 | 
					    "scripts": {
 | 
				
			||||||
        "dev": "vite",
 | 
					        "dev": "vite",
 | 
				
			||||||
@@ -9,10 +9,15 @@
 | 
				
			|||||||
        "lint": "eslint . --report-unused-disable-directives --max-warnings 0",
 | 
					        "lint": "eslint . --report-unused-disable-directives --max-warnings 0",
 | 
				
			||||||
        "lint:fix": "npm run lint -- --fix",
 | 
					        "lint:fix": "npm run lint -- --fix",
 | 
				
			||||||
        "preview": "vite preview",
 | 
					        "preview": "vite preview",
 | 
				
			||||||
        "prepare": "husky"
 | 
					        "prepare": "husky",
 | 
				
			||||||
 | 
					        "test": "vitest",
 | 
				
			||||||
 | 
					        "test:ci": "vitest run --reporter=verbose --bail=1",
 | 
				
			||||||
 | 
					        "test:ui": "vitest --ui",
 | 
				
			||||||
 | 
					        "test:coverage": "vitest --coverage"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "dependencies": {
 | 
					    "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 +26,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.1",
 | 
					        "@radix-ui/react-slot": "^1.2.3",
 | 
				
			||||||
        "@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",
 | 
				
			||||||
@@ -49,8 +54,9 @@
 | 
				
			|||||||
        "html-to-image": "^1.11.11",
 | 
					        "html-to-image": "^1.11.11",
 | 
				
			||||||
        "i18next": "^23.14.0",
 | 
					        "i18next": "^23.14.0",
 | 
				
			||||||
        "i18next-browser-languagedetector": "^8.0.0",
 | 
					        "i18next-browser-languagedetector": "^8.0.0",
 | 
				
			||||||
        "lucide-react": "^0.441.0",
 | 
					        "lucide-react": "^0.525.0",
 | 
				
			||||||
        "monaco-editor": "^0.52.0",
 | 
					        "monaco-editor": "^0.52.0",
 | 
				
			||||||
 | 
					        "motion": "^12.23.6",
 | 
				
			||||||
        "nanoid": "^5.0.7",
 | 
					        "nanoid": "^5.0.7",
 | 
				
			||||||
        "node-sql-parser": "^5.3.2",
 | 
					        "node-sql-parser": "^5.3.2",
 | 
				
			||||||
        "react": "^18.3.1",
 | 
					        "react": "^18.3.1",
 | 
				
			||||||
@@ -72,12 +78,16 @@
 | 
				
			|||||||
        "@eslint/compat": "^1.2.4",
 | 
					        "@eslint/compat": "^1.2.4",
 | 
				
			||||||
        "@eslint/eslintrc": "^3.2.0",
 | 
					        "@eslint/eslintrc": "^3.2.0",
 | 
				
			||||||
        "@eslint/js": "^9.16.0",
 | 
					        "@eslint/js": "^9.16.0",
 | 
				
			||||||
 | 
					        "@testing-library/jest-dom": "^6.6.3",
 | 
				
			||||||
 | 
					        "@testing-library/react": "^16.3.0",
 | 
				
			||||||
 | 
					        "@testing-library/user-event": "^14.6.1",
 | 
				
			||||||
        "@types/node": "^22.1.0",
 | 
					        "@types/node": "^22.1.0",
 | 
				
			||||||
        "@types/react": "^18.3.3",
 | 
					        "@types/react": "^18.3.3",
 | 
				
			||||||
        "@types/react-dom": "^18.3.0",
 | 
					        "@types/react-dom": "^18.3.0",
 | 
				
			||||||
        "@typescript-eslint/eslint-plugin": "^8.18.0",
 | 
					        "@typescript-eslint/eslint-plugin": "^8.18.0",
 | 
				
			||||||
        "@typescript-eslint/parser": "^8.18.0",
 | 
					        "@typescript-eslint/parser": "^8.18.0",
 | 
				
			||||||
        "@vitejs/plugin-react": "^4.3.1",
 | 
					        "@vitejs/plugin-react": "^4.3.1",
 | 
				
			||||||
 | 
					        "@vitest/ui": "^3.2.4",
 | 
				
			||||||
        "autoprefixer": "^10.4.20",
 | 
					        "autoprefixer": "^10.4.20",
 | 
				
			||||||
        "eslint": "^9.16.0",
 | 
					        "eslint": "^9.16.0",
 | 
				
			||||||
        "eslint-config-prettier": "^9.1.0",
 | 
					        "eslint-config-prettier": "^9.1.0",
 | 
				
			||||||
@@ -89,6 +99,7 @@
 | 
				
			|||||||
        "eslint-plugin-react-refresh": "^0.4.7",
 | 
					        "eslint-plugin-react-refresh": "^0.4.7",
 | 
				
			||||||
        "eslint-plugin-tailwindcss": "^3.17.4",
 | 
					        "eslint-plugin-tailwindcss": "^3.17.4",
 | 
				
			||||||
        "globals": "^15.13.0",
 | 
					        "globals": "^15.13.0",
 | 
				
			||||||
 | 
					        "happy-dom": "^18.0.1",
 | 
				
			||||||
        "husky": "^9.1.5",
 | 
					        "husky": "^9.1.5",
 | 
				
			||||||
        "postcss": "^8.4.40",
 | 
					        "postcss": "^8.4.40",
 | 
				
			||||||
        "prettier": "^3.3.3",
 | 
					        "prettier": "^3.3.3",
 | 
				
			||||||
@@ -96,6 +107,7 @@
 | 
				
			|||||||
        "tailwindcss": "^3.4.7",
 | 
					        "tailwindcss": "^3.4.7",
 | 
				
			||||||
        "typescript": "^5.2.2",
 | 
					        "typescript": "^5.2.2",
 | 
				
			||||||
        "unplugin-inject-preload": "^3.0.0",
 | 
					        "unplugin-inject-preload": "^3.0.0",
 | 
				
			||||||
        "vite": "^5.3.4"
 | 
					        "vite": "^5.3.4",
 | 
				
			||||||
 | 
					        "vitest": "^3.2.4"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/cloudflare_d1.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 937 B  | 
							
								
								
									
										
											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,14 +128,12 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
				
			|||||||
                ) : (
 | 
					                ) : (
 | 
				
			||||||
                    <Suspense fallback={<Spinner />}>
 | 
					                    <Suspense fallback={<Spinner />}>
 | 
				
			||||||
                        {isComplete ? (
 | 
					                        {isComplete ? (
 | 
				
			||||||
 | 
					                            <div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
 | 
				
			||||||
                                <Tooltip
 | 
					                                <Tooltip
 | 
				
			||||||
                                    onOpenChange={setTooltipOpen}
 | 
					                                    onOpenChange={setTooltipOpen}
 | 
				
			||||||
                                    open={isCopied || tooltipOpen}
 | 
					                                    open={isCopied || tooltipOpen}
 | 
				
			||||||
                                >
 | 
					                                >
 | 
				
			||||||
                                <TooltipTrigger
 | 
					                                    <TooltipTrigger asChild>
 | 
				
			||||||
                                    asChild
 | 
					 | 
				
			||||||
                                    className="absolute right-1 top-1 z-10"
 | 
					 | 
				
			||||||
                                >
 | 
					 | 
				
			||||||
                                        <span>
 | 
					                                        <span>
 | 
				
			||||||
                                            <Button
 | 
					                                            <Button
 | 
				
			||||||
                                                className="h-fit p-1.5"
 | 
					                                                className="h-fit p-1.5"
 | 
				
			||||||
@@ -137,6 +156,30 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
				
			|||||||
                                        )}
 | 
					                                        )}
 | 
				
			||||||
                                    </TooltipContent>
 | 
					                                    </TooltipContent>
 | 
				
			||||||
                                </Tooltip>
 | 
					                                </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,7 +1,9 @@
 | 
				
			|||||||
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;
 | 
				
			||||||
@@ -25,7 +27,10 @@ export const EmptyState = forwardRef<
 | 
				
			|||||||
            imageClassName,
 | 
					            imageClassName,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        ref
 | 
					        ref
 | 
				
			||||||
    ) => (
 | 
					    ) => {
 | 
				
			||||||
 | 
					        const { effectiveTheme } = useTheme();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
            <div
 | 
					            <div
 | 
				
			||||||
                ref={ref}
 | 
					                ref={ref}
 | 
				
			||||||
                className={cn(
 | 
					                className={cn(
 | 
				
			||||||
@@ -34,21 +39,28 @@ export const EmptyState = forwardRef<
 | 
				
			|||||||
                )}
 | 
					                )}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
                <img
 | 
					                <img
 | 
				
			||||||
                src={EmptyStateImage}
 | 
					                    src={
 | 
				
			||||||
 | 
					                        effectiveTheme === 'dark'
 | 
				
			||||||
 | 
					                            ? EmptyStateImageDark
 | 
				
			||||||
 | 
					                            : EmptyStateImage
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
                    alt="Empty state"
 | 
					                    alt="Empty state"
 | 
				
			||||||
                    className={cn('mb-2 w-20', imageClassName)}
 | 
					                    className={cn('mb-2 w-20', imageClassName)}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
            <Label className={cn('text-base', titleClassName)}>{title}</Label>
 | 
					                <Label className={cn('text-base', titleClassName)}>
 | 
				
			||||||
 | 
					                    {title}
 | 
				
			||||||
 | 
					                </Label>
 | 
				
			||||||
                <Label
 | 
					                <Label
 | 
				
			||||||
                    className={cn(
 | 
					                    className={cn(
 | 
				
			||||||
                    'text-sm font-normal text-muted-foreground',
 | 
					                        'text-sm text-center font-normal text-muted-foreground',
 | 
				
			||||||
                        descriptionClassName
 | 
					                        descriptionClassName
 | 
				
			||||||
                    )}
 | 
					                    )}
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                    {description}
 | 
					                    {description}
 | 
				
			||||||
                </Label>
 | 
					                </Label>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
    )
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
EmptyState.displayName = 'EmptyState';
 | 
					EmptyState.displayName = 'EmptyState';
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										121
									
								
								src/components/pagination/pagination.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,121 @@
 | 
				
			|||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { cn } from '@/lib/utils';
 | 
				
			||||||
 | 
					import type { ButtonProps } from '../button/button';
 | 
				
			||||||
 | 
					import { buttonVariants } from '../button/button-variants';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    ChevronLeftIcon,
 | 
				
			||||||
 | 
					    ChevronRightIcon,
 | 
				
			||||||
 | 
					    DotsHorizontalIcon,
 | 
				
			||||||
 | 
					} from '@radix-ui/react-icons';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
 | 
				
			||||||
 | 
					    <nav
 | 
				
			||||||
 | 
					        role="navigation"
 | 
				
			||||||
 | 
					        aria-label="pagination"
 | 
				
			||||||
 | 
					        className={cn('mx-auto flex w-full justify-center', className)}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					Pagination.displayName = 'Pagination';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PaginationContent = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLUListElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'ul'>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					    <ul
 | 
				
			||||||
 | 
					        ref={ref}
 | 
				
			||||||
 | 
					        className={cn('flex flex-row items-center gap-1', className)}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					));
 | 
				
			||||||
 | 
					PaginationContent.displayName = 'PaginationContent';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PaginationItem = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLLIElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'li'>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					    <li ref={ref} className={cn('', className)} {...props} />
 | 
				
			||||||
 | 
					));
 | 
				
			||||||
 | 
					PaginationItem.displayName = 'PaginationItem';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PaginationLinkProps = {
 | 
				
			||||||
 | 
					    isActive?: boolean;
 | 
				
			||||||
 | 
					} & Pick<ButtonProps, 'size'> &
 | 
				
			||||||
 | 
					    React.ComponentProps<'a'>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PaginationLink = ({
 | 
				
			||||||
 | 
					    className,
 | 
				
			||||||
 | 
					    isActive,
 | 
				
			||||||
 | 
					    size = 'icon',
 | 
				
			||||||
 | 
					    ...props
 | 
				
			||||||
 | 
					}: PaginationLinkProps) => (
 | 
				
			||||||
 | 
					    <a
 | 
				
			||||||
 | 
					        aria-current={isActive ? 'page' : undefined}
 | 
				
			||||||
 | 
					        className={cn(
 | 
				
			||||||
 | 
					            buttonVariants({
 | 
				
			||||||
 | 
					                variant: isActive ? 'outline' : 'ghost',
 | 
				
			||||||
 | 
					                size,
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            className
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					PaginationLink.displayName = 'PaginationLink';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PaginationPrevious = ({
 | 
				
			||||||
 | 
					    className,
 | 
				
			||||||
 | 
					    ...props
 | 
				
			||||||
 | 
					}: React.ComponentProps<typeof PaginationLink>) => (
 | 
				
			||||||
 | 
					    <PaginationLink
 | 
				
			||||||
 | 
					        aria-label="Go to previous page"
 | 
				
			||||||
 | 
					        size="default"
 | 
				
			||||||
 | 
					        className={cn('gap-1 pl-2.5', className)}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					        <ChevronLeftIcon className="size-4" />
 | 
				
			||||||
 | 
					        <span>Previous</span>
 | 
				
			||||||
 | 
					    </PaginationLink>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					PaginationPrevious.displayName = 'PaginationPrevious';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PaginationNext = ({
 | 
				
			||||||
 | 
					    className,
 | 
				
			||||||
 | 
					    ...props
 | 
				
			||||||
 | 
					}: React.ComponentProps<typeof PaginationLink>) => (
 | 
				
			||||||
 | 
					    <PaginationLink
 | 
				
			||||||
 | 
					        aria-label="Go to next page"
 | 
				
			||||||
 | 
					        size="default"
 | 
				
			||||||
 | 
					        className={cn('gap-1 pr-2.5', className)}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					        <span>Next</span>
 | 
				
			||||||
 | 
					        <ChevronRightIcon className="size-4" />
 | 
				
			||||||
 | 
					    </PaginationLink>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					PaginationNext.displayName = 'PaginationNext';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const PaginationEllipsis = ({
 | 
				
			||||||
 | 
					    className,
 | 
				
			||||||
 | 
					    ...props
 | 
				
			||||||
 | 
					}: React.ComponentProps<'span'>) => (
 | 
				
			||||||
 | 
					    <span
 | 
				
			||||||
 | 
					        aria-hidden
 | 
				
			||||||
 | 
					        className={cn('flex h-9 w-9 items-center justify-center', className)}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					        <DotsHorizontalIcon className="size-4" />
 | 
				
			||||||
 | 
					        <span className="sr-only">More pages</span>
 | 
				
			||||||
 | 
					    </span>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					PaginationEllipsis.displayName = 'PaginationEllipsis';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					    Pagination,
 | 
				
			||||||
 | 
					    PaginationContent,
 | 
				
			||||||
 | 
					    PaginationLink,
 | 
				
			||||||
 | 
					    PaginationItem,
 | 
				
			||||||
 | 
					    PaginationPrevious,
 | 
				
			||||||
 | 
					    PaginationNext,
 | 
				
			||||||
 | 
					    PaginationEllipsis,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -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 (
 | 
				
			||||||
                                ? 1
 | 
					                                keywords?.length &&
 | 
				
			||||||
                                : 0
 | 
					                                keywords.some((keyword) =>
 | 
				
			||||||
 | 
					                                    new RegExp(keyword).test(search)
 | 
				
			||||||
 | 
					                                )
 | 
				
			||||||
 | 
					                            ) {
 | 
				
			||||||
 | 
					                                return 1;
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            return value
 | 
				
			||||||
 | 
					                                .toLowerCase()
 | 
				
			||||||
 | 
					                                .includes(search.toLowerCase())
 | 
				
			||||||
 | 
					                                ? 1
 | 
				
			||||||
 | 
					                                : 0;
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
                    >
 | 
					                    >
 | 
				
			||||||
                        <div className="relative">
 | 
					                        <div className="relative">
 | 
				
			||||||
                            <CommandInput
 | 
					                            <CommandInput
 | 
				
			||||||
@@ -296,65 +418,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                        <ScrollArea>
 | 
					                        <ScrollArea>
 | 
				
			||||||
                            <div className="max-h-64 w-full">
 | 
					                            <div className="max-h-64 w-full">
 | 
				
			||||||
                                <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) &&
 | 
					                                              ([groupName, groupOptions]) => (
 | 
				
			||||||
                                                value.includes(option.value);
 | 
					                                                  <CommandGroup
 | 
				
			||||||
                                            return (
 | 
					                                                      key={groupName}
 | 
				
			||||||
                                                <CommandItem
 | 
					                                                      heading={groupName}
 | 
				
			||||||
                                                    className="flex items-center"
 | 
					 | 
				
			||||||
                                                    key={option.value}
 | 
					 | 
				
			||||||
                                                    // value={option.value}
 | 
					 | 
				
			||||||
                                                    onSelect={() =>
 | 
					 | 
				
			||||||
                                                        handleSelect(
 | 
					 | 
				
			||||||
                                                            option.value
 | 
					 | 
				
			||||||
                                                        )
 | 
					 | 
				
			||||||
                                                    }
 | 
					 | 
				
			||||||
                                                  >
 | 
					                                                  >
 | 
				
			||||||
                                                    {multiple && (
 | 
					                                                      {groupOptions.map(
 | 
				
			||||||
                                                        <div
 | 
					                                                          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>
 | 
					 | 
				
			||||||
                                                  </CommandGroup>
 | 
					                                                  </CommandGroup>
 | 
				
			||||||
 | 
					                                              )
 | 
				
			||||||
 | 
					                                          )
 | 
				
			||||||
 | 
					                                        : options.map(renderOption)}
 | 
				
			||||||
 | 
					                                </CommandList>
 | 
				
			||||||
                            </div>
 | 
					                            </div>
 | 
				
			||||||
                        </ScrollArea>
 | 
					                        </ScrollArea>
 | 
				
			||||||
                    </Command>
 | 
					                    </Command>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										135
									
								
								src/components/sheet/sheet.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,135 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import * as SheetPrimitive from '@radix-ui/react-dialog';
 | 
				
			||||||
 | 
					import { cva, type VariantProps } from 'class-variance-authority';
 | 
				
			||||||
 | 
					import { cn } from '@/lib/utils';
 | 
				
			||||||
 | 
					import { Cross2Icon } from '@radix-ui/react-icons';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Sheet = SheetPrimitive.Root;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetTrigger = SheetPrimitive.Trigger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetClose = SheetPrimitive.Close;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetPortal = SheetPrimitive.Portal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetOverlay = React.forwardRef<
 | 
				
			||||||
 | 
					    React.ElementRef<typeof SheetPrimitive.Overlay>,
 | 
				
			||||||
 | 
					    React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					    <SheetPrimitive.Overlay
 | 
				
			||||||
 | 
					        className={cn(
 | 
				
			||||||
 | 
					            'fixed inset-0 z-50 bg-black/80  data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
 | 
				
			||||||
 | 
					            className
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					        ref={ref}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					));
 | 
				
			||||||
 | 
					SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const sheetVariants = cva(
 | 
				
			||||||
 | 
					    'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        variants: {
 | 
				
			||||||
 | 
					            side: {
 | 
				
			||||||
 | 
					                top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
 | 
				
			||||||
 | 
					                bottom: 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
 | 
				
			||||||
 | 
					                left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
 | 
				
			||||||
 | 
					                right: 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        defaultVariants: {
 | 
				
			||||||
 | 
					            side: 'right',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SheetContentProps
 | 
				
			||||||
 | 
					    extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
 | 
				
			||||||
 | 
					        VariantProps<typeof sheetVariants> {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetContent = React.forwardRef<
 | 
				
			||||||
 | 
					    React.ElementRef<typeof SheetPrimitive.Content>,
 | 
				
			||||||
 | 
					    SheetContentProps
 | 
				
			||||||
 | 
					>(({ side = 'right', className, children, ...props }, ref) => (
 | 
				
			||||||
 | 
					    <SheetPortal>
 | 
				
			||||||
 | 
					        <SheetOverlay />
 | 
				
			||||||
 | 
					        <SheetPrimitive.Content
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            className={cn(sheetVariants({ side }), className)}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
 | 
				
			||||||
 | 
					                <Cross2Icon className="size-4" />
 | 
				
			||||||
 | 
					                <span className="sr-only">Close</span>
 | 
				
			||||||
 | 
					            </SheetPrimitive.Close>
 | 
				
			||||||
 | 
					            {children}
 | 
				
			||||||
 | 
					        </SheetPrimitive.Content>
 | 
				
			||||||
 | 
					    </SheetPortal>
 | 
				
			||||||
 | 
					));
 | 
				
			||||||
 | 
					SheetContent.displayName = SheetPrimitive.Content.displayName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetHeader = ({
 | 
				
			||||||
 | 
					    className,
 | 
				
			||||||
 | 
					    ...props
 | 
				
			||||||
 | 
					}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					        className={cn(
 | 
				
			||||||
 | 
					            'flex flex-col space-y-2 text-center sm:text-left',
 | 
				
			||||||
 | 
					            className
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					SheetHeader.displayName = 'SheetHeader';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetFooter = ({
 | 
				
			||||||
 | 
					    className,
 | 
				
			||||||
 | 
					    ...props
 | 
				
			||||||
 | 
					}: React.HTMLAttributes<HTMLDivElement>) => (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					        className={cn(
 | 
				
			||||||
 | 
					            'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
 | 
				
			||||||
 | 
					            className
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					SheetFooter.displayName = 'SheetFooter';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetTitle = React.forwardRef<
 | 
				
			||||||
 | 
					    React.ElementRef<typeof SheetPrimitive.Title>,
 | 
				
			||||||
 | 
					    React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					    <SheetPrimitive.Title
 | 
				
			||||||
 | 
					        ref={ref}
 | 
				
			||||||
 | 
					        className={cn('text-lg font-semibold text-foreground', className)}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					));
 | 
				
			||||||
 | 
					SheetTitle.displayName = SheetPrimitive.Title.displayName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SheetDescription = React.forwardRef<
 | 
				
			||||||
 | 
					    React.ElementRef<typeof SheetPrimitive.Description>,
 | 
				
			||||||
 | 
					    React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					    <SheetPrimitive.Description
 | 
				
			||||||
 | 
					        ref={ref}
 | 
				
			||||||
 | 
					        className={cn('text-sm text-muted-foreground', className)}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					));
 | 
				
			||||||
 | 
					SheetDescription.displayName = SheetPrimitive.Description.displayName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					    Sheet,
 | 
				
			||||||
 | 
					    SheetPortal,
 | 
				
			||||||
 | 
					    SheetOverlay,
 | 
				
			||||||
 | 
					    SheetTrigger,
 | 
				
			||||||
 | 
					    SheetClose,
 | 
				
			||||||
 | 
					    SheetContent,
 | 
				
			||||||
 | 
					    SheetHeader,
 | 
				
			||||||
 | 
					    SheetFooter,
 | 
				
			||||||
 | 
					    SheetTitle,
 | 
				
			||||||
 | 
					    SheetDescription,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										790
									
								
								src/components/sidebar/sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,790 @@
 | 
				
			|||||||
 | 
					import * as React from 'react';
 | 
				
			||||||
 | 
					import { Slot } from '@radix-ui/react-slot';
 | 
				
			||||||
 | 
					import type { VariantProps } from 'class-variance-authority';
 | 
				
			||||||
 | 
					import { cva } from 'class-variance-authority';
 | 
				
			||||||
 | 
					import { useIsMobile } from '@/hooks/use-mobile';
 | 
				
			||||||
 | 
					import { cn } from '@/lib/utils';
 | 
				
			||||||
 | 
					import { Button } from '@/components/button/button';
 | 
				
			||||||
 | 
					import { Input } from '@/components/input/input';
 | 
				
			||||||
 | 
					import { Separator } from '@/components/separator/separator';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    Sheet,
 | 
				
			||||||
 | 
					    SheetContent,
 | 
				
			||||||
 | 
					    SheetDescription,
 | 
				
			||||||
 | 
					    SheetHeader,
 | 
				
			||||||
 | 
					    SheetTitle,
 | 
				
			||||||
 | 
					} from '@/components/sheet/sheet';
 | 
				
			||||||
 | 
					import { Skeleton } from '@/components/skeleton/skeleton';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    Tooltip,
 | 
				
			||||||
 | 
					    TooltipContent,
 | 
				
			||||||
 | 
					    TooltipProvider,
 | 
				
			||||||
 | 
					    TooltipTrigger,
 | 
				
			||||||
 | 
					} from '@/components/tooltip/tooltip';
 | 
				
			||||||
 | 
					import { ViewVerticalIcon } from '@radix-ui/react-icons';
 | 
				
			||||||
 | 
					import { useSidebar } from './use-sidebar';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SIDEBAR_COOKIE_NAME = 'sidebar_state';
 | 
				
			||||||
 | 
					const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
 | 
				
			||||||
 | 
					const SIDEBAR_WIDTH = '16rem';
 | 
				
			||||||
 | 
					const SIDEBAR_WIDTH_MOBILE = '18rem';
 | 
				
			||||||
 | 
					const SIDEBAR_WIDTH_ICON = '3rem';
 | 
				
			||||||
 | 
					const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type SidebarContext = {
 | 
				
			||||||
 | 
					    state: 'expanded' | 'collapsed';
 | 
				
			||||||
 | 
					    open: boolean;
 | 
				
			||||||
 | 
					    setOpen: (open: boolean) => void;
 | 
				
			||||||
 | 
					    openMobile: boolean;
 | 
				
			||||||
 | 
					    setOpenMobile: (open: boolean) => void;
 | 
				
			||||||
 | 
					    isMobile: boolean;
 | 
				
			||||||
 | 
					    toggleSidebar: () => void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarContext = React.createContext<SidebarContext | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarProvider = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLDivElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'div'> & {
 | 
				
			||||||
 | 
					        defaultOpen?: boolean;
 | 
				
			||||||
 | 
					        open?: boolean;
 | 
				
			||||||
 | 
					        onOpenChange?: (open: boolean) => void;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					>(
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            defaultOpen = true,
 | 
				
			||||||
 | 
					            open: openProp,
 | 
				
			||||||
 | 
					            onOpenChange: setOpenProp,
 | 
				
			||||||
 | 
					            className,
 | 
				
			||||||
 | 
					            style,
 | 
				
			||||||
 | 
					            children,
 | 
				
			||||||
 | 
					            ...props
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        ref
 | 
				
			||||||
 | 
					    ) => {
 | 
				
			||||||
 | 
					        const isMobile = useIsMobile();
 | 
				
			||||||
 | 
					        const [openMobile, setOpenMobile] = React.useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // This is the internal state of the sidebar.
 | 
				
			||||||
 | 
					        // We use openProp and setOpenProp for control from outside the component.
 | 
				
			||||||
 | 
					        const [_open, _setOpen] = React.useState(defaultOpen);
 | 
				
			||||||
 | 
					        const open = openProp ?? _open;
 | 
				
			||||||
 | 
					        const setOpen = React.useCallback(
 | 
				
			||||||
 | 
					            (value: boolean | ((value: boolean) => boolean)) => {
 | 
				
			||||||
 | 
					                const openState =
 | 
				
			||||||
 | 
					                    typeof value === 'function' ? value(open) : value;
 | 
				
			||||||
 | 
					                if (setOpenProp) {
 | 
				
			||||||
 | 
					                    setOpenProp(openState);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    _setOpen(openState);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // This sets the cookie to keep the sidebar state.
 | 
				
			||||||
 | 
					                document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            [setOpenProp, open]
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Helper to toggle the sidebar.
 | 
				
			||||||
 | 
					        const toggleSidebar = React.useCallback(() => {
 | 
				
			||||||
 | 
					            return isMobile
 | 
				
			||||||
 | 
					                ? setOpenMobile((open) => !open)
 | 
				
			||||||
 | 
					                : setOpen((open) => !open);
 | 
				
			||||||
 | 
					        }, [isMobile, setOpen, setOpenMobile]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Adds a keyboard shortcut to toggle the sidebar.
 | 
				
			||||||
 | 
					        React.useEffect(() => {
 | 
				
			||||||
 | 
					            const handleKeyDown = (event: KeyboardEvent) => {
 | 
				
			||||||
 | 
					                if (
 | 
				
			||||||
 | 
					                    event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
 | 
				
			||||||
 | 
					                    (event.metaKey || event.ctrlKey)
 | 
				
			||||||
 | 
					                ) {
 | 
				
			||||||
 | 
					                    event.preventDefault();
 | 
				
			||||||
 | 
					                    toggleSidebar();
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            window.addEventListener('keydown', handleKeyDown);
 | 
				
			||||||
 | 
					            return () => window.removeEventListener('keydown', handleKeyDown);
 | 
				
			||||||
 | 
					        }, [toggleSidebar]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // We add a state so that we can do data-state="expanded" or "collapsed".
 | 
				
			||||||
 | 
					        // This makes it easier to style the sidebar with Tailwind classes.
 | 
				
			||||||
 | 
					        const state = open ? 'expanded' : 'collapsed';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const contextValue = React.useMemo<SidebarContext>(
 | 
				
			||||||
 | 
					            () => ({
 | 
				
			||||||
 | 
					                state,
 | 
				
			||||||
 | 
					                open,
 | 
				
			||||||
 | 
					                setOpen,
 | 
				
			||||||
 | 
					                isMobile,
 | 
				
			||||||
 | 
					                openMobile,
 | 
				
			||||||
 | 
					                setOpenMobile,
 | 
				
			||||||
 | 
					                toggleSidebar,
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            [
 | 
				
			||||||
 | 
					                state,
 | 
				
			||||||
 | 
					                open,
 | 
				
			||||||
 | 
					                setOpen,
 | 
				
			||||||
 | 
					                isMobile,
 | 
				
			||||||
 | 
					                openMobile,
 | 
				
			||||||
 | 
					                setOpenMobile,
 | 
				
			||||||
 | 
					                toggleSidebar,
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            <SidebarContext.Provider value={contextValue}>
 | 
				
			||||||
 | 
					                <TooltipProvider delayDuration={0}>
 | 
				
			||||||
 | 
					                    <div
 | 
				
			||||||
 | 
					                        style={
 | 
				
			||||||
 | 
					                            {
 | 
				
			||||||
 | 
					                                '--sidebar-width': SIDEBAR_WIDTH,
 | 
				
			||||||
 | 
					                                '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
 | 
				
			||||||
 | 
					                                ...style,
 | 
				
			||||||
 | 
					                            } as React.CSSProperties
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        className={cn(
 | 
				
			||||||
 | 
					                            'group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar',
 | 
				
			||||||
 | 
					                            className
 | 
				
			||||||
 | 
					                        )}
 | 
				
			||||||
 | 
					                        ref={ref}
 | 
				
			||||||
 | 
					                        {...props}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                        {children}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </TooltipProvider>
 | 
				
			||||||
 | 
					            </SidebarContext.Provider>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					SidebarProvider.displayName = 'SidebarProvider';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Sidebar = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLDivElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'div'> & {
 | 
				
			||||||
 | 
					        side?: 'left' | 'right';
 | 
				
			||||||
 | 
					        variant?: 'sidebar' | 'floating' | 'inset';
 | 
				
			||||||
 | 
					        collapsible?: 'offcanvas' | 'icon' | 'none';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					>(
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            side = 'left',
 | 
				
			||||||
 | 
					            variant = 'sidebar',
 | 
				
			||||||
 | 
					            collapsible = 'offcanvas',
 | 
				
			||||||
 | 
					            className,
 | 
				
			||||||
 | 
					            children,
 | 
				
			||||||
 | 
					            ...props
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        ref
 | 
				
			||||||
 | 
					    ) => {
 | 
				
			||||||
 | 
					        const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (collapsible === 'none') {
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                <div
 | 
				
			||||||
 | 
					                    className={cn(
 | 
				
			||||||
 | 
					                        'flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground',
 | 
				
			||||||
 | 
					                        className
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                    ref={ref}
 | 
				
			||||||
 | 
					                    {...props}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                    {children}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (isMobile) {
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                <Sheet
 | 
				
			||||||
 | 
					                    open={openMobile}
 | 
				
			||||||
 | 
					                    onOpenChange={setOpenMobile}
 | 
				
			||||||
 | 
					                    {...props}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                    <SheetContent
 | 
				
			||||||
 | 
					                        data-sidebar="sidebar"
 | 
				
			||||||
 | 
					                        data-mobile="true"
 | 
				
			||||||
 | 
					                        className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
 | 
				
			||||||
 | 
					                        style={
 | 
				
			||||||
 | 
					                            {
 | 
				
			||||||
 | 
					                                '--sidebar-width': SIDEBAR_WIDTH_MOBILE,
 | 
				
			||||||
 | 
					                            } as React.CSSProperties
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        side={side}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                        <SheetHeader className="sr-only">
 | 
				
			||||||
 | 
					                            <SheetTitle>Sidebar</SheetTitle>
 | 
				
			||||||
 | 
					                            <SheetDescription>
 | 
				
			||||||
 | 
					                                Displays the mobile sidebar.
 | 
				
			||||||
 | 
					                            </SheetDescription>
 | 
				
			||||||
 | 
					                        </SheetHeader>
 | 
				
			||||||
 | 
					                        <div className="flex size-full flex-col">
 | 
				
			||||||
 | 
					                            {children}
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </SheetContent>
 | 
				
			||||||
 | 
					                </Sheet>
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					                ref={ref}
 | 
				
			||||||
 | 
					                className="group peer hidden text-sidebar-foreground md:block"
 | 
				
			||||||
 | 
					                data-state={state}
 | 
				
			||||||
 | 
					                data-collapsible={state === 'collapsed' ? collapsible : ''}
 | 
				
			||||||
 | 
					                data-variant={variant}
 | 
				
			||||||
 | 
					                data-side={side}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					                {/* This is what handles the sidebar gap on desktop */}
 | 
				
			||||||
 | 
					                <div
 | 
				
			||||||
 | 
					                    className={cn(
 | 
				
			||||||
 | 
					                        'relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear',
 | 
				
			||||||
 | 
					                        'group-data-[collapsible=offcanvas]:w-0',
 | 
				
			||||||
 | 
					                        'group-data-[side=right]:rotate-180',
 | 
				
			||||||
 | 
					                        variant === 'floating' || variant === 'inset'
 | 
				
			||||||
 | 
					                            ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]'
 | 
				
			||||||
 | 
					                            : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]'
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					                <div
 | 
				
			||||||
 | 
					                    className={cn(
 | 
				
			||||||
 | 
					                        'fixed inset-y-0 z-10 hidden h-svh w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex',
 | 
				
			||||||
 | 
					                        side === 'left'
 | 
				
			||||||
 | 
					                            ? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
 | 
				
			||||||
 | 
					                            : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
 | 
				
			||||||
 | 
					                        // Adjust the padding for floating and inset variants.
 | 
				
			||||||
 | 
					                        variant === 'floating' || variant === 'inset'
 | 
				
			||||||
 | 
					                            ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]'
 | 
				
			||||||
 | 
					                            : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l',
 | 
				
			||||||
 | 
					                        className
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                    {...props}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                    <div
 | 
				
			||||||
 | 
					                        data-sidebar="sidebar"
 | 
				
			||||||
 | 
					                        className="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                        {children}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					Sidebar.displayName = 'Sidebar';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarTrigger = React.forwardRef<
 | 
				
			||||||
 | 
					    React.ElementRef<typeof Button>,
 | 
				
			||||||
 | 
					    React.ComponentProps<typeof Button>
 | 
				
			||||||
 | 
					>(({ className, onClick, ...props }, ref) => {
 | 
				
			||||||
 | 
					    const { toggleSidebar } = useSidebar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <Button
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            data-sidebar="trigger"
 | 
				
			||||||
 | 
					            variant="ghost"
 | 
				
			||||||
 | 
					            size="icon"
 | 
				
			||||||
 | 
					            className={cn('h-7 w-7', className)}
 | 
				
			||||||
 | 
					            onClick={(event) => {
 | 
				
			||||||
 | 
					                onClick?.(event);
 | 
				
			||||||
 | 
					                toggleSidebar();
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            <ViewVerticalIcon />
 | 
				
			||||||
 | 
					            <span className="sr-only">Toggle Sidebar</span>
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					SidebarTrigger.displayName = 'SidebarTrigger';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarRail = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLButtonElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'button'>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => {
 | 
				
			||||||
 | 
					    const { toggleSidebar } = useSidebar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <button
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            data-sidebar="rail"
 | 
				
			||||||
 | 
					            aria-label="Toggle Sidebar"
 | 
				
			||||||
 | 
					            tabIndex={-1}
 | 
				
			||||||
 | 
					            onClick={toggleSidebar}
 | 
				
			||||||
 | 
					            title="Toggle Sidebar"
 | 
				
			||||||
 | 
					            className={cn(
 | 
				
			||||||
 | 
					                'absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex',
 | 
				
			||||||
 | 
					                '[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize',
 | 
				
			||||||
 | 
					                '[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize',
 | 
				
			||||||
 | 
					                'group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar',
 | 
				
			||||||
 | 
					                '[[data-side=left][data-collapsible=offcanvas]_&]:-right-2',
 | 
				
			||||||
 | 
					                '[[data-side=right][data-collapsible=offcanvas]_&]:-left-2',
 | 
				
			||||||
 | 
					                className
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					SidebarRail.displayName = 'SidebarRail';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarInset = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLDivElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'main'>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <main
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            className={cn(
 | 
				
			||||||
 | 
					                'relative flex w-full flex-1 flex-col bg-background',
 | 
				
			||||||
 | 
					                'md:peer-data-[variant=inset]:m-2 md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow',
 | 
				
			||||||
 | 
					                className
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					SidebarInset.displayName = 'SidebarInset';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarInput = React.forwardRef<
 | 
				
			||||||
 | 
					    React.ElementRef<typeof Input>,
 | 
				
			||||||
 | 
					    React.ComponentProps<typeof Input>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <Input
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            data-sidebar="input"
 | 
				
			||||||
 | 
					            className={cn(
 | 
				
			||||||
 | 
					                'h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring',
 | 
				
			||||||
 | 
					                className
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					SidebarInput.displayName = 'SidebarInput';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarHeader = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLDivElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'div'>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            data-sidebar="header"
 | 
				
			||||||
 | 
					            className={cn('flex flex-col gap-2 p-2', className)}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					SidebarHeader.displayName = 'SidebarHeader';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarFooter = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLDivElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'div'>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            data-sidebar="footer"
 | 
				
			||||||
 | 
					            className={cn('flex flex-col gap-2 p-2', className)}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					SidebarFooter.displayName = 'SidebarFooter';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarSeparator = React.forwardRef<
 | 
				
			||||||
 | 
					    React.ElementRef<typeof Separator>,
 | 
				
			||||||
 | 
					    React.ComponentProps<typeof Separator>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <Separator
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            data-sidebar="separator"
 | 
				
			||||||
 | 
					            className={cn('mx-2 w-auto bg-sidebar-border', className)}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					SidebarSeparator.displayName = 'SidebarSeparator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarContent = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLDivElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'div'>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            data-sidebar="content"
 | 
				
			||||||
 | 
					            className={cn(
 | 
				
			||||||
 | 
					                'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden',
 | 
				
			||||||
 | 
					                className
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					SidebarContent.displayName = 'SidebarContent';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarGroup = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLDivElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'div'>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            data-sidebar="group"
 | 
				
			||||||
 | 
					            className={cn(
 | 
				
			||||||
 | 
					                'relative flex w-full min-w-0 flex-col p-2',
 | 
				
			||||||
 | 
					                className
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					SidebarGroup.displayName = 'SidebarGroup';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarGroupLabel = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLDivElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'div'> & { asChild?: boolean }
 | 
				
			||||||
 | 
					>(({ className, asChild = false, ...props }, ref) => {
 | 
				
			||||||
 | 
					    const Comp = asChild ? Slot : 'div';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <Comp
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            data-sidebar="group-label"
 | 
				
			||||||
 | 
					            className={cn(
 | 
				
			||||||
 | 
					                'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
 | 
				
			||||||
 | 
					                'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
 | 
				
			||||||
 | 
					                className
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					SidebarGroupLabel.displayName = 'SidebarGroupLabel';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarGroupAction = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLButtonElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'button'> & { asChild?: boolean }
 | 
				
			||||||
 | 
					>(({ className, asChild = false, ...props }, ref) => {
 | 
				
			||||||
 | 
					    const Comp = asChild ? Slot : 'button';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <Comp
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            data-sidebar="group-action"
 | 
				
			||||||
 | 
					            className={cn(
 | 
				
			||||||
 | 
					                'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0',
 | 
				
			||||||
 | 
					                // Increases the hit area of the button on mobile.
 | 
				
			||||||
 | 
					                'after:absolute after:-inset-2 after:md:hidden',
 | 
				
			||||||
 | 
					                'group-data-[collapsible=icon]:hidden',
 | 
				
			||||||
 | 
					                className
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					SidebarGroupAction.displayName = 'SidebarGroupAction';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarGroupContent = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLDivElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'div'>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					        ref={ref}
 | 
				
			||||||
 | 
					        data-sidebar="group-content"
 | 
				
			||||||
 | 
					        className={cn('w-full text-sm', className)}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					));
 | 
				
			||||||
 | 
					SidebarGroupContent.displayName = 'SidebarGroupContent';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarMenu = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLUListElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'ul'>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					    <ul
 | 
				
			||||||
 | 
					        ref={ref}
 | 
				
			||||||
 | 
					        data-sidebar="menu"
 | 
				
			||||||
 | 
					        className={cn('flex w-full min-w-0 flex-col gap-1', className)}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					));
 | 
				
			||||||
 | 
					SidebarMenu.displayName = 'SidebarMenu';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarMenuItem = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLLIElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'li'>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					    <li
 | 
				
			||||||
 | 
					        ref={ref}
 | 
				
			||||||
 | 
					        data-sidebar="menu-item"
 | 
				
			||||||
 | 
					        className={cn('group/menu-item relative', className)}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					));
 | 
				
			||||||
 | 
					SidebarMenuItem.displayName = 'SidebarMenuItem';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const sidebarMenuButtonVariants = cva(
 | 
				
			||||||
 | 
					    'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0',
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        variants: {
 | 
				
			||||||
 | 
					            variant: {
 | 
				
			||||||
 | 
					                default:
 | 
				
			||||||
 | 
					                    'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
 | 
				
			||||||
 | 
					                outline:
 | 
				
			||||||
 | 
					                    'bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            size: {
 | 
				
			||||||
 | 
					                default: 'h-8 text-sm',
 | 
				
			||||||
 | 
					                sm: 'h-7 text-xs',
 | 
				
			||||||
 | 
					                lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        defaultVariants: {
 | 
				
			||||||
 | 
					            variant: 'default',
 | 
				
			||||||
 | 
					            size: 'default',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarMenuButton = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLButtonElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'button'> & {
 | 
				
			||||||
 | 
					        asChild?: boolean;
 | 
				
			||||||
 | 
					        isActive?: boolean;
 | 
				
			||||||
 | 
					        tooltip?: string | React.ComponentProps<typeof TooltipContent>;
 | 
				
			||||||
 | 
					    } & VariantProps<typeof sidebarMenuButtonVariants>
 | 
				
			||||||
 | 
					>(
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            asChild = false,
 | 
				
			||||||
 | 
					            isActive = false,
 | 
				
			||||||
 | 
					            variant = 'default',
 | 
				
			||||||
 | 
					            size = 'default',
 | 
				
			||||||
 | 
					            tooltip,
 | 
				
			||||||
 | 
					            className,
 | 
				
			||||||
 | 
					            ...props
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        ref
 | 
				
			||||||
 | 
					    ) => {
 | 
				
			||||||
 | 
					        const Comp = asChild ? Slot : 'button';
 | 
				
			||||||
 | 
					        const { isMobile, state } = useSidebar();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const button = (
 | 
				
			||||||
 | 
					            <Comp
 | 
				
			||||||
 | 
					                ref={ref}
 | 
				
			||||||
 | 
					                data-sidebar="menu-button"
 | 
				
			||||||
 | 
					                data-size={size}
 | 
				
			||||||
 | 
					                data-active={isActive}
 | 
				
			||||||
 | 
					                className={cn(
 | 
				
			||||||
 | 
					                    sidebarMenuButtonVariants({ variant, size }),
 | 
				
			||||||
 | 
					                    className
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					                {...props}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!tooltip) {
 | 
				
			||||||
 | 
					            return button;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (typeof tooltip === 'string') {
 | 
				
			||||||
 | 
					            tooltip = {
 | 
				
			||||||
 | 
					                children: tooltip,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            <Tooltip>
 | 
				
			||||||
 | 
					                <TooltipTrigger asChild>{button}</TooltipTrigger>
 | 
				
			||||||
 | 
					                <TooltipContent
 | 
				
			||||||
 | 
					                    side="right"
 | 
				
			||||||
 | 
					                    align="center"
 | 
				
			||||||
 | 
					                    hidden={state !== 'collapsed' || isMobile}
 | 
				
			||||||
 | 
					                    {...tooltip}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					            </Tooltip>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					SidebarMenuButton.displayName = 'SidebarMenuButton';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarMenuAction = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLButtonElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'button'> & {
 | 
				
			||||||
 | 
					        asChild?: boolean;
 | 
				
			||||||
 | 
					        showOnHover?: boolean;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
 | 
				
			||||||
 | 
					    const Comp = asChild ? Slot : 'button';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <Comp
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            data-sidebar="menu-action"
 | 
				
			||||||
 | 
					            className={cn(
 | 
				
			||||||
 | 
					                'absolute right-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0',
 | 
				
			||||||
 | 
					                // Increases the hit area of the button on mobile.
 | 
				
			||||||
 | 
					                'after:absolute after:-inset-2 after:md:hidden',
 | 
				
			||||||
 | 
					                'peer-data-[size=sm]/menu-button:top-1',
 | 
				
			||||||
 | 
					                'peer-data-[size=default]/menu-button:top-1.5',
 | 
				
			||||||
 | 
					                'peer-data-[size=lg]/menu-button:top-2.5',
 | 
				
			||||||
 | 
					                'group-data-[collapsible=icon]:hidden',
 | 
				
			||||||
 | 
					                showOnHover &&
 | 
				
			||||||
 | 
					                    'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0',
 | 
				
			||||||
 | 
					                className
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					SidebarMenuAction.displayName = 'SidebarMenuAction';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarMenuBadge = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLDivElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'div'>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					        ref={ref}
 | 
				
			||||||
 | 
					        data-sidebar="menu-badge"
 | 
				
			||||||
 | 
					        className={cn(
 | 
				
			||||||
 | 
					            'pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums text-sidebar-foreground',
 | 
				
			||||||
 | 
					            'peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground',
 | 
				
			||||||
 | 
					            'peer-data-[size=sm]/menu-button:top-1',
 | 
				
			||||||
 | 
					            'peer-data-[size=default]/menu-button:top-1.5',
 | 
				
			||||||
 | 
					            'peer-data-[size=lg]/menu-button:top-2.5',
 | 
				
			||||||
 | 
					            'group-data-[collapsible=icon]:hidden',
 | 
				
			||||||
 | 
					            className
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					));
 | 
				
			||||||
 | 
					SidebarMenuBadge.displayName = 'SidebarMenuBadge';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarMenuSkeleton = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLDivElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'div'> & {
 | 
				
			||||||
 | 
					        showIcon?: boolean;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					>(({ className, showIcon = false, ...props }, ref) => {
 | 
				
			||||||
 | 
					    // Random width between 50 to 90%.
 | 
				
			||||||
 | 
					    const width = React.useMemo(() => {
 | 
				
			||||||
 | 
					        return `${Math.floor(Math.random() * 40) + 50}%`;
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            data-sidebar="menu-skeleton"
 | 
				
			||||||
 | 
					            className={cn(
 | 
				
			||||||
 | 
					                'flex h-8 items-center gap-2 rounded-md px-2',
 | 
				
			||||||
 | 
					                className
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            {showIcon && (
 | 
				
			||||||
 | 
					                <Skeleton
 | 
				
			||||||
 | 
					                    className="size-4 rounded-md"
 | 
				
			||||||
 | 
					                    data-sidebar="menu-skeleton-icon"
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            <Skeleton
 | 
				
			||||||
 | 
					                className="h-4 max-w-[--skeleton-width] flex-1"
 | 
				
			||||||
 | 
					                data-sidebar="menu-skeleton-text"
 | 
				
			||||||
 | 
					                style={
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        '--skeleton-width': width,
 | 
				
			||||||
 | 
					                    } as React.CSSProperties
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					SidebarMenuSkeleton.displayName = 'SidebarMenuSkeleton';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarMenuSub = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLUListElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'ul'>
 | 
				
			||||||
 | 
					>(({ className, ...props }, ref) => (
 | 
				
			||||||
 | 
					    <ul
 | 
				
			||||||
 | 
					        ref={ref}
 | 
				
			||||||
 | 
					        data-sidebar="menu-sub"
 | 
				
			||||||
 | 
					        className={cn(
 | 
				
			||||||
 | 
					            'mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5',
 | 
				
			||||||
 | 
					            'group-data-[collapsible=icon]:hidden',
 | 
				
			||||||
 | 
					            className
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        {...props}
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					));
 | 
				
			||||||
 | 
					SidebarMenuSub.displayName = 'SidebarMenuSub';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarMenuSubItem = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLLIElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'li'>
 | 
				
			||||||
 | 
					>(({ ...props }, ref) => <li ref={ref} {...props} />);
 | 
				
			||||||
 | 
					SidebarMenuSubItem.displayName = 'SidebarMenuSubItem';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SidebarMenuSubButton = React.forwardRef<
 | 
				
			||||||
 | 
					    HTMLAnchorElement,
 | 
				
			||||||
 | 
					    React.ComponentProps<'a'> & {
 | 
				
			||||||
 | 
					        asChild?: boolean;
 | 
				
			||||||
 | 
					        size?: 'sm' | 'md';
 | 
				
			||||||
 | 
					        isActive?: boolean;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					>(({ asChild = false, size = 'md', isActive, className, ...props }, ref) => {
 | 
				
			||||||
 | 
					    const Comp = asChild ? Slot : 'a';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <Comp
 | 
				
			||||||
 | 
					            ref={ref}
 | 
				
			||||||
 | 
					            data-sidebar="menu-sub-button"
 | 
				
			||||||
 | 
					            data-size={size}
 | 
				
			||||||
 | 
					            data-active={isActive}
 | 
				
			||||||
 | 
					            className={cn(
 | 
				
			||||||
 | 
					                'flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground',
 | 
				
			||||||
 | 
					                'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground',
 | 
				
			||||||
 | 
					                size === 'sm' && 'text-xs',
 | 
				
			||||||
 | 
					                size === 'md' && 'text-sm',
 | 
				
			||||||
 | 
					                'group-data-[collapsible=icon]:hidden',
 | 
				
			||||||
 | 
					                className
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					SidebarMenuSubButton.displayName = 'SidebarMenuSubButton';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export {
 | 
				
			||||||
 | 
					    Sidebar,
 | 
				
			||||||
 | 
					    SidebarContent,
 | 
				
			||||||
 | 
					    SidebarFooter,
 | 
				
			||||||
 | 
					    SidebarGroup,
 | 
				
			||||||
 | 
					    SidebarGroupAction,
 | 
				
			||||||
 | 
					    SidebarGroupContent,
 | 
				
			||||||
 | 
					    SidebarGroupLabel,
 | 
				
			||||||
 | 
					    SidebarHeader,
 | 
				
			||||||
 | 
					    SidebarInput,
 | 
				
			||||||
 | 
					    SidebarInset,
 | 
				
			||||||
 | 
					    SidebarMenu,
 | 
				
			||||||
 | 
					    SidebarMenuAction,
 | 
				
			||||||
 | 
					    SidebarMenuBadge,
 | 
				
			||||||
 | 
					    SidebarMenuButton,
 | 
				
			||||||
 | 
					    SidebarMenuItem,
 | 
				
			||||||
 | 
					    SidebarMenuSkeleton,
 | 
				
			||||||
 | 
					    SidebarMenuSub,
 | 
				
			||||||
 | 
					    SidebarMenuSubButton,
 | 
				
			||||||
 | 
					    SidebarMenuSubItem,
 | 
				
			||||||
 | 
					    SidebarProvider,
 | 
				
			||||||
 | 
					    SidebarRail,
 | 
				
			||||||
 | 
					    SidebarSeparator,
 | 
				
			||||||
 | 
					    SidebarTrigger,
 | 
				
			||||||
 | 
					    SidebarContext,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/components/sidebar/use-sidebar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { SidebarContext } from './sidebar';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useSidebar = () => {
 | 
				
			||||||
 | 
					    const context = React.useContext(SidebarContext);
 | 
				
			||||||
 | 
					    if (!context) {
 | 
				
			||||||
 | 
					        throw new Error('useSidebar must be used within a SidebarProvider.');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return context;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										16
									
								
								src/components/skeleton/skeleton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { cn } from '@/lib/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function Skeleton({
 | 
				
			||||||
 | 
					    className,
 | 
				
			||||||
 | 
					    ...props
 | 
				
			||||||
 | 
					}: React.HTMLAttributes<HTMLDivElement>) {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					            className={cn('animate-pulse rounded-md bg-primary/10', className)}
 | 
				
			||||||
 | 
					            {...props}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export { Skeleton };
 | 
				
			||||||
@@ -20,6 +20,7 @@ export function Toaster() {
 | 
				
			|||||||
                description,
 | 
					                description,
 | 
				
			||||||
                action,
 | 
					                action,
 | 
				
			||||||
                layout = 'row',
 | 
					                layout = 'row',
 | 
				
			||||||
 | 
					                hideCloseButton = false,
 | 
				
			||||||
                ...props
 | 
					                ...props
 | 
				
			||||||
            }) {
 | 
					            }) {
 | 
				
			||||||
                return (
 | 
					                return (
 | 
				
			||||||
@@ -38,7 +39,7 @@ export function Toaster() {
 | 
				
			|||||||
                            ) : null}
 | 
					                            ) : null}
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                        {layout === 'row' ? action : null}
 | 
					                        {layout === 'row' ? action : null}
 | 
				
			||||||
                        <ToastClose />
 | 
					                        {!hideCloseButton ? <ToastClose /> : null}
 | 
				
			||||||
                    </Toast>
 | 
					                    </Toast>
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
            })}
 | 
					            })}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@ type ToasterToast = ToastProps & {
 | 
				
			|||||||
    description?: React.ReactNode;
 | 
					    description?: React.ReactNode;
 | 
				
			||||||
    action?: ToastActionElement;
 | 
					    action?: ToastActionElement;
 | 
				
			||||||
    layout?: 'row' | 'column';
 | 
					    layout?: 'row' | 'column';
 | 
				
			||||||
 | 
					    hideCloseButton?: boolean;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								src/components/tree-view/tree-item-skeleton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { Skeleton } from '../skeleton/skeleton';
 | 
				
			||||||
 | 
					import { cn } from '@/lib/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface TreeItemSkeletonProps
 | 
				
			||||||
 | 
					    extends React.HTMLAttributes<HTMLDivElement> {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const TreeItemSkeleton: React.FC<TreeItemSkeletonProps> = ({
 | 
				
			||||||
 | 
					    className,
 | 
				
			||||||
 | 
					    style,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div className={cn('px-2 py-1', className)} style={style}>
 | 
				
			||||||
 | 
					            <Skeleton className="h-3.5 w-full rounded-sm" />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										461
									
								
								src/components/tree-view/tree-view.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,461 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					    ChevronRight,
 | 
				
			||||||
 | 
					    File,
 | 
				
			||||||
 | 
					    Folder,
 | 
				
			||||||
 | 
					    Loader2,
 | 
				
			||||||
 | 
					    type LucideIcon,
 | 
				
			||||||
 | 
					} from 'lucide-react';
 | 
				
			||||||
 | 
					import { motion, AnimatePresence } from 'framer-motion';
 | 
				
			||||||
 | 
					import { cn } from '@/lib/utils';
 | 
				
			||||||
 | 
					import { Button } from '@/components/button/button';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					    TreeNode,
 | 
				
			||||||
 | 
					    FetchChildrenFunction,
 | 
				
			||||||
 | 
					    SelectableTreeProps,
 | 
				
			||||||
 | 
					} from './tree';
 | 
				
			||||||
 | 
					import type { ExpandedState } from './use-tree';
 | 
				
			||||||
 | 
					import { useTree } from './use-tree';
 | 
				
			||||||
 | 
					import type { Dispatch, ReactNode, SetStateAction } from 'react';
 | 
				
			||||||
 | 
					import React, { useCallback, useEffect, useMemo, useState } from 'react';
 | 
				
			||||||
 | 
					import { TreeItemSkeleton } from './tree-item-skeleton';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    Tooltip,
 | 
				
			||||||
 | 
					    TooltipContent,
 | 
				
			||||||
 | 
					    TooltipTrigger,
 | 
				
			||||||
 | 
					} from '@/components/tooltip/tooltip';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface TreeViewProps<
 | 
				
			||||||
 | 
					    Type extends string,
 | 
				
			||||||
 | 
					    Context extends Record<Type, unknown>,
 | 
				
			||||||
 | 
					> {
 | 
				
			||||||
 | 
					    data: TreeNode<Type, Context>[];
 | 
				
			||||||
 | 
					    fetchChildren?: FetchChildrenFunction<Type, Context>;
 | 
				
			||||||
 | 
					    onNodeClick?: (node: TreeNode<Type, Context>) => void;
 | 
				
			||||||
 | 
					    className?: string;
 | 
				
			||||||
 | 
					    defaultIcon?: LucideIcon;
 | 
				
			||||||
 | 
					    defaultFolderIcon?: LucideIcon;
 | 
				
			||||||
 | 
					    defaultIconProps?: React.ComponentProps<LucideIcon>;
 | 
				
			||||||
 | 
					    defaultFolderIconProps?: React.ComponentProps<LucideIcon>;
 | 
				
			||||||
 | 
					    selectable?: SelectableTreeProps<Type, Context>;
 | 
				
			||||||
 | 
					    expanded?: ExpandedState;
 | 
				
			||||||
 | 
					    setExpanded?: Dispatch<SetStateAction<ExpandedState>>;
 | 
				
			||||||
 | 
					    renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode;
 | 
				
			||||||
 | 
					    renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode;
 | 
				
			||||||
 | 
					    loadingNodeIds?: string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function TreeView<
 | 
				
			||||||
 | 
					    Type extends string,
 | 
				
			||||||
 | 
					    Context extends Record<Type, unknown>,
 | 
				
			||||||
 | 
					>({
 | 
				
			||||||
 | 
					    data,
 | 
				
			||||||
 | 
					    fetchChildren,
 | 
				
			||||||
 | 
					    onNodeClick,
 | 
				
			||||||
 | 
					    className,
 | 
				
			||||||
 | 
					    defaultIcon = File,
 | 
				
			||||||
 | 
					    defaultFolderIcon = Folder,
 | 
				
			||||||
 | 
					    defaultIconProps,
 | 
				
			||||||
 | 
					    defaultFolderIconProps,
 | 
				
			||||||
 | 
					    selectable,
 | 
				
			||||||
 | 
					    expanded: expandedProp,
 | 
				
			||||||
 | 
					    setExpanded: setExpandedProp,
 | 
				
			||||||
 | 
					    renderHoverComponent,
 | 
				
			||||||
 | 
					    renderActionsComponent,
 | 
				
			||||||
 | 
					    loadingNodeIds,
 | 
				
			||||||
 | 
					}: TreeViewProps<Type, Context>) {
 | 
				
			||||||
 | 
					    const { expanded, loading, loadedChildren, hasMoreChildren, toggleNode } =
 | 
				
			||||||
 | 
					        useTree({
 | 
				
			||||||
 | 
					            fetchChildren,
 | 
				
			||||||
 | 
					            expanded: expandedProp,
 | 
				
			||||||
 | 
					            setExpanded: setExpandedProp,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    const [selectedIdInternal, setSelectedIdInternal] = React.useState<
 | 
				
			||||||
 | 
					        string | undefined
 | 
				
			||||||
 | 
					    >(selectable?.defaultSelectedId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const selectedId = useMemo(() => {
 | 
				
			||||||
 | 
					        return selectable?.selectedId ?? selectedIdInternal;
 | 
				
			||||||
 | 
					    }, [selectable?.selectedId, selectedIdInternal]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const setSelectedId = useCallback(
 | 
				
			||||||
 | 
					        (value: SetStateAction<string | undefined>) => {
 | 
				
			||||||
 | 
					            if (selectable?.setSelectedId) {
 | 
				
			||||||
 | 
					                selectable.setSelectedId(value);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                setSelectedIdInternal(value);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [selectable, setSelectedIdInternal]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        if (selectable?.enabled && selectable.defaultSelectedId) {
 | 
				
			||||||
 | 
					            if (selectable.defaultSelectedId === selectedId) return;
 | 
				
			||||||
 | 
					            setSelectedId(selectable.defaultSelectedId);
 | 
				
			||||||
 | 
					            const { node, path } = findNodeById(
 | 
				
			||||||
 | 
					                data,
 | 
				
			||||||
 | 
					                selectable.defaultSelectedId
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (node) {
 | 
				
			||||||
 | 
					                selectable.onSelectedChange?.(node);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Expand all parent nodes
 | 
				
			||||||
 | 
					                for (const parent of path) {
 | 
				
			||||||
 | 
					                    if (expanded[parent.id]) continue;
 | 
				
			||||||
 | 
					                    toggleNode(
 | 
				
			||||||
 | 
					                        parent.id,
 | 
				
			||||||
 | 
					                        parent.type,
 | 
				
			||||||
 | 
					                        parent.context,
 | 
				
			||||||
 | 
					                        parent.children
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }, [selectable, toggleNode, selectedId, data, expanded, setSelectedId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleNodeSelect = (node: TreeNode<Type, Context>) => {
 | 
				
			||||||
 | 
					        if (selectable?.enabled) {
 | 
				
			||||||
 | 
					            setSelectedId(node.id);
 | 
				
			||||||
 | 
					            selectable.onSelectedChange?.(node);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div className={cn('w-full', className)}>
 | 
				
			||||||
 | 
					            {data.map((node, index) => (
 | 
				
			||||||
 | 
					                <TreeNode
 | 
				
			||||||
 | 
					                    key={node.id}
 | 
				
			||||||
 | 
					                    node={node}
 | 
				
			||||||
 | 
					                    level={0}
 | 
				
			||||||
 | 
					                    expanded={expanded}
 | 
				
			||||||
 | 
					                    loading={loading}
 | 
				
			||||||
 | 
					                    loadedChildren={loadedChildren}
 | 
				
			||||||
 | 
					                    hasMoreChildren={hasMoreChildren}
 | 
				
			||||||
 | 
					                    onToggle={toggleNode}
 | 
				
			||||||
 | 
					                    onNodeClick={onNodeClick}
 | 
				
			||||||
 | 
					                    defaultIcon={defaultIcon}
 | 
				
			||||||
 | 
					                    defaultFolderIcon={defaultFolderIcon}
 | 
				
			||||||
 | 
					                    defaultIconProps={defaultIconProps}
 | 
				
			||||||
 | 
					                    defaultFolderIconProps={defaultFolderIconProps}
 | 
				
			||||||
 | 
					                    selectable={selectable?.enabled}
 | 
				
			||||||
 | 
					                    selectedId={selectedId}
 | 
				
			||||||
 | 
					                    onSelect={handleNodeSelect}
 | 
				
			||||||
 | 
					                    className={index > 0 ? 'mt-0.5' : ''}
 | 
				
			||||||
 | 
					                    renderHoverComponent={renderHoverComponent}
 | 
				
			||||||
 | 
					                    renderActionsComponent={renderActionsComponent}
 | 
				
			||||||
 | 
					                    loadingNodeIds={loadingNodeIds}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface TreeNodeProps<
 | 
				
			||||||
 | 
					    Type extends string,
 | 
				
			||||||
 | 
					    Context extends Record<Type, unknown>,
 | 
				
			||||||
 | 
					> {
 | 
				
			||||||
 | 
					    node: TreeNode<Type, Context>;
 | 
				
			||||||
 | 
					    level: number;
 | 
				
			||||||
 | 
					    expanded: Record<string, boolean>;
 | 
				
			||||||
 | 
					    loading: Record<string, boolean>;
 | 
				
			||||||
 | 
					    loadedChildren: Record<string, TreeNode<Type, Context>[]>;
 | 
				
			||||||
 | 
					    hasMoreChildren: Record<string, boolean>;
 | 
				
			||||||
 | 
					    onToggle: (
 | 
				
			||||||
 | 
					        nodeId: string,
 | 
				
			||||||
 | 
					        nodeType: Type,
 | 
				
			||||||
 | 
					        nodeContext: Context[Type],
 | 
				
			||||||
 | 
					        staticChildren?: TreeNode<Type, Context>[]
 | 
				
			||||||
 | 
					    ) => void;
 | 
				
			||||||
 | 
					    onNodeClick?: (node: TreeNode<Type, Context>) => void;
 | 
				
			||||||
 | 
					    defaultIcon: LucideIcon;
 | 
				
			||||||
 | 
					    defaultFolderIcon: LucideIcon;
 | 
				
			||||||
 | 
					    defaultIconProps?: React.ComponentProps<LucideIcon>;
 | 
				
			||||||
 | 
					    defaultFolderIconProps?: React.ComponentProps<LucideIcon>;
 | 
				
			||||||
 | 
					    selectable?: boolean;
 | 
				
			||||||
 | 
					    selectedId?: string;
 | 
				
			||||||
 | 
					    onSelect: (node: TreeNode<Type, Context>) => void;
 | 
				
			||||||
 | 
					    className?: string;
 | 
				
			||||||
 | 
					    renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode;
 | 
				
			||||||
 | 
					    renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode;
 | 
				
			||||||
 | 
					    loadingNodeIds?: string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function TreeNode<Type extends string, Context extends Record<Type, unknown>>({
 | 
				
			||||||
 | 
					    node,
 | 
				
			||||||
 | 
					    level,
 | 
				
			||||||
 | 
					    expanded,
 | 
				
			||||||
 | 
					    loading,
 | 
				
			||||||
 | 
					    loadedChildren,
 | 
				
			||||||
 | 
					    hasMoreChildren,
 | 
				
			||||||
 | 
					    onToggle,
 | 
				
			||||||
 | 
					    onNodeClick,
 | 
				
			||||||
 | 
					    defaultIcon: DefaultIcon,
 | 
				
			||||||
 | 
					    defaultFolderIcon: DefaultFolderIcon,
 | 
				
			||||||
 | 
					    defaultIconProps,
 | 
				
			||||||
 | 
					    defaultFolderIconProps,
 | 
				
			||||||
 | 
					    selectable,
 | 
				
			||||||
 | 
					    selectedId,
 | 
				
			||||||
 | 
					    onSelect,
 | 
				
			||||||
 | 
					    className,
 | 
				
			||||||
 | 
					    renderHoverComponent,
 | 
				
			||||||
 | 
					    renderActionsComponent,
 | 
				
			||||||
 | 
					    loadingNodeIds,
 | 
				
			||||||
 | 
					}: TreeNodeProps<Type, Context>) {
 | 
				
			||||||
 | 
					    const [isHovered, setIsHovered] = useState(false);
 | 
				
			||||||
 | 
					    const isExpanded = expanded[node.id];
 | 
				
			||||||
 | 
					    const isLoading = loading[node.id];
 | 
				
			||||||
 | 
					    const children = loadedChildren[node.id] || node.children;
 | 
				
			||||||
 | 
					    const isSelected = selectedId === node.id;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const IconComponent =
 | 
				
			||||||
 | 
					        node.icon || (node.isFolder ? DefaultFolderIcon : DefaultIcon);
 | 
				
			||||||
 | 
					    const iconProps: React.ComponentProps<LucideIcon> = {
 | 
				
			||||||
 | 
					        strokeWidth: isSelected ? 2.5 : 2,
 | 
				
			||||||
 | 
					        ...(node.isFolder ? defaultFolderIconProps : defaultIconProps),
 | 
				
			||||||
 | 
					        ...node.iconProps,
 | 
				
			||||||
 | 
					        className: cn(
 | 
				
			||||||
 | 
					            'h-3.5 w-3.5 text-muted-foreground flex-none',
 | 
				
			||||||
 | 
					            isSelected && 'text-primary text-white',
 | 
				
			||||||
 | 
					            node.iconProps?.className
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div className={cn(className)}>
 | 
				
			||||||
 | 
					            <div
 | 
				
			||||||
 | 
					                className={cn(
 | 
				
			||||||
 | 
					                    'flex items-center gap-1.5 px-2 py-1 rounded-lg cursor-pointer group h-6',
 | 
				
			||||||
 | 
					                    'transition-colors duration-200',
 | 
				
			||||||
 | 
					                    isSelected
 | 
				
			||||||
 | 
					                        ? 'bg-sky-500 border border-sky-600 border dark:bg-sky-600 dark:border-sky-700'
 | 
				
			||||||
 | 
					                        : 'hover:bg-gray-200/50 border border-transparent dark:hover:bg-gray-700/50',
 | 
				
			||||||
 | 
					                    node.className
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					                {...(isSelected ? { 'data-selected': true } : {})}
 | 
				
			||||||
 | 
					                style={{ paddingLeft: `${level * 16 + 8}px` }}
 | 
				
			||||||
 | 
					                onMouseEnter={() => setIsHovered(true)}
 | 
				
			||||||
 | 
					                onMouseLeave={() => setIsHovered(false)}
 | 
				
			||||||
 | 
					                onClick={(e) => {
 | 
				
			||||||
 | 
					                    e.stopPropagation();
 | 
				
			||||||
 | 
					                    if (selectable && !node.unselectable) {
 | 
				
			||||||
 | 
					                        onSelect(node);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    // if (node.isFolder) {
 | 
				
			||||||
 | 
					                    //     onToggle(node.id, node.children);
 | 
				
			||||||
 | 
					                    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // called only once in case of double click
 | 
				
			||||||
 | 
					                    if (e.detail !== 2) {
 | 
				
			||||||
 | 
					                        onNodeClick?.(node);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					                onDoubleClick={(e) => {
 | 
				
			||||||
 | 
					                    e.stopPropagation();
 | 
				
			||||||
 | 
					                    if (node.isFolder) {
 | 
				
			||||||
 | 
					                        onToggle(
 | 
				
			||||||
 | 
					                            node.id,
 | 
				
			||||||
 | 
					                            node.type,
 | 
				
			||||||
 | 
					                            node.context,
 | 
				
			||||||
 | 
					                            node.children
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					                <div className="flex flex-none items-center gap-1.5">
 | 
				
			||||||
 | 
					                    <Button
 | 
				
			||||||
 | 
					                        variant="ghost"
 | 
				
			||||||
 | 
					                        size="icon"
 | 
				
			||||||
 | 
					                        className={cn(
 | 
				
			||||||
 | 
					                            'h-3.5 w-3.5 p-0 hover:bg-transparent flex-none',
 | 
				
			||||||
 | 
					                            isExpanded && 'rotate-90',
 | 
				
			||||||
 | 
					                            'transition-transform duration-200'
 | 
				
			||||||
 | 
					                        )}
 | 
				
			||||||
 | 
					                        onClick={(e) => {
 | 
				
			||||||
 | 
					                            e.stopPropagation();
 | 
				
			||||||
 | 
					                            if (node.isFolder) {
 | 
				
			||||||
 | 
					                                onToggle(
 | 
				
			||||||
 | 
					                                    node.id,
 | 
				
			||||||
 | 
					                                    node.type,
 | 
				
			||||||
 | 
					                                    node.context,
 | 
				
			||||||
 | 
					                                    node.children
 | 
				
			||||||
 | 
					                                );
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                        {node.isFolder &&
 | 
				
			||||||
 | 
					                            (isLoading ? (
 | 
				
			||||||
 | 
					                                <Loader2
 | 
				
			||||||
 | 
					                                    className={cn('size-3.5 animate-spin', {
 | 
				
			||||||
 | 
					                                        'text-white': isSelected,
 | 
				
			||||||
 | 
					                                    })}
 | 
				
			||||||
 | 
					                                />
 | 
				
			||||||
 | 
					                            ) : (
 | 
				
			||||||
 | 
					                                <ChevronRight
 | 
				
			||||||
 | 
					                                    className={cn('size-3.5', {
 | 
				
			||||||
 | 
					                                        'text-white': isSelected,
 | 
				
			||||||
 | 
					                                    })}
 | 
				
			||||||
 | 
					                                    strokeWidth={2}
 | 
				
			||||||
 | 
					                                />
 | 
				
			||||||
 | 
					                            ))}
 | 
				
			||||||
 | 
					                    </Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    {node.tooltip ? (
 | 
				
			||||||
 | 
					                        <Tooltip>
 | 
				
			||||||
 | 
					                            <TooltipTrigger asChild>
 | 
				
			||||||
 | 
					                                {loadingNodeIds?.includes(node.id) ? (
 | 
				
			||||||
 | 
					                                    <Loader2
 | 
				
			||||||
 | 
					                                        className={cn('size-3.5 animate-spin', {
 | 
				
			||||||
 | 
					                                            'text-white': isSelected,
 | 
				
			||||||
 | 
					                                        })}
 | 
				
			||||||
 | 
					                                    />
 | 
				
			||||||
 | 
					                                ) : (
 | 
				
			||||||
 | 
					                                    <IconComponent
 | 
				
			||||||
 | 
					                                        {...(isSelected
 | 
				
			||||||
 | 
					                                            ? { 'data-selected': true }
 | 
				
			||||||
 | 
					                                            : {})}
 | 
				
			||||||
 | 
					                                        {...iconProps}
 | 
				
			||||||
 | 
					                                    />
 | 
				
			||||||
 | 
					                                )}
 | 
				
			||||||
 | 
					                            </TooltipTrigger>
 | 
				
			||||||
 | 
					                            <TooltipContent
 | 
				
			||||||
 | 
					                                align="center"
 | 
				
			||||||
 | 
					                                className="max-w-[400px]"
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
 | 
					                                {node.tooltip}
 | 
				
			||||||
 | 
					                            </TooltipContent>
 | 
				
			||||||
 | 
					                        </Tooltip>
 | 
				
			||||||
 | 
					                    ) : node.empty ? null : loadingNodeIds?.includes(
 | 
				
			||||||
 | 
					                          node.id
 | 
				
			||||||
 | 
					                      ) ? (
 | 
				
			||||||
 | 
					                        <Loader2
 | 
				
			||||||
 | 
					                            className={cn('size-3.5 animate-spin', {
 | 
				
			||||||
 | 
					                                // 'text-white': isSelected,
 | 
				
			||||||
 | 
					                            })}
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                    ) : (
 | 
				
			||||||
 | 
					                        <IconComponent
 | 
				
			||||||
 | 
					                            {...(isSelected ? { 'data-selected': true } : {})}
 | 
				
			||||||
 | 
					                            {...iconProps}
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <span
 | 
				
			||||||
 | 
					                    {...node.labelProps}
 | 
				
			||||||
 | 
					                    className={cn(
 | 
				
			||||||
 | 
					                        'text-xs truncate min-w-0 flex-1 w-0',
 | 
				
			||||||
 | 
					                        isSelected && 'font-medium text-primary text-white',
 | 
				
			||||||
 | 
					                        node.labelProps?.className
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                    {...(isSelected ? { 'data-selected': true } : {})}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                    {node.empty ? '' : node.name}
 | 
				
			||||||
 | 
					                </span>
 | 
				
			||||||
 | 
					                {renderActionsComponent && renderActionsComponent(node)}
 | 
				
			||||||
 | 
					                {isHovered && renderHoverComponent
 | 
				
			||||||
 | 
					                    ? renderHoverComponent(node)
 | 
				
			||||||
 | 
					                    : null}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <AnimatePresence initial={false}>
 | 
				
			||||||
 | 
					                {isExpanded && children && (
 | 
				
			||||||
 | 
					                    <motion.div
 | 
				
			||||||
 | 
					                        initial={{ height: 0, opacity: 0 }}
 | 
				
			||||||
 | 
					                        animate={{
 | 
				
			||||||
 | 
					                            height: 'auto',
 | 
				
			||||||
 | 
					                            opacity: 1,
 | 
				
			||||||
 | 
					                            transition: {
 | 
				
			||||||
 | 
					                                height: {
 | 
				
			||||||
 | 
					                                    duration: Math.min(
 | 
				
			||||||
 | 
					                                        0.3 + children.length * 0.018,
 | 
				
			||||||
 | 
					                                        0.7
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    ease: 'easeInOut',
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                opacity: {
 | 
				
			||||||
 | 
					                                    duration: Math.min(
 | 
				
			||||||
 | 
					                                        0.2 + children.length * 0.012,
 | 
				
			||||||
 | 
					                                        0.4
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    ease: 'easeInOut',
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                        exit={{
 | 
				
			||||||
 | 
					                            height: 0,
 | 
				
			||||||
 | 
					                            opacity: 0,
 | 
				
			||||||
 | 
					                            transition: {
 | 
				
			||||||
 | 
					                                height: {
 | 
				
			||||||
 | 
					                                    duration: Math.min(
 | 
				
			||||||
 | 
					                                        0.2 + children.length * 0.01,
 | 
				
			||||||
 | 
					                                        0.45
 | 
				
			||||||
 | 
					                                    ),
 | 
				
			||||||
 | 
					                                    ease: 'easeInOut',
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                opacity: {
 | 
				
			||||||
 | 
					                                    duration: 0.1,
 | 
				
			||||||
 | 
					                                    ease: 'easeOut',
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                        style={{ overflow: 'hidden' }}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                        {children.map((child) => (
 | 
				
			||||||
 | 
					                            <TreeNode
 | 
				
			||||||
 | 
					                                key={child.id}
 | 
				
			||||||
 | 
					                                node={child}
 | 
				
			||||||
 | 
					                                level={level + 1}
 | 
				
			||||||
 | 
					                                expanded={expanded}
 | 
				
			||||||
 | 
					                                loading={loading}
 | 
				
			||||||
 | 
					                                loadedChildren={loadedChildren}
 | 
				
			||||||
 | 
					                                hasMoreChildren={hasMoreChildren}
 | 
				
			||||||
 | 
					                                onToggle={onToggle}
 | 
				
			||||||
 | 
					                                onNodeClick={onNodeClick}
 | 
				
			||||||
 | 
					                                defaultIcon={DefaultIcon}
 | 
				
			||||||
 | 
					                                defaultFolderIcon={DefaultFolderIcon}
 | 
				
			||||||
 | 
					                                defaultIconProps={defaultIconProps}
 | 
				
			||||||
 | 
					                                defaultFolderIconProps={defaultFolderIconProps}
 | 
				
			||||||
 | 
					                                selectable={selectable}
 | 
				
			||||||
 | 
					                                selectedId={selectedId}
 | 
				
			||||||
 | 
					                                onSelect={onSelect}
 | 
				
			||||||
 | 
					                                className="mt-0.5"
 | 
				
			||||||
 | 
					                                renderHoverComponent={renderHoverComponent}
 | 
				
			||||||
 | 
					                                renderActionsComponent={renderActionsComponent}
 | 
				
			||||||
 | 
					                                loadingNodeIds={loadingNodeIds}
 | 
				
			||||||
 | 
					                            />
 | 
				
			||||||
 | 
					                        ))}
 | 
				
			||||||
 | 
					                        {isLoading ? (
 | 
				
			||||||
 | 
					                            <TreeItemSkeleton
 | 
				
			||||||
 | 
					                                style={{
 | 
				
			||||||
 | 
					                                    paddingLeft: `${level + 2 * 16 + 8}px`,
 | 
				
			||||||
 | 
					                                }}
 | 
				
			||||||
 | 
					                            />
 | 
				
			||||||
 | 
					                        ) : null}
 | 
				
			||||||
 | 
					                    </motion.div>
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					            </AnimatePresence>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function findNodeById<
 | 
				
			||||||
 | 
					    Type extends string,
 | 
				
			||||||
 | 
					    Context extends Record<Type, unknown>,
 | 
				
			||||||
 | 
					>(
 | 
				
			||||||
 | 
					    nodes: TreeNode<Type, Context>[],
 | 
				
			||||||
 | 
					    id: string,
 | 
				
			||||||
 | 
					    initialPath: TreeNode<Type, Context>[] = []
 | 
				
			||||||
 | 
					): { node: TreeNode<Type, Context> | null; path: TreeNode<Type, Context>[] } {
 | 
				
			||||||
 | 
					    const path: TreeNode<Type, Context>[] = [...initialPath];
 | 
				
			||||||
 | 
					    for (const node of nodes) {
 | 
				
			||||||
 | 
					        if (node.id === id) return { node, path };
 | 
				
			||||||
 | 
					        if (node.children) {
 | 
				
			||||||
 | 
					            const found = findNodeById(node.children, id, [...path, node]);
 | 
				
			||||||
 | 
					            if (found.node) {
 | 
				
			||||||
 | 
					                return found;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return { node: null, path };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										41
									
								
								src/components/tree-view/tree.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					import type { LucideIcon } from 'lucide-react';
 | 
				
			||||||
 | 
					import type React from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface TreeNode<
 | 
				
			||||||
 | 
					    Type extends string,
 | 
				
			||||||
 | 
					    Context extends Record<Type, unknown>,
 | 
				
			||||||
 | 
					> {
 | 
				
			||||||
 | 
					    id: string;
 | 
				
			||||||
 | 
					    name: string;
 | 
				
			||||||
 | 
					    isFolder?: boolean;
 | 
				
			||||||
 | 
					    children?: TreeNode<Type, Context>[];
 | 
				
			||||||
 | 
					    icon?: LucideIcon;
 | 
				
			||||||
 | 
					    iconProps?: React.ComponentProps<LucideIcon>;
 | 
				
			||||||
 | 
					    labelProps?: React.ComponentProps<'span'>;
 | 
				
			||||||
 | 
					    type: Type;
 | 
				
			||||||
 | 
					    unselectable?: boolean;
 | 
				
			||||||
 | 
					    tooltip?: string;
 | 
				
			||||||
 | 
					    context: Context[Type];
 | 
				
			||||||
 | 
					    empty?: boolean;
 | 
				
			||||||
 | 
					    className?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type FetchChildrenFunction<
 | 
				
			||||||
 | 
					    Type extends string,
 | 
				
			||||||
 | 
					    Context extends Record<Type, unknown>,
 | 
				
			||||||
 | 
					> = (
 | 
				
			||||||
 | 
					    nodeId: string,
 | 
				
			||||||
 | 
					    nodeType: Type,
 | 
				
			||||||
 | 
					    nodeContext: Context[Type]
 | 
				
			||||||
 | 
					) => Promise<TreeNode<Type, Context>[]>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface SelectableTreeProps<
 | 
				
			||||||
 | 
					    Type extends string,
 | 
				
			||||||
 | 
					    Context extends Record<Type, unknown>,
 | 
				
			||||||
 | 
					> {
 | 
				
			||||||
 | 
					    enabled: boolean;
 | 
				
			||||||
 | 
					    defaultSelectedId?: string;
 | 
				
			||||||
 | 
					    onSelectedChange?: (node: TreeNode<Type, Context>) => void;
 | 
				
			||||||
 | 
					    selectedId?: string;
 | 
				
			||||||
 | 
					    setSelectedId?: React.Dispatch<React.SetStateAction<string | undefined>>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										153
									
								
								src/components/tree-view/use-tree.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,153 @@
 | 
				
			|||||||
 | 
					import type { Dispatch, SetStateAction } from 'react';
 | 
				
			||||||
 | 
					import { useState, useCallback, useMemo } from 'react';
 | 
				
			||||||
 | 
					import type { TreeNode, FetchChildrenFunction } from './tree';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ExpandedState {
 | 
				
			||||||
 | 
					    [key: string]: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface LoadingState {
 | 
				
			||||||
 | 
					    [key: string]: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface LoadedChildren<
 | 
				
			||||||
 | 
					    Type extends string,
 | 
				
			||||||
 | 
					    Context extends Record<Type, unknown>,
 | 
				
			||||||
 | 
					> {
 | 
				
			||||||
 | 
					    [key: string]: TreeNode<Type, Context>[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface HasMoreChildrenState {
 | 
				
			||||||
 | 
					    [key: string]: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function useTree<
 | 
				
			||||||
 | 
					    Type extends string,
 | 
				
			||||||
 | 
					    Context extends Record<Type, unknown>,
 | 
				
			||||||
 | 
					>({
 | 
				
			||||||
 | 
					    fetchChildren,
 | 
				
			||||||
 | 
					    expanded: expandedProp,
 | 
				
			||||||
 | 
					    setExpanded: setExpandedProp,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					    fetchChildren?: FetchChildrenFunction<Type, Context>;
 | 
				
			||||||
 | 
					    expanded?: ExpandedState;
 | 
				
			||||||
 | 
					    setExpanded?: Dispatch<SetStateAction<ExpandedState>>;
 | 
				
			||||||
 | 
					}) {
 | 
				
			||||||
 | 
					    const [expandedInternal, setExpandedInternal] = useState<ExpandedState>({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const expanded = useMemo(
 | 
				
			||||||
 | 
					        () => expandedProp ?? expandedInternal,
 | 
				
			||||||
 | 
					        [expandedProp, expandedInternal]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const setExpanded = useCallback(
 | 
				
			||||||
 | 
					        (value: SetStateAction<ExpandedState>) => {
 | 
				
			||||||
 | 
					            if (setExpandedProp) {
 | 
				
			||||||
 | 
					                setExpandedProp(value);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                setExpandedInternal(value);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [setExpandedProp, setExpandedInternal]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [loading, setLoading] = useState<LoadingState>({});
 | 
				
			||||||
 | 
					    const [loadedChildren, setLoadedChildren] = useState<
 | 
				
			||||||
 | 
					        LoadedChildren<Type, Context>
 | 
				
			||||||
 | 
					    >({});
 | 
				
			||||||
 | 
					    const [hasMoreChildren, setHasMoreChildren] =
 | 
				
			||||||
 | 
					        useState<HasMoreChildrenState>({});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const mergeChildren = useCallback(
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					            staticChildren: TreeNode<Type, Context>[] = [],
 | 
				
			||||||
 | 
					            fetchedChildren: TreeNode<Type, Context>[] = []
 | 
				
			||||||
 | 
					        ) => {
 | 
				
			||||||
 | 
					            const fetchedChildrenIds = new Set(
 | 
				
			||||||
 | 
					                fetchedChildren.map((child) => child.id)
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            const uniqueStaticChildren = staticChildren.filter(
 | 
				
			||||||
 | 
					                (child) => !fetchedChildrenIds.has(child.id)
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            return [...uniqueStaticChildren, ...fetchedChildren];
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        []
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const toggleNode = useCallback(
 | 
				
			||||||
 | 
					        async (
 | 
				
			||||||
 | 
					            nodeId: string,
 | 
				
			||||||
 | 
					            nodeType: Type,
 | 
				
			||||||
 | 
					            nodeContext: Context[Type],
 | 
				
			||||||
 | 
					            staticChildren?: TreeNode<Type, Context>[]
 | 
				
			||||||
 | 
					        ) => {
 | 
				
			||||||
 | 
					            if (expanded[nodeId]) {
 | 
				
			||||||
 | 
					                // If we're collapsing, just update expanded state
 | 
				
			||||||
 | 
					                setExpanded((prev) => ({ ...prev, [nodeId]: false }));
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Get any previously fetched children
 | 
				
			||||||
 | 
					            const previouslyFetchedChildren = loadedChildren[nodeId] || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // If we have static children, merge them with any previously fetched children
 | 
				
			||||||
 | 
					            if (staticChildren?.length) {
 | 
				
			||||||
 | 
					                const mergedChildren = mergeChildren(
 | 
				
			||||||
 | 
					                    staticChildren,
 | 
				
			||||||
 | 
					                    previouslyFetchedChildren
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					                setLoadedChildren((prev) => ({
 | 
				
			||||||
 | 
					                    ...prev,
 | 
				
			||||||
 | 
					                    [nodeId]: mergedChildren,
 | 
				
			||||||
 | 
					                }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Only show "more loading" if we haven't fetched children before
 | 
				
			||||||
 | 
					                setHasMoreChildren((prev) => ({
 | 
				
			||||||
 | 
					                    ...prev,
 | 
				
			||||||
 | 
					                    [nodeId]: !previouslyFetchedChildren.length,
 | 
				
			||||||
 | 
					                }));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Set expanded state immediately to show static/previously fetched children
 | 
				
			||||||
 | 
					            setExpanded((prev) => ({ ...prev, [nodeId]: true }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // If we haven't loaded dynamic children yet
 | 
				
			||||||
 | 
					            if (!previouslyFetchedChildren.length) {
 | 
				
			||||||
 | 
					                setLoading((prev) => ({ ...prev, [nodeId]: true }));
 | 
				
			||||||
 | 
					                try {
 | 
				
			||||||
 | 
					                    const fetchedChildren = await fetchChildren?.(
 | 
				
			||||||
 | 
					                        nodeId,
 | 
				
			||||||
 | 
					                        nodeType,
 | 
				
			||||||
 | 
					                        nodeContext
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    // Merge static and newly fetched children
 | 
				
			||||||
 | 
					                    const allChildren = mergeChildren(
 | 
				
			||||||
 | 
					                        staticChildren || [],
 | 
				
			||||||
 | 
					                        fetchedChildren
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    setLoadedChildren((prev) => ({
 | 
				
			||||||
 | 
					                        ...prev,
 | 
				
			||||||
 | 
					                        [nodeId]: allChildren,
 | 
				
			||||||
 | 
					                    }));
 | 
				
			||||||
 | 
					                    setHasMoreChildren((prev) => ({
 | 
				
			||||||
 | 
					                        ...prev,
 | 
				
			||||||
 | 
					                        [nodeId]: false,
 | 
				
			||||||
 | 
					                    }));
 | 
				
			||||||
 | 
					                } catch (error) {
 | 
				
			||||||
 | 
					                    console.error('Error loading children:', error);
 | 
				
			||||||
 | 
					                } finally {
 | 
				
			||||||
 | 
					                    setLoading((prev) => ({ ...prev, [nodeId]: false }));
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [expanded, loadedChildren, fetchChildren, mergeChildren, setExpanded]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        expanded,
 | 
				
			||||||
 | 
					        loading,
 | 
				
			||||||
 | 
					        loadedChildren,
 | 
				
			||||||
 | 
					        hasMoreChildren,
 | 
				
			||||||
 | 
					        toggleNode,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										26
									
								
								src/context/canvas-context/canvas-context.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,26 @@
 | 
				
			|||||||
 | 
					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>;
 | 
				
			||||||
 | 
					    setShowFilter: React.Dispatch<React.SetStateAction<boolean>>;
 | 
				
			||||||
 | 
					    showFilter: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const canvasContext = createContext<CanvasContext>({
 | 
				
			||||||
 | 
					    reorderTables: emptyFn,
 | 
				
			||||||
 | 
					    fitView: emptyFn,
 | 
				
			||||||
 | 
					    setOverlapGraph: emptyFn,
 | 
				
			||||||
 | 
					    overlapGraph: createGraph(),
 | 
				
			||||||
 | 
					    setShowFilter: emptyFn,
 | 
				
			||||||
 | 
					    showFilter: false,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										89
									
								
								src/context/canvas-context/canvas-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,89 @@
 | 
				
			|||||||
 | 
					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 [showFilter, setShowFilter] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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,
 | 
				
			||||||
 | 
					                setShowFilter,
 | 
				
			||||||
 | 
					                showFilter,
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            {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,63 @@ 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>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Filters
 | 
				
			||||||
 | 
					    hiddenTableIds?: string[];
 | 
				
			||||||
 | 
					    addHiddenTableId: (tableId: string) => Promise<void>;
 | 
				
			||||||
 | 
					    removeHiddenTableId: (tableId: string) => Promise<void>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const chartDBContext = createContext<ChartDBContext>({
 | 
					export const chartDBContext = createContext<ChartDBContext>({
 | 
				
			||||||
@@ -230,6 +291,8 @@ export const chartDBContext = createContext<ChartDBContext>({
 | 
				
			|||||||
    tables: [],
 | 
					    tables: [],
 | 
				
			||||||
    relationships: [],
 | 
					    relationships: [],
 | 
				
			||||||
    dependencies: [],
 | 
					    dependencies: [],
 | 
				
			||||||
 | 
					    areas: [],
 | 
				
			||||||
 | 
					    customTypes: [],
 | 
				
			||||||
    schemas: [],
 | 
					    schemas: [],
 | 
				
			||||||
    filteredSchemas: [],
 | 
					    filteredSchemas: [],
 | 
				
			||||||
    filterSchemas: emptyFn,
 | 
					    filterSchemas: emptyFn,
 | 
				
			||||||
@@ -296,4 +359,27 @@ 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,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Filters
 | 
				
			||||||
 | 
					    hiddenTableIds: [],
 | 
				
			||||||
 | 
					    addHiddenTableId: emptyFn,
 | 
				
			||||||
 | 
					    removeHiddenTableId: emptyFn,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import React, { useCallback, useMemo, useState } from 'react';
 | 
					import React, { useCallback, useEffect, useMemo, useState } from 'react';
 | 
				
			||||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
					import type { DBTable } from '@/lib/domain/db-table';
 | 
				
			||||||
import { deepCopy, generateId } from '@/lib/utils';
 | 
					import { deepCopy, generateId } from '@/lib/utils';
 | 
				
			||||||
import { randomColor } from '@/lib/colors';
 | 
					import { randomColor } from '@/lib/colors';
 | 
				
			||||||
@@ -21,7 +21,15 @@ 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';
 | 
				
			||||||
 | 
					import { useConfig } from '@/hooks/use-config';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ChartDBProviderProps {
 | 
					export interface ChartDBProviderProps {
 | 
				
			||||||
    diagram?: Diagram;
 | 
					    diagram?: Diagram;
 | 
				
			||||||
@@ -30,12 +38,18 @@ 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();
 | 
				
			||||||
    const { addUndoAction, resetRedoStack, resetUndoStack } =
 | 
					    const { addUndoAction, resetRedoStack, resetUndoStack } =
 | 
				
			||||||
        useRedoUndoStack();
 | 
					        useRedoUndoStack();
 | 
				
			||||||
 | 
					    const {
 | 
				
			||||||
 | 
					        getHiddenTablesForDiagram,
 | 
				
			||||||
 | 
					        hideTableForDiagram,
 | 
				
			||||||
 | 
					        unhideTableForDiagram,
 | 
				
			||||||
 | 
					    } = useConfig();
 | 
				
			||||||
    const [diagramId, setDiagramId] = useState('');
 | 
					    const [diagramId, setDiagramId] = useState('');
 | 
				
			||||||
    const [diagramName, setDiagramName] = useState('');
 | 
					    const [diagramName, setDiagramName] = useState('');
 | 
				
			||||||
    const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
 | 
					    const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
 | 
				
			||||||
@@ -53,9 +67,46 @@ 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 [hiddenTableIds, setHiddenTableIds] = useState<string[]>([]);
 | 
				
			||||||
 | 
					    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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Sync hiddenTableIds with config
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        if (diagramId) {
 | 
				
			||||||
 | 
					            const hiddenTables = getHiddenTablesForDiagram(diagramId);
 | 
				
			||||||
 | 
					            setHiddenTableIds(hiddenTables);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }, [diagramId, getHiddenTablesForDiagram]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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 +176,8 @@ export const ChartDBProvider: React.FC<
 | 
				
			|||||||
            tables,
 | 
					            tables,
 | 
				
			||||||
            relationships,
 | 
					            relationships,
 | 
				
			||||||
            dependencies,
 | 
					            dependencies,
 | 
				
			||||||
 | 
					            areas,
 | 
				
			||||||
 | 
					            customTypes,
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
            diagramId,
 | 
					            diagramId,
 | 
				
			||||||
@@ -134,6 +187,8 @@ export const ChartDBProvider: React.FC<
 | 
				
			|||||||
            tables,
 | 
					            tables,
 | 
				
			||||||
            relationships,
 | 
					            relationships,
 | 
				
			||||||
            dependencies,
 | 
					            dependencies,
 | 
				
			||||||
 | 
					            areas,
 | 
				
			||||||
 | 
					            customTypes,
 | 
				
			||||||
            diagramCreatedAt,
 | 
					            diagramCreatedAt,
 | 
				
			||||||
            diagramUpdatedAt,
 | 
					            diagramUpdatedAt,
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
@@ -145,6 +200,8 @@ export const ChartDBProvider: React.FC<
 | 
				
			|||||||
            setTables([]);
 | 
					            setTables([]);
 | 
				
			||||||
            setRelationships([]);
 | 
					            setRelationships([]);
 | 
				
			||||||
            setDependencies([]);
 | 
					            setDependencies([]);
 | 
				
			||||||
 | 
					            setAreas([]);
 | 
				
			||||||
 | 
					            setCustomTypes([]);
 | 
				
			||||||
            setDiagramUpdatedAt(updatedAt);
 | 
					            setDiagramUpdatedAt(updatedAt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            resetRedoStack();
 | 
					            resetRedoStack();
 | 
				
			||||||
@@ -155,6 +212,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 +226,8 @@ export const ChartDBProvider: React.FC<
 | 
				
			|||||||
            setTables([]);
 | 
					            setTables([]);
 | 
				
			||||||
            setRelationships([]);
 | 
					            setRelationships([]);
 | 
				
			||||||
            setDependencies([]);
 | 
					            setDependencies([]);
 | 
				
			||||||
 | 
					            setAreas([]);
 | 
				
			||||||
 | 
					            setCustomTypes([]);
 | 
				
			||||||
            resetRedoStack();
 | 
					            resetRedoStack();
 | 
				
			||||||
            resetUndoStack();
 | 
					            resetUndoStack();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -175,6 +236,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]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -256,22 +319,27 @@ export const ChartDBProvider: React.FC<
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const addTables: ChartDBContext['addTables'] = useCallback(
 | 
					    const addTables: ChartDBContext['addTables'] = useCallback(
 | 
				
			||||||
        async (tables: DBTable[], options = { updateHistory: true }) => {
 | 
					        async (tablesToAdd: DBTable[], options = { updateHistory: true }) => {
 | 
				
			||||||
            setTables((currentTables) => [...currentTables, ...tables]);
 | 
					            setTables((currentTables) => [...currentTables, ...tablesToAdd]);
 | 
				
			||||||
            const updatedAt = new Date();
 | 
					            const updatedAt = new Date();
 | 
				
			||||||
            setDiagramUpdatedAt(updatedAt);
 | 
					            setDiagramUpdatedAt(updatedAt);
 | 
				
			||||||
            await Promise.all([
 | 
					            await Promise.all([
 | 
				
			||||||
                db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
 | 
					                db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
 | 
				
			||||||
                ...tables.map((table) => db.addTable({ diagramId, table })),
 | 
					                ...tablesToAdd.map((table) =>
 | 
				
			||||||
 | 
					                    db.addTable({ diagramId, table })
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
            ]);
 | 
					            ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            events.emit({ action: 'add_tables', data: { tables } });
 | 
					            events.emit({
 | 
				
			||||||
 | 
					                action: 'add_tables',
 | 
				
			||||||
 | 
					                data: { tables: tablesToAdd },
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (options.updateHistory) {
 | 
					            if (options.updateHistory) {
 | 
				
			||||||
                addUndoAction({
 | 
					                addUndoAction({
 | 
				
			||||||
                    action: 'addTables',
 | 
					                    action: 'addTables',
 | 
				
			||||||
                    redoData: { tables },
 | 
					                    redoData: { tables: tablesToAdd },
 | 
				
			||||||
                    undoData: { tableIds: tables.map((t) => t.id) },
 | 
					                    undoData: { tableIds: tablesToAdd.map((t) => t.id) },
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
                resetRedoStack();
 | 
					                resetRedoStack();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -730,13 +798,23 @@ export const ChartDBProvider: React.FC<
 | 
				
			|||||||
            options = { updateHistory: true }
 | 
					            options = { updateHistory: true }
 | 
				
			||||||
        ) => {
 | 
					        ) => {
 | 
				
			||||||
            const fields = getTable(tableId)?.fields ?? [];
 | 
					            const fields = getTable(tableId)?.fields ?? [];
 | 
				
			||||||
            setTables((tables) =>
 | 
					            setTables((tables) => {
 | 
				
			||||||
                tables.map((table) =>
 | 
					                return tables.map((table) => {
 | 
				
			||||||
                    table.id === tableId
 | 
					                    if (table.id === tableId) {
 | 
				
			||||||
                        ? { ...table, fields: [...table.fields, field] }
 | 
					                        db.updateTable({
 | 
				
			||||||
                        : table
 | 
					                            id: tableId,
 | 
				
			||||||
                )
 | 
					                            attributes: {
 | 
				
			||||||
            );
 | 
					                                ...table,
 | 
				
			||||||
 | 
					                                fields: [...table.fields, field],
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        return { ...table, fields: [...table.fields, field] };
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    return table;
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            events.emit({
 | 
					            events.emit({
 | 
				
			||||||
                action: 'add_field',
 | 
					                action: 'add_field',
 | 
				
			||||||
@@ -757,13 +835,6 @@ export const ChartDBProvider: React.FC<
 | 
				
			|||||||
            setDiagramUpdatedAt(updatedAt);
 | 
					            setDiagramUpdatedAt(updatedAt);
 | 
				
			||||||
            await Promise.all([
 | 
					            await Promise.all([
 | 
				
			||||||
                db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
 | 
					                db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
 | 
				
			||||||
                db.updateTable({
 | 
					 | 
				
			||||||
                    id: tableId,
 | 
					 | 
				
			||||||
                    attributes: {
 | 
					 | 
				
			||||||
                        ...table,
 | 
					 | 
				
			||||||
                        fields: [...table.fields, field],
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                }),
 | 
					 | 
				
			||||||
            ]);
 | 
					            ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (options.updateHistory) {
 | 
					            if (options.updateHistory) {
 | 
				
			||||||
@@ -1336,6 +1407,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 +1541,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 +1556,8 @@ export const ChartDBProvider: React.FC<
 | 
				
			|||||||
                setTables,
 | 
					                setTables,
 | 
				
			||||||
                setRelationships,
 | 
					                setRelationships,
 | 
				
			||||||
                setDependencies,
 | 
					                setDependencies,
 | 
				
			||||||
 | 
					                setAreas,
 | 
				
			||||||
 | 
					                setCustomTypes,
 | 
				
			||||||
                setDiagramCreatedAt,
 | 
					                setDiagramCreatedAt,
 | 
				
			||||||
                setDiagramUpdatedAt,
 | 
					                setDiagramUpdatedAt,
 | 
				
			||||||
                events,
 | 
					                events,
 | 
				
			||||||
@@ -1371,6 +1570,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 +1583,173 @@ 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,
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const addHiddenTableId: ChartDBContext['addHiddenTableId'] = useCallback(
 | 
				
			||||||
 | 
					        async (tableId: string) => {
 | 
				
			||||||
 | 
					            if (!hiddenTableIds.includes(tableId)) {
 | 
				
			||||||
 | 
					                setHiddenTableIds((prev) => [...prev, tableId]);
 | 
				
			||||||
 | 
					                await hideTableForDiagram(diagramId, tableId);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [hiddenTableIds, diagramId, hideTableForDiagram]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const removeHiddenTableId: ChartDBContext['removeHiddenTableId'] =
 | 
				
			||||||
 | 
					        useCallback(
 | 
				
			||||||
 | 
					            async (tableId: string) => {
 | 
				
			||||||
 | 
					                if (hiddenTableIds.includes(tableId)) {
 | 
				
			||||||
 | 
					                    setHiddenTableIds((prev) =>
 | 
				
			||||||
 | 
					                        prev.filter((id) => id !== tableId)
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    await unhideTableForDiagram(diagramId, tableId);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            [hiddenTableIds, diagramId, unhideTableForDiagram]
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <chartDBContext.Provider
 | 
					        <chartDBContext.Provider
 | 
				
			||||||
            value={{
 | 
					            value={{
 | 
				
			||||||
@@ -1391,6 +1759,7 @@ export const ChartDBProvider: React.FC<
 | 
				
			|||||||
                tables,
 | 
					                tables,
 | 
				
			||||||
                relationships,
 | 
					                relationships,
 | 
				
			||||||
                dependencies,
 | 
					                dependencies,
 | 
				
			||||||
 | 
					                areas,
 | 
				
			||||||
                currentDiagram,
 | 
					                currentDiagram,
 | 
				
			||||||
                schemas,
 | 
					                schemas,
 | 
				
			||||||
                filteredSchemas,
 | 
					                filteredSchemas,
 | 
				
			||||||
@@ -1438,6 +1807,24 @@ 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,
 | 
				
			||||||
 | 
					                hiddenTableIds,
 | 
				
			||||||
 | 
					                addHiddenTableId,
 | 
				
			||||||
 | 
					                removeHiddenTableId,
 | 
				
			||||||
            }}
 | 
					            }}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
            {children}
 | 
					            {children}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,10 +4,27 @@ 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>;
 | 
				
			||||||
 | 
					    getHiddenTablesForDiagram: (diagramId: string) => string[];
 | 
				
			||||||
 | 
					    setHiddenTablesForDiagram: (
 | 
				
			||||||
 | 
					        diagramId: string,
 | 
				
			||||||
 | 
					        hiddenTableIds: string[]
 | 
				
			||||||
 | 
					    ) => Promise<void>;
 | 
				
			||||||
 | 
					    hideTableForDiagram: (diagramId: string, tableId: string) => Promise<void>;
 | 
				
			||||||
 | 
					    unhideTableForDiagram: (
 | 
				
			||||||
 | 
					        diagramId: string,
 | 
				
			||||||
 | 
					        tableId: string
 | 
				
			||||||
 | 
					    ) => Promise<void>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ConfigContext = createContext<ConfigContext>({
 | 
					export const ConfigContext = createContext<ConfigContext>({
 | 
				
			||||||
    config: undefined,
 | 
					    config: undefined,
 | 
				
			||||||
    updateConfig: emptyFn,
 | 
					    updateConfig: emptyFn,
 | 
				
			||||||
 | 
					    getHiddenTablesForDiagram: () => [],
 | 
				
			||||||
 | 
					    setHiddenTablesForDiagram: emptyFn,
 | 
				
			||||||
 | 
					    hideTableForDiagram: emptyFn,
 | 
				
			||||||
 | 
					    unhideTableForDiagram: emptyFn,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,19 +19,111 @@ 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;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getHiddenTablesForDiagram = (diagramId: string): string[] => {
 | 
				
			||||||
 | 
					        return config?.hiddenTablesByDiagram?.[diagramId] ?? [];
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const setHiddenTablesForDiagram = async (
 | 
				
			||||||
 | 
					        diagramId: string,
 | 
				
			||||||
 | 
					        hiddenTableIds: string[]
 | 
				
			||||||
 | 
					    ): Promise<void> => {
 | 
				
			||||||
 | 
					        return updateConfig({
 | 
				
			||||||
 | 
					            updateFn: (currentConfig) => ({
 | 
				
			||||||
 | 
					                ...currentConfig,
 | 
				
			||||||
 | 
					                hiddenTablesByDiagram: {
 | 
				
			||||||
 | 
					                    ...currentConfig.hiddenTablesByDiagram,
 | 
				
			||||||
 | 
					                    [diagramId]: hiddenTableIds,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const hideTableForDiagram = async (
 | 
				
			||||||
 | 
					        diagramId: string,
 | 
				
			||||||
 | 
					        tableId: string
 | 
				
			||||||
 | 
					    ): Promise<void> => {
 | 
				
			||||||
 | 
					        return updateConfig({
 | 
				
			||||||
 | 
					            updateFn: (currentConfig) => {
 | 
				
			||||||
 | 
					                const currentHiddenTables =
 | 
				
			||||||
 | 
					                    currentConfig.hiddenTablesByDiagram?.[diagramId] ?? [];
 | 
				
			||||||
 | 
					                if (currentHiddenTables.includes(tableId)) {
 | 
				
			||||||
 | 
					                    return currentConfig; // Already hidden, no change needed
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
 | 
					                    ...currentConfig,
 | 
				
			||||||
 | 
					                    hiddenTablesByDiagram: {
 | 
				
			||||||
 | 
					                        ...currentConfig.hiddenTablesByDiagram,
 | 
				
			||||||
 | 
					                        [diagramId]: [...currentHiddenTables, tableId],
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const unhideTableForDiagram = async (
 | 
				
			||||||
 | 
					        diagramId: string,
 | 
				
			||||||
 | 
					        tableId: string
 | 
				
			||||||
 | 
					    ): Promise<void> => {
 | 
				
			||||||
 | 
					        return updateConfig({
 | 
				
			||||||
 | 
					            updateFn: (currentConfig) => {
 | 
				
			||||||
 | 
					                const currentHiddenTables =
 | 
				
			||||||
 | 
					                    currentConfig.hiddenTablesByDiagram?.[diagramId] ?? [];
 | 
				
			||||||
 | 
					                const filteredTables = currentHiddenTables.filter(
 | 
				
			||||||
 | 
					                    (id) => id !== tableId
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (filteredTables.length === currentHiddenTables.length) {
 | 
				
			||||||
 | 
					                    return currentConfig; // Not hidden, no change needed
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
 | 
					                    ...currentConfig,
 | 
				
			||||||
 | 
					                    hiddenTablesByDiagram: {
 | 
				
			||||||
 | 
					                        ...currentConfig.hiddenTablesByDiagram,
 | 
				
			||||||
 | 
					                        [diagramId]: filteredTables,
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <ConfigContext.Provider value={{ config, updateConfig }}>
 | 
					        <ConfigContext.Provider
 | 
				
			||||||
 | 
					            value={{
 | 
				
			||||||
 | 
					                config,
 | 
				
			||||||
 | 
					                updateConfig,
 | 
				
			||||||
 | 
					                getHiddenTablesForDiagram,
 | 
				
			||||||
 | 
					                setHiddenTablesForDiagram,
 | 
				
			||||||
 | 
					                hideTableForDiagram,
 | 
				
			||||||
 | 
					                unhideTableForDiagram,
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
            {children}
 | 
					            {children}
 | 
				
			||||||
        </ConfigContext.Provider>
 | 
					        </ConfigContext.Provider>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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);
 | 
				
			||||||
							
								
								
									
										376
									
								
								src/context/diff-context/diff-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,376 @@
 | 
				
			|||||||
 | 
					import React, { useCallback } from 'react';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					    DiffCalculatedData,
 | 
				
			||||||
 | 
					    DiffContext,
 | 
				
			||||||
 | 
					    DiffEvent,
 | 
				
			||||||
 | 
					} from './diff-context';
 | 
				
			||||||
 | 
					import { diffContext } from './diff-context';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    generateDiff,
 | 
				
			||||||
 | 
					    getDiffMapKey,
 | 
				
			||||||
 | 
					} from '@/lib/domain/diff/diff-check/diff-check';
 | 
				
			||||||
 | 
					import type { Diagram } from '@/lib/domain/diagram';
 | 
				
			||||||
 | 
					import { useEventEmitter } from 'ahooks';
 | 
				
			||||||
 | 
					import type { DBField } from '@/lib/domain/db-field';
 | 
				
			||||||
 | 
					import type { DataType } from '@/lib/data/data-types/data-types';
 | 
				
			||||||
 | 
					import type { DBRelationship } from '@/lib/domain/db-relationship';
 | 
				
			||||||
 | 
					import type { ChartDBDiff, DiffMap } from '@/lib/domain/diff/diff';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			||||||
 | 
					    children,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					    const [newDiagram, setNewDiagram] = React.useState<Diagram | null>(null);
 | 
				
			||||||
 | 
					    const [originalDiagram, setOriginalDiagram] =
 | 
				
			||||||
 | 
					        React.useState<Diagram | null>(null);
 | 
				
			||||||
 | 
					    const [diffMap, setDiffMap] = React.useState<DiffMap>(
 | 
				
			||||||
 | 
					        new Map<string, ChartDBDiff>()
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const [tablesChanged, setTablesChanged] = React.useState<
 | 
				
			||||||
 | 
					        Map<string, boolean>
 | 
				
			||||||
 | 
					    >(new Map<string, boolean>());
 | 
				
			||||||
 | 
					    const [fieldsChanged, setFieldsChanged] = React.useState<
 | 
				
			||||||
 | 
					        Map<string, boolean>
 | 
				
			||||||
 | 
					    >(new Map<string, boolean>());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const events = useEventEmitter<DiffEvent>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const generateNewFieldsMap = useCallback(
 | 
				
			||||||
 | 
					        ({
 | 
				
			||||||
 | 
					            diffMap,
 | 
				
			||||||
 | 
					            newDiagram,
 | 
				
			||||||
 | 
					        }: {
 | 
				
			||||||
 | 
					            diffMap: DiffMap;
 | 
				
			||||||
 | 
					            newDiagram: Diagram;
 | 
				
			||||||
 | 
					        }) => {
 | 
				
			||||||
 | 
					            const newFieldsMap = new Map<string, DBField[]>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            diffMap.forEach((diff) => {
 | 
				
			||||||
 | 
					                if (diff.object === 'field' && diff.type === 'added') {
 | 
				
			||||||
 | 
					                    const field = newDiagram?.tables
 | 
				
			||||||
 | 
					                        ?.find((table) => table.id === diff.tableId)
 | 
				
			||||||
 | 
					                        ?.fields.find((f) => f.id === diff.newField.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (field) {
 | 
				
			||||||
 | 
					                        newFieldsMap.set(diff.tableId, [
 | 
				
			||||||
 | 
					                            ...(newFieldsMap.get(diff.tableId) ?? []),
 | 
				
			||||||
 | 
					                            field,
 | 
				
			||||||
 | 
					                        ]);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return newFieldsMap;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        []
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const findNewRelationships = useCallback(
 | 
				
			||||||
 | 
					        ({
 | 
				
			||||||
 | 
					            diffMap,
 | 
				
			||||||
 | 
					            newDiagram,
 | 
				
			||||||
 | 
					        }: {
 | 
				
			||||||
 | 
					            diffMap: DiffMap;
 | 
				
			||||||
 | 
					            newDiagram: Diagram;
 | 
				
			||||||
 | 
					        }) => {
 | 
				
			||||||
 | 
					            const relationships: DBRelationship[] = [];
 | 
				
			||||||
 | 
					            diffMap.forEach((diff) => {
 | 
				
			||||||
 | 
					                if (diff.object === 'relationship' && diff.type === 'added') {
 | 
				
			||||||
 | 
					                    const relationship = newDiagram?.relationships?.find(
 | 
				
			||||||
 | 
					                        (rel) => rel.id === diff.newRelationship.id
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (relationship) {
 | 
				
			||||||
 | 
					                        relationships.push(relationship);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return relationships;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        []
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const generateDiffCalculatedData = useCallback(
 | 
				
			||||||
 | 
					        ({
 | 
				
			||||||
 | 
					            newDiagram,
 | 
				
			||||||
 | 
					            diffMap,
 | 
				
			||||||
 | 
					        }: {
 | 
				
			||||||
 | 
					            newDiagram: Diagram;
 | 
				
			||||||
 | 
					            diffMap: DiffMap;
 | 
				
			||||||
 | 
					        }): DiffCalculatedData => {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                tablesAdded:
 | 
				
			||||||
 | 
					                    newDiagram?.tables?.filter((table) => {
 | 
				
			||||||
 | 
					                        const tableKey = getDiffMapKey({
 | 
				
			||||||
 | 
					                            diffObject: 'table',
 | 
				
			||||||
 | 
					                            objectId: table.id,
 | 
				
			||||||
 | 
					                        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        return (
 | 
				
			||||||
 | 
					                            diffMap.has(tableKey) &&
 | 
				
			||||||
 | 
					                            diffMap.get(tableKey)?.type === 'added'
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                    }) ?? [],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                fieldsAdded: generateNewFieldsMap({
 | 
				
			||||||
 | 
					                    diffMap: diffMap,
 | 
				
			||||||
 | 
					                    newDiagram: newDiagram,
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
 | 
					                relationshipsAdded: findNewRelationships({
 | 
				
			||||||
 | 
					                    diffMap: diffMap,
 | 
				
			||||||
 | 
					                    newDiagram: newDiagram,
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [findNewRelationships, generateNewFieldsMap]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const calculateDiff: DiffContext['calculateDiff'] = useCallback(
 | 
				
			||||||
 | 
					        ({ diagram, newDiagram: newDiagramArg }) => {
 | 
				
			||||||
 | 
					            const {
 | 
				
			||||||
 | 
					                diffMap: newDiffs,
 | 
				
			||||||
 | 
					                changedTables: newChangedTables,
 | 
				
			||||||
 | 
					                changedFields: newChangedFields,
 | 
				
			||||||
 | 
					            } = generateDiff({ diagram, newDiagram: newDiagramArg });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            setDiffMap(newDiffs);
 | 
				
			||||||
 | 
					            setTablesChanged(newChangedTables);
 | 
				
			||||||
 | 
					            setFieldsChanged(newChangedFields);
 | 
				
			||||||
 | 
					            setNewDiagram(newDiagramArg);
 | 
				
			||||||
 | 
					            setOriginalDiagram(diagram);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            events.emit({
 | 
				
			||||||
 | 
					                action: 'diff_calculated',
 | 
				
			||||||
 | 
					                data: generateDiffCalculatedData({
 | 
				
			||||||
 | 
					                    diffMap: newDiffs,
 | 
				
			||||||
 | 
					                    newDiagram: newDiagramArg,
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [setDiffMap, events, generateDiffCalculatedData]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getTableNewName = useCallback<DiffContext['getTableNewName']>(
 | 
				
			||||||
 | 
					        ({ tableId }) => {
 | 
				
			||||||
 | 
					            const tableNameKey = getDiffMapKey({
 | 
				
			||||||
 | 
					                diffObject: 'table',
 | 
				
			||||||
 | 
					                objectId: tableId,
 | 
				
			||||||
 | 
					                attribute: 'name',
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (diffMap.has(tableNameKey)) {
 | 
				
			||||||
 | 
					                const diff = diffMap.get(tableNameKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (diff?.type === 'changed') {
 | 
				
			||||||
 | 
					                    return diff.newValue as string;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [diffMap]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getTableNewColor = useCallback<DiffContext['getTableNewColor']>(
 | 
				
			||||||
 | 
					        ({ tableId }) => {
 | 
				
			||||||
 | 
					            const tableColorKey = getDiffMapKey({
 | 
				
			||||||
 | 
					                diffObject: 'table',
 | 
				
			||||||
 | 
					                objectId: tableId,
 | 
				
			||||||
 | 
					                attribute: 'color',
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (diffMap.has(tableColorKey)) {
 | 
				
			||||||
 | 
					                const diff = diffMap.get(tableColorKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (diff?.type === 'changed') {
 | 
				
			||||||
 | 
					                    return diff.newValue as string;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [diffMap]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const checkIfTableHasChange = useCallback<
 | 
				
			||||||
 | 
					        DiffContext['checkIfTableHasChange']
 | 
				
			||||||
 | 
					    >(({ tableId }) => tablesChanged.get(tableId) ?? false, [tablesChanged]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const checkIfNewTable = useCallback<DiffContext['checkIfNewTable']>(
 | 
				
			||||||
 | 
					        ({ tableId }) => {
 | 
				
			||||||
 | 
					            const tableKey = getDiffMapKey({
 | 
				
			||||||
 | 
					                diffObject: 'table',
 | 
				
			||||||
 | 
					                objectId: tableId,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                diffMap.has(tableKey) && diffMap.get(tableKey)?.type === 'added'
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [diffMap]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const checkIfTableRemoved = useCallback<DiffContext['checkIfTableRemoved']>(
 | 
				
			||||||
 | 
					        ({ tableId }) => {
 | 
				
			||||||
 | 
					            const tableKey = getDiffMapKey({
 | 
				
			||||||
 | 
					                diffObject: 'table',
 | 
				
			||||||
 | 
					                objectId: tableId,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                diffMap.has(tableKey) &&
 | 
				
			||||||
 | 
					                diffMap.get(tableKey)?.type === 'removed'
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [diffMap]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const checkIfFieldHasChange = useCallback<
 | 
				
			||||||
 | 
					        DiffContext['checkIfFieldHasChange']
 | 
				
			||||||
 | 
					    >(
 | 
				
			||||||
 | 
					        ({ fieldId }) => {
 | 
				
			||||||
 | 
					            return fieldsChanged.get(fieldId) ?? false;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [fieldsChanged]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const checkIfFieldRemoved = useCallback<DiffContext['checkIfFieldRemoved']>(
 | 
				
			||||||
 | 
					        ({ fieldId }) => {
 | 
				
			||||||
 | 
					            const fieldKey = getDiffMapKey({
 | 
				
			||||||
 | 
					                diffObject: 'field',
 | 
				
			||||||
 | 
					                objectId: fieldId,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                diffMap.has(fieldKey) &&
 | 
				
			||||||
 | 
					                diffMap.get(fieldKey)?.type === 'removed'
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [diffMap]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const checkIfNewField = useCallback<DiffContext['checkIfNewField']>(
 | 
				
			||||||
 | 
					        ({ fieldId }) => {
 | 
				
			||||||
 | 
					            const fieldKey = getDiffMapKey({
 | 
				
			||||||
 | 
					                diffObject: 'field',
 | 
				
			||||||
 | 
					                objectId: fieldId,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                diffMap.has(fieldKey) && diffMap.get(fieldKey)?.type === 'added'
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [diffMap]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getFieldNewName = useCallback<DiffContext['getFieldNewName']>(
 | 
				
			||||||
 | 
					        ({ fieldId }) => {
 | 
				
			||||||
 | 
					            const fieldKey = getDiffMapKey({
 | 
				
			||||||
 | 
					                diffObject: 'field',
 | 
				
			||||||
 | 
					                objectId: fieldId,
 | 
				
			||||||
 | 
					                attribute: 'name',
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (diffMap.has(fieldKey)) {
 | 
				
			||||||
 | 
					                const diff = diffMap.get(fieldKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (diff?.type === 'changed') {
 | 
				
			||||||
 | 
					                    return diff.newValue as string;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [diffMap]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getFieldNewType = useCallback<DiffContext['getFieldNewType']>(
 | 
				
			||||||
 | 
					        ({ fieldId }) => {
 | 
				
			||||||
 | 
					            const fieldKey = getDiffMapKey({
 | 
				
			||||||
 | 
					                diffObject: 'field',
 | 
				
			||||||
 | 
					                objectId: fieldId,
 | 
				
			||||||
 | 
					                attribute: 'type',
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (diffMap.has(fieldKey)) {
 | 
				
			||||||
 | 
					                const diff = diffMap.get(fieldKey);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (diff?.type === 'changed') {
 | 
				
			||||||
 | 
					                    return diff.newValue as DataType;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [diffMap]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const checkIfNewRelationship = useCallback<
 | 
				
			||||||
 | 
					        DiffContext['checkIfNewRelationship']
 | 
				
			||||||
 | 
					    >(
 | 
				
			||||||
 | 
					        ({ relationshipId }) => {
 | 
				
			||||||
 | 
					            const relationshipKey = getDiffMapKey({
 | 
				
			||||||
 | 
					                diffObject: 'relationship',
 | 
				
			||||||
 | 
					                objectId: relationshipId,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                diffMap.has(relationshipKey) &&
 | 
				
			||||||
 | 
					                diffMap.get(relationshipKey)?.type === 'added'
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [diffMap]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const checkIfRelationshipRemoved = useCallback<
 | 
				
			||||||
 | 
					        DiffContext['checkIfRelationshipRemoved']
 | 
				
			||||||
 | 
					    >(
 | 
				
			||||||
 | 
					        ({ relationshipId }) => {
 | 
				
			||||||
 | 
					            const relationshipKey = getDiffMapKey({
 | 
				
			||||||
 | 
					                diffObject: 'relationship',
 | 
				
			||||||
 | 
					                objectId: relationshipId,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                diffMap.has(relationshipKey) &&
 | 
				
			||||||
 | 
					                diffMap.get(relationshipKey)?.type === 'removed'
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [diffMap]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <diffContext.Provider
 | 
				
			||||||
 | 
					            value={{
 | 
				
			||||||
 | 
					                newDiagram,
 | 
				
			||||||
 | 
					                originalDiagram,
 | 
				
			||||||
 | 
					                diffMap,
 | 
				
			||||||
 | 
					                hasDiff: diffMap.size > 0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                calculateDiff,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // table diff
 | 
				
			||||||
 | 
					                getTableNewName,
 | 
				
			||||||
 | 
					                checkIfNewTable,
 | 
				
			||||||
 | 
					                checkIfTableRemoved,
 | 
				
			||||||
 | 
					                checkIfTableHasChange,
 | 
				
			||||||
 | 
					                getTableNewColor,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // field diff
 | 
				
			||||||
 | 
					                checkIfFieldHasChange,
 | 
				
			||||||
 | 
					                checkIfFieldRemoved,
 | 
				
			||||||
 | 
					                checkIfNewField,
 | 
				
			||||||
 | 
					                getFieldNewName,
 | 
				
			||||||
 | 
					                getFieldNewType,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // relationship diff
 | 
				
			||||||
 | 
					                checkIfNewRelationship,
 | 
				
			||||||
 | 
					                checkIfRelationshipRemoved,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                events,
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					            {children}
 | 
				
			||||||
 | 
					        </diffContext.Provider>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										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,6 +123,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
                    defs.innerHTML = markerDefs.innerHTML;
 | 
					                    defs.innerHTML = markerDefs.innerHTML;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (includePatternBG) {
 | 
				
			||||||
                    const pattern = document.createElementNS(
 | 
					                    const pattern = document.createElementNS(
 | 
				
			||||||
                        'http://www.w3.org/2000/svg',
 | 
					                        'http://www.w3.org/2000/svg',
 | 
				
			||||||
                        'pattern'
 | 
					                        'pattern'
 | 
				
			||||||
@@ -122,22 +152,30 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                    pattern.appendChild(dot);
 | 
					                    pattern.appendChild(dot);
 | 
				
			||||||
                    defs.appendChild(pattern);
 | 
					                    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,15 +186,9 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
                );
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                try {
 | 
					                try {
 | 
				
			||||||
 | 
					                    // Handle SVG export differently
 | 
				
			||||||
 | 
					                    if (type === 'svg') {
 | 
				
			||||||
                        const dataUrl = await imageCreateFn(viewportElement, {
 | 
					                        const dataUrl = await imageCreateFn(viewportElement, {
 | 
				
			||||||
                        ...(type === 'jpeg' || type === 'png'
 | 
					 | 
				
			||||||
                            ? {
 | 
					 | 
				
			||||||
                                  backgroundColor:
 | 
					 | 
				
			||||||
                                      effectiveTheme === 'light'
 | 
					 | 
				
			||||||
                                          ? '#ffffff'
 | 
					 | 
				
			||||||
                                          : '#141414',
 | 
					 | 
				
			||||||
                              }
 | 
					 | 
				
			||||||
                            : {}),
 | 
					 | 
				
			||||||
                            width: reactFlowBounds.width,
 | 
					                            width: reactFlowBounds.width,
 | 
				
			||||||
                            height: reactFlowBounds.height,
 | 
					                            height: reactFlowBounds.height,
 | 
				
			||||||
                            style: {
 | 
					                            style: {
 | 
				
			||||||
@@ -168,8 +200,96 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
                            pixelRatio: scale,
 | 
					                            pixelRatio: scale,
 | 
				
			||||||
                            skipFonts: true,
 | 
					                            skipFonts: true,
 | 
				
			||||||
                        });
 | 
					                        });
 | 
				
			||||||
 | 
					 | 
				
			||||||
                        downloadImage(dataUrl, type);
 | 
					                        downloadImage(dataUrl, type);
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // 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();
 | 
				
			||||||
@@ -177,6 +297,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
            }, 0);
 | 
					            }, 0);
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
 | 
					            getBackgroundColor,
 | 
				
			||||||
            downloadImage,
 | 
					            downloadImage,
 | 
				
			||||||
            getViewport,
 | 
					            getViewport,
 | 
				
			||||||
            hideLoader,
 | 
					            hideLoader,
 | 
				
			||||||
@@ -184,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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,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,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,8 @@ export enum KeyboardShortcutAction {
 | 
				
			|||||||
    SAVE_DIAGRAM = 'save_diagram',
 | 
					    SAVE_DIAGRAM = 'save_diagram',
 | 
				
			||||||
    TOGGLE_SIDE_PANEL = 'toggle_side_panel',
 | 
					    TOGGLE_SIDE_PANEL = 'toggle_side_panel',
 | 
				
			||||||
    SHOW_ALL = 'show_all',
 | 
					    SHOW_ALL = 'show_all',
 | 
				
			||||||
 | 
					    TOGGLE_THEME = 'toggle_theme',
 | 
				
			||||||
 | 
					    TOGGLE_FILTER = 'toggle_filter',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface KeyboardShortcut {
 | 
					export interface KeyboardShortcut {
 | 
				
			||||||
@@ -63,6 +65,20 @@ export const keyboardShortcuts: Record<
 | 
				
			|||||||
        keyCombinationMac: 'meta+0',
 | 
					        keyCombinationMac: 'meta+0',
 | 
				
			||||||
        keyCombinationWin: 'ctrl+0',
 | 
					        keyCombinationWin: 'ctrl+0',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    [KeyboardShortcutAction.TOGGLE_THEME]: {
 | 
				
			||||||
 | 
					        action: KeyboardShortcutAction.TOGGLE_THEME,
 | 
				
			||||||
 | 
					        keyCombinationLabelMac: '⌘M',
 | 
				
			||||||
 | 
					        keyCombinationLabelWin: 'Ctrl+M',
 | 
				
			||||||
 | 
					        keyCombinationMac: 'meta+m',
 | 
				
			||||||
 | 
					        keyCombinationWin: 'ctrl+m',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [KeyboardShortcutAction.TOGGLE_FILTER]: {
 | 
				
			||||||
 | 
					        action: KeyboardShortcutAction.TOGGLE_FILTER,
 | 
				
			||||||
 | 
					        keyCombinationLabelMac: '⌘F',
 | 
				
			||||||
 | 
					        keyCombinationLabelWin: 'Ctrl+F',
 | 
				
			||||||
 | 
					        keyCombinationMac: 'meta+f',
 | 
				
			||||||
 | 
					        keyCombinationWin: 'ctrl+f',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface KeyboardShortcutForOS {
 | 
					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 =
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import React from 'react';
 | 
					import React, { useCallback, useMemo } from 'react';
 | 
				
			||||||
import type { StorageContext } from './storage-context';
 | 
					import type { StorageContext } from './storage-context';
 | 
				
			||||||
import { storageContext } from './storage-context';
 | 
					import { storageContext } from './storage-context';
 | 
				
			||||||
import Dexie, { type EntityTable } from 'dexie';
 | 
					import Dexie, { type EntityTable } from 'dexie';
 | 
				
			||||||
@@ -8,11 +8,14 @@ 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,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
    const db = new Dexie('ChartDB') as Dexie & {
 | 
					    const db = useMemo(() => {
 | 
				
			||||||
 | 
					        const dexieDB = new Dexie('ChartDB') as Dexie & {
 | 
				
			||||||
            diagrams: EntityTable<
 | 
					            diagrams: EntityTable<
 | 
				
			||||||
                Diagram,
 | 
					                Diagram,
 | 
				
			||||||
                'id' // primary key "id" (for the typings only)
 | 
					                'id' // primary key "id" (for the typings only)
 | 
				
			||||||
@@ -29,6 +32,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)
 | 
				
			||||||
@@ -36,7 +47,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Schema declaration:
 | 
					        // Schema declaration:
 | 
				
			||||||
    db.version(1).stores({
 | 
					        dexieDB.version(1).stores({
 | 
				
			||||||
            diagrams: '++id, name, databaseType, createdAt, updatedAt',
 | 
					            diagrams: '++id, name, databaseType, createdAt, updatedAt',
 | 
				
			||||||
            db_tables:
 | 
					            db_tables:
 | 
				
			||||||
                '++id, diagramId, name, x, y, fields, indexes, color, createdAt, width',
 | 
					                '++id, diagramId, name, x, y, fields, indexes, color, createdAt, width',
 | 
				
			||||||
@@ -45,7 +56,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
            config: '++id, defaultDiagramId',
 | 
					            config: '++id, defaultDiagramId',
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db.version(2).upgrade((tx) =>
 | 
					        dexieDB.version(2).upgrade((tx) =>
 | 
				
			||||||
            tx
 | 
					            tx
 | 
				
			||||||
                .table<DBTable & { diagramId: string }>('db_tables')
 | 
					                .table<DBTable & { diagramId: string }>('db_tables')
 | 
				
			||||||
                .toCollection()
 | 
					                .toCollection()
 | 
				
			||||||
@@ -61,7 +72,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
                })
 | 
					                })
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db.version(3).stores({
 | 
					        dexieDB.version(3).stores({
 | 
				
			||||||
            diagrams:
 | 
					            diagrams:
 | 
				
			||||||
                '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
 | 
					                '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
 | 
				
			||||||
            db_tables:
 | 
					            db_tables:
 | 
				
			||||||
@@ -71,7 +82,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
            config: '++id, defaultDiagramId',
 | 
					            config: '++id, defaultDiagramId',
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db.version(4).stores({
 | 
					        dexieDB.version(4).stores({
 | 
				
			||||||
            diagrams:
 | 
					            diagrams:
 | 
				
			||||||
                '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
 | 
					                '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
 | 
				
			||||||
            db_tables:
 | 
					            db_tables:
 | 
				
			||||||
@@ -81,7 +92,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
            config: '++id, defaultDiagramId',
 | 
					            config: '++id, defaultDiagramId',
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db.version(5).stores({
 | 
					        dexieDB.version(5).stores({
 | 
				
			||||||
            diagrams:
 | 
					            diagrams:
 | 
				
			||||||
                '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
 | 
					                '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
 | 
				
			||||||
            db_tables:
 | 
					            db_tables:
 | 
				
			||||||
@@ -91,16 +102,18 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
            config: '++id, defaultDiagramId',
 | 
					            config: '++id, defaultDiagramId',
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db.version(6).upgrade((tx) =>
 | 
					        dexieDB.version(6).upgrade((tx) =>
 | 
				
			||||||
            tx
 | 
					            tx
 | 
				
			||||||
            .table<DBRelationship & { diagramId: string }>('db_relationships')
 | 
					                .table<DBRelationship & { diagramId: string }>(
 | 
				
			||||||
 | 
					                    'db_relationships'
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
                .toCollection()
 | 
					                .toCollection()
 | 
				
			||||||
                .modify((relationship, ref) => {
 | 
					                .modify((relationship, ref) => {
 | 
				
			||||||
                const {
 | 
					                    const { sourceCardinality, targetCardinality } =
 | 
				
			||||||
                    sourceCardinality,
 | 
					                        determineCardinalities(
 | 
				
			||||||
                    targetCardinality,
 | 
					                            // @ts-expect-error string before
 | 
				
			||||||
                } = // @ts-expect-error string before
 | 
					                            relationship.type ?? 'one_to_one'
 | 
				
			||||||
                    determineCardinalities(relationship.type ?? 'one_to_one');
 | 
					                        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    relationship.sourceCardinality = sourceCardinality;
 | 
					                    relationship.sourceCardinality = sourceCardinality;
 | 
				
			||||||
                    relationship.targetCardinality = targetCardinality;
 | 
					                    relationship.targetCardinality = targetCardinality;
 | 
				
			||||||
@@ -110,7 +123,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
                })
 | 
					                })
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db.version(7).stores({
 | 
					        dexieDB.version(7).stores({
 | 
				
			||||||
            diagrams:
 | 
					            diagrams:
 | 
				
			||||||
                '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
 | 
					                '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
 | 
				
			||||||
            db_tables:
 | 
					            db_tables:
 | 
				
			||||||
@@ -122,7 +135,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
            config: '++id, defaultDiagramId',
 | 
					            config: '++id, defaultDiagramId',
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db.version(8).stores({
 | 
					        dexieDB.version(8).stores({
 | 
				
			||||||
            diagrams:
 | 
					            diagrams:
 | 
				
			||||||
                '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
 | 
					                '++id, name, databaseType, databaseEdition, createdAt, updatedAt',
 | 
				
			||||||
            db_tables:
 | 
					            db_tables:
 | 
				
			||||||
@@ -134,7 +147,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
            config: '++id, defaultDiagramId',
 | 
					            config: '++id, defaultDiagramId',
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db.version(9).upgrade((tx) =>
 | 
					        dexieDB.version(9).upgrade((tx) =>
 | 
				
			||||||
            tx
 | 
					            tx
 | 
				
			||||||
                .table<DBTable & { diagramId: string }>('db_tables')
 | 
					                .table<DBTable & { diagramId: string }>('db_tables')
 | 
				
			||||||
                .toCollection()
 | 
					                .toCollection()
 | 
				
			||||||
@@ -142,42 +155,349 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
                    for (const field of table.fields) {
 | 
					                    for (const field of table.fields) {
 | 
				
			||||||
                        if (typeof field.nullable === 'string') {
 | 
					                        if (typeof field.nullable === 'string') {
 | 
				
			||||||
                            field.nullable =
 | 
					                            field.nullable =
 | 
				
			||||||
                            (field.nullable as string).toLowerCase() === 'true';
 | 
					                                (field.nullable as string).toLowerCase() ===
 | 
				
			||||||
 | 
					                                'true';
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db.on('ready', async () => {
 | 
					        dexieDB.version(10).stores({
 | 
				
			||||||
        const config = await getConfig();
 | 
					            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',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        dexieDB.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',
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        dexieDB.on('ready', async () => {
 | 
				
			||||||
 | 
					            const config = await dexieDB.config.get(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!config) {
 | 
					            if (!config) {
 | 
				
			||||||
            const diagrams = await db.diagrams.toArray();
 | 
					                const diagrams = await dexieDB.diagrams.toArray();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await db.config.add({
 | 
					                await dexieDB.config.add({
 | 
				
			||||||
                    id: 1,
 | 
					                    id: 1,
 | 
				
			||||||
                    defaultDiagramId: diagrams?.[0]?.id ?? '',
 | 
					                    defaultDiagramId: diagrams?.[0]?.id ?? '',
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					        return dexieDB;
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const getConfig: StorageContext['getConfig'] = async (): Promise<
 | 
					    const getConfig: StorageContext['getConfig'] =
 | 
				
			||||||
        ChartDBConfig | undefined
 | 
					        useCallback(async (): Promise<ChartDBConfig | undefined> => {
 | 
				
			||||||
    > => {
 | 
					 | 
				
			||||||
            return await db.config.get(1);
 | 
					            return await db.config.get(1);
 | 
				
			||||||
    };
 | 
					        }, [db]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const updateConfig: StorageContext['updateConfig'] = async (
 | 
					    const updateConfig: StorageContext['updateConfig'] = useCallback(
 | 
				
			||||||
        config: Partial<ChartDBConfig>
 | 
					        async (config) => {
 | 
				
			||||||
    ) => {
 | 
					 | 
				
			||||||
            await db.config.update(1, config);
 | 
					            await db.config.update(1, config);
 | 
				
			||||||
    };
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const addDiagram: StorageContext['addDiagram'] = async ({
 | 
					    const addTable: StorageContext['addTable'] = useCallback(
 | 
				
			||||||
        diagram,
 | 
					        async ({ diagramId, table }) => {
 | 
				
			||||||
    }: {
 | 
					            await db.db_tables.add({
 | 
				
			||||||
        diagram: Diagram;
 | 
					                ...table,
 | 
				
			||||||
    }) => {
 | 
					                diagramId,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getTable: StorageContext['getTable'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ id, diagramId }): Promise<DBTable | undefined> => {
 | 
				
			||||||
 | 
					            return await db.db_tables.get({ id, diagramId });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const deleteDiagramTables: StorageContext['deleteDiagramTables'] =
 | 
				
			||||||
 | 
					        useCallback(
 | 
				
			||||||
 | 
					            async (diagramId) => {
 | 
				
			||||||
 | 
					                await db.db_tables
 | 
				
			||||||
 | 
					                    .where('diagramId')
 | 
				
			||||||
 | 
					                    .equals(diagramId)
 | 
				
			||||||
 | 
					                    .delete();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            [db]
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const updateTable: StorageContext['updateTable'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ id, attributes }) => {
 | 
				
			||||||
 | 
					            await db.db_tables.update(id, attributes);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const putTable: StorageContext['putTable'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ diagramId, table }) => {
 | 
				
			||||||
 | 
					            await db.db_tables.put({ ...table, diagramId });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const deleteTable: StorageContext['deleteTable'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ id, diagramId }) => {
 | 
				
			||||||
 | 
					            await db.db_tables.where({ id, diagramId }).delete();
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const listTables: StorageContext['listTables'] = useCallback(
 | 
				
			||||||
 | 
					        async (diagramId): Promise<DBTable[]> => {
 | 
				
			||||||
 | 
					            // Fetch all tables associated with the diagram
 | 
				
			||||||
 | 
					            const tables = await db.db_tables
 | 
				
			||||||
 | 
					                .where('diagramId')
 | 
				
			||||||
 | 
					                .equals(diagramId)
 | 
				
			||||||
 | 
					                .toArray();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return tables;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const addRelationship: StorageContext['addRelationship'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ diagramId, relationship }) => {
 | 
				
			||||||
 | 
					            await db.db_relationships.add({
 | 
				
			||||||
 | 
					                ...relationship,
 | 
				
			||||||
 | 
					                diagramId,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const deleteDiagramRelationships: StorageContext['deleteDiagramRelationships'] =
 | 
				
			||||||
 | 
					        useCallback(
 | 
				
			||||||
 | 
					            async (diagramId) => {
 | 
				
			||||||
 | 
					                await db.db_relationships
 | 
				
			||||||
 | 
					                    .where('diagramId')
 | 
				
			||||||
 | 
					                    .equals(diagramId)
 | 
				
			||||||
 | 
					                    .delete();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            [db]
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getRelationship: StorageContext['getRelationship'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ id, diagramId }): Promise<DBRelationship | undefined> => {
 | 
				
			||||||
 | 
					            return await db.db_relationships.get({ id, diagramId });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const updateRelationship: StorageContext['updateRelationship'] =
 | 
				
			||||||
 | 
					        useCallback(
 | 
				
			||||||
 | 
					            async ({ id, attributes }) => {
 | 
				
			||||||
 | 
					                await db.db_relationships.update(id, attributes);
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            [db]
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const deleteRelationship: StorageContext['deleteRelationship'] =
 | 
				
			||||||
 | 
					        useCallback(
 | 
				
			||||||
 | 
					            async ({ id, diagramId }) => {
 | 
				
			||||||
 | 
					                await db.db_relationships.where({ id, diagramId }).delete();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            [db]
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const listRelationships: StorageContext['listRelationships'] = useCallback(
 | 
				
			||||||
 | 
					        async (diagramId): Promise<DBRelationship[]> => {
 | 
				
			||||||
 | 
					            // Sort relationships alphabetically
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                await db.db_relationships
 | 
				
			||||||
 | 
					                    .where('diagramId')
 | 
				
			||||||
 | 
					                    .equals(diagramId)
 | 
				
			||||||
 | 
					                    .toArray()
 | 
				
			||||||
 | 
					            ).sort((a, b) => {
 | 
				
			||||||
 | 
					                return a.name.localeCompare(b.name);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const addDependency: StorageContext['addDependency'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ diagramId, dependency }) => {
 | 
				
			||||||
 | 
					            await db.db_dependencies.add({
 | 
				
			||||||
 | 
					                ...dependency,
 | 
				
			||||||
 | 
					                diagramId,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getDependency: StorageContext['getDependency'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ diagramId, id }) => {
 | 
				
			||||||
 | 
					            return await db.db_dependencies.get({ id, diagramId });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const updateDependency: StorageContext['updateDependency'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ id, attributes }) => {
 | 
				
			||||||
 | 
					            await db.db_dependencies.update(id, attributes);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const deleteDependency: StorageContext['deleteDependency'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ diagramId, id }) => {
 | 
				
			||||||
 | 
					            await db.db_dependencies.where({ id, diagramId }).delete();
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const listDependencies: StorageContext['listDependencies'] = useCallback(
 | 
				
			||||||
 | 
					        async (diagramId) => {
 | 
				
			||||||
 | 
					            return await db.db_dependencies
 | 
				
			||||||
 | 
					                .where('diagramId')
 | 
				
			||||||
 | 
					                .equals(diagramId)
 | 
				
			||||||
 | 
					                .toArray();
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const deleteDiagramDependencies: StorageContext['deleteDiagramDependencies'] =
 | 
				
			||||||
 | 
					        useCallback(
 | 
				
			||||||
 | 
					            async (diagramId) => {
 | 
				
			||||||
 | 
					                await db.db_dependencies
 | 
				
			||||||
 | 
					                    .where('diagramId')
 | 
				
			||||||
 | 
					                    .equals(diagramId)
 | 
				
			||||||
 | 
					                    .delete();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            [db]
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const addArea: StorageContext['addArea'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ area, diagramId }) => {
 | 
				
			||||||
 | 
					            await db.areas.add({
 | 
				
			||||||
 | 
					                ...area,
 | 
				
			||||||
 | 
					                diagramId,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getArea: StorageContext['getArea'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ diagramId, id }) => {
 | 
				
			||||||
 | 
					            return await db.areas.get({ id, diagramId });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const updateArea: StorageContext['updateArea'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ id, attributes }) => {
 | 
				
			||||||
 | 
					            await db.areas.update(id, attributes);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const deleteArea: StorageContext['deleteArea'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ diagramId, id }) => {
 | 
				
			||||||
 | 
					            await db.areas.where({ id, diagramId }).delete();
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const listAreas: StorageContext['listAreas'] = useCallback(
 | 
				
			||||||
 | 
					        async (diagramId) => {
 | 
				
			||||||
 | 
					            return await db.areas
 | 
				
			||||||
 | 
					                .where('diagramId')
 | 
				
			||||||
 | 
					                .equals(diagramId)
 | 
				
			||||||
 | 
					                .toArray();
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const deleteDiagramAreas: StorageContext['deleteDiagramAreas'] =
 | 
				
			||||||
 | 
					        useCallback(
 | 
				
			||||||
 | 
					            async (diagramId) => {
 | 
				
			||||||
 | 
					                await db.areas.where('diagramId').equals(diagramId).delete();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            [db]
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Custom type operations
 | 
				
			||||||
 | 
					    const addCustomType: StorageContext['addCustomType'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ diagramId, customType }) => {
 | 
				
			||||||
 | 
					            await db.db_custom_types.add({
 | 
				
			||||||
 | 
					                ...customType,
 | 
				
			||||||
 | 
					                diagramId,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const getCustomType: StorageContext['getCustomType'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ diagramId, id }): Promise<DBCustomType | undefined> => {
 | 
				
			||||||
 | 
					            return await db.db_custom_types.get({ id, diagramId });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const updateCustomType: StorageContext['updateCustomType'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ id, attributes }) => {
 | 
				
			||||||
 | 
					            await db.db_custom_types.update(id, attributes);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const deleteCustomType: StorageContext['deleteCustomType'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ diagramId, id }) => {
 | 
				
			||||||
 | 
					            await db.db_custom_types.where({ id, diagramId }).delete();
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const listCustomTypes: StorageContext['listCustomTypes'] = useCallback(
 | 
				
			||||||
 | 
					        async (diagramId): Promise<DBCustomType[]> => {
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					                await db.db_custom_types
 | 
				
			||||||
 | 
					                    .where('diagramId')
 | 
				
			||||||
 | 
					                    .equals(diagramId)
 | 
				
			||||||
 | 
					                    .toArray()
 | 
				
			||||||
 | 
					            ).sort((a, b) => {
 | 
				
			||||||
 | 
					                return a.name.localeCompare(b.name);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const deleteDiagramCustomTypes: StorageContext['deleteDiagramCustomTypes'] =
 | 
				
			||||||
 | 
					        useCallback(
 | 
				
			||||||
 | 
					            async (diagramId) => {
 | 
				
			||||||
 | 
					                await db.db_custom_types
 | 
				
			||||||
 | 
					                    .where('diagramId')
 | 
				
			||||||
 | 
					                    .equals(diagramId)
 | 
				
			||||||
 | 
					                    .delete();
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            [db]
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const addDiagram: StorageContext['addDiagram'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ diagram }) => {
 | 
				
			||||||
            const promises = [];
 | 
					            const promises = [];
 | 
				
			||||||
            promises.push(
 | 
					            promises.push(
 | 
				
			||||||
                db.diagrams.add({
 | 
					                db.diagrams.add({
 | 
				
			||||||
@@ -192,7 +512,9 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            const tables = diagram.tables ?? [];
 | 
					            const tables = diagram.tables ?? [];
 | 
				
			||||||
            promises.push(
 | 
					            promises.push(
 | 
				
			||||||
            ...tables.map((table) => addTable({ diagramId: diagram.id, table }))
 | 
					                ...tables.map((table) =>
 | 
				
			||||||
 | 
					                    addTable({ diagramId: diagram.id, table })
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const relationships = diagram.relationships ?? [];
 | 
					            const relationships = diagram.relationships ?? [];
 | 
				
			||||||
@@ -209,18 +531,31 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
                )
 | 
					                )
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await Promise.all(promises);
 | 
					            const areas = diagram.areas ?? [];
 | 
				
			||||||
    };
 | 
					            promises.push(
 | 
				
			||||||
 | 
					                ...areas.map((area) => addArea({ diagramId: diagram.id, area }))
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const listDiagrams: StorageContext['listDiagrams'] = async (
 | 
					            const customTypes = diagram.customTypes ?? [];
 | 
				
			||||||
        options: {
 | 
					            promises.push(
 | 
				
			||||||
            includeTables?: boolean;
 | 
					                ...customTypes.map((customType) =>
 | 
				
			||||||
            includeRelationships?: boolean;
 | 
					                    addCustomType({ diagramId: diagram.id, customType })
 | 
				
			||||||
            includeDependencies?: boolean;
 | 
					                )
 | 
				
			||||||
        } = {
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await Promise.all(promises);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [db, addArea, addCustomType, addDependency, addRelationship, addTable]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const listDiagrams: StorageContext['listDiagrams'] = useCallback(
 | 
				
			||||||
 | 
					        async (
 | 
				
			||||||
 | 
					            options = {
 | 
				
			||||||
                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();
 | 
				
			||||||
@@ -237,7 +572,9 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
            if (options.includeRelationships) {
 | 
					            if (options.includeRelationships) {
 | 
				
			||||||
                diagrams = await Promise.all(
 | 
					                diagrams = await Promise.all(
 | 
				
			||||||
                    diagrams.map(async (diagram) => {
 | 
					                    diagrams.map(async (diagram) => {
 | 
				
			||||||
                    diagram.relationships = await listRelationships(diagram.id);
 | 
					                        diagram.relationships = await listRelationships(
 | 
				
			||||||
 | 
					                            diagram.id
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
                        return diagram;
 | 
					                        return diagram;
 | 
				
			||||||
                    })
 | 
					                    })
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
@@ -246,25 +583,53 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
            if (options.includeDependencies) {
 | 
					            if (options.includeDependencies) {
 | 
				
			||||||
                diagrams = await Promise.all(
 | 
					                diagrams = await Promise.all(
 | 
				
			||||||
                    diagrams.map(async (diagram) => {
 | 
					                    diagrams.map(async (diagram) => {
 | 
				
			||||||
                    diagram.dependencies = await listDependencies(diagram.id);
 | 
					                        diagram.dependencies = await listDependencies(
 | 
				
			||||||
 | 
					                            diagram.id
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                        return diagram;
 | 
				
			||||||
 | 
					                    })
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            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 diagram;
 | 
				
			||||||
                    })
 | 
					                    })
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return diagrams;
 | 
					            return diagrams;
 | 
				
			||||||
    };
 | 
					        },
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            db,
 | 
				
			||||||
 | 
					            listAreas,
 | 
				
			||||||
 | 
					            listCustomTypes,
 | 
				
			||||||
 | 
					            listDependencies,
 | 
				
			||||||
 | 
					            listRelationships,
 | 
				
			||||||
 | 
					            listTables,
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const getDiagram: StorageContext['getDiagram'] = async (
 | 
					    const getDiagram: StorageContext['getDiagram'] = useCallback(
 | 
				
			||||||
        id: string,
 | 
					        async (
 | 
				
			||||||
        options: {
 | 
					            id,
 | 
				
			||||||
            includeTables?: boolean;
 | 
					            options = {
 | 
				
			||||||
            includeRelationships?: boolean;
 | 
					 | 
				
			||||||
            includeDependencies?: 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,16 +650,28 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
				
			|||||||
                diagram.dependencies = await listDependencies(id);
 | 
					                diagram.dependencies = await listDependencies(id);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return diagram;
 | 
					            if (options.includeAreas) {
 | 
				
			||||||
    };
 | 
					                diagram.areas = await listAreas(id);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const updateDiagram: StorageContext['updateDiagram'] = async ({
 | 
					            if (options.includeCustomTypes) {
 | 
				
			||||||
        id,
 | 
					                diagram.customTypes = await listCustomTypes(id);
 | 
				
			||||||
        attributes,
 | 
					            }
 | 
				
			||||||
    }: {
 | 
					
 | 
				
			||||||
        id: string;
 | 
					            return diagram;
 | 
				
			||||||
        attributes: Partial<Diagram>;
 | 
					        },
 | 
				
			||||||
    }) => {
 | 
					        [
 | 
				
			||||||
 | 
					            db,
 | 
				
			||||||
 | 
					            listAreas,
 | 
				
			||||||
 | 
					            listCustomTypes,
 | 
				
			||||||
 | 
					            listDependencies,
 | 
				
			||||||
 | 
					            listRelationships,
 | 
				
			||||||
 | 
					            listTables,
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const updateDiagram: StorageContext['updateDiagram'] = useCallback(
 | 
				
			||||||
 | 
					        async ({ id, attributes }) => {
 | 
				
			||||||
            await db.diagrams.update(id, attributes);
 | 
					            await db.diagrams.update(id, attributes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (attributes.id) {
 | 
					            if (attributes.id) {
 | 
				
			||||||
@@ -311,198 +688,32 @@ 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 }),
 | 
				
			||||||
                ]);
 | 
					                ]);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
    };
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const deleteDiagram: StorageContext['deleteDiagram'] = async (
 | 
					    const deleteDiagram: StorageContext['deleteDiagram'] = useCallback(
 | 
				
			||||||
        id: string
 | 
					        async (id) => {
 | 
				
			||||||
    ) => {
 | 
					 | 
				
			||||||
            await Promise.all([
 | 
					            await Promise.all([
 | 
				
			||||||
                db.diagrams.delete(id),
 | 
					                db.diagrams.delete(id),
 | 
				
			||||||
                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(),
 | 
				
			||||||
            ]);
 | 
					            ]);
 | 
				
			||||||
    };
 | 
					        },
 | 
				
			||||||
 | 
					        [db]
 | 
				
			||||||
    const addTable: StorageContext['addTable'] = async ({
 | 
					    );
 | 
				
			||||||
        diagramId,
 | 
					 | 
				
			||||||
        table,
 | 
					 | 
				
			||||||
    }: {
 | 
					 | 
				
			||||||
        diagramId: string;
 | 
					 | 
				
			||||||
        table: DBTable;
 | 
					 | 
				
			||||||
    }) => {
 | 
					 | 
				
			||||||
        await db.db_tables.add({
 | 
					 | 
				
			||||||
            ...table,
 | 
					 | 
				
			||||||
            diagramId,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const getTable: StorageContext['getTable'] = async ({
 | 
					 | 
				
			||||||
        id,
 | 
					 | 
				
			||||||
        diagramId,
 | 
					 | 
				
			||||||
    }: {
 | 
					 | 
				
			||||||
        diagramId: string;
 | 
					 | 
				
			||||||
        id: string;
 | 
					 | 
				
			||||||
    }): Promise<DBTable | undefined> => {
 | 
					 | 
				
			||||||
        return await db.db_tables.get({ id, diagramId });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const deleteDiagramTables: StorageContext['deleteDiagramTables'] = async (
 | 
					 | 
				
			||||||
        diagramId: string
 | 
					 | 
				
			||||||
    ) => {
 | 
					 | 
				
			||||||
        await db.db_tables.where('diagramId').equals(diagramId).delete();
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const updateTable: StorageContext['updateTable'] = async ({
 | 
					 | 
				
			||||||
        id,
 | 
					 | 
				
			||||||
        attributes,
 | 
					 | 
				
			||||||
    }) => {
 | 
					 | 
				
			||||||
        await db.db_tables.update(id, attributes);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const putTable: StorageContext['putTable'] = async ({
 | 
					 | 
				
			||||||
        diagramId,
 | 
					 | 
				
			||||||
        table,
 | 
					 | 
				
			||||||
    }) => {
 | 
					 | 
				
			||||||
        await db.db_tables.put({ ...table, diagramId });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const deleteTable: StorageContext['deleteTable'] = async ({
 | 
					 | 
				
			||||||
        id,
 | 
					 | 
				
			||||||
        diagramId,
 | 
					 | 
				
			||||||
    }: {
 | 
					 | 
				
			||||||
        id: string;
 | 
					 | 
				
			||||||
        diagramId: string;
 | 
					 | 
				
			||||||
    }) => {
 | 
					 | 
				
			||||||
        await db.db_tables.where({ id, diagramId }).delete();
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const listTables: StorageContext['listTables'] = async (
 | 
					 | 
				
			||||||
        diagramId: string
 | 
					 | 
				
			||||||
    ): Promise<DBTable[]> => {
 | 
					 | 
				
			||||||
        // Fetch all tables associated with the diagram
 | 
					 | 
				
			||||||
        const tables = await db.db_tables
 | 
					 | 
				
			||||||
            .where('diagramId')
 | 
					 | 
				
			||||||
            .equals(diagramId)
 | 
					 | 
				
			||||||
            .toArray();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return tables;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const addRelationship: StorageContext['addRelationship'] = async ({
 | 
					 | 
				
			||||||
        diagramId,
 | 
					 | 
				
			||||||
        relationship,
 | 
					 | 
				
			||||||
    }: {
 | 
					 | 
				
			||||||
        diagramId: string;
 | 
					 | 
				
			||||||
        relationship: DBRelationship;
 | 
					 | 
				
			||||||
    }) => {
 | 
					 | 
				
			||||||
        await db.db_relationships.add({
 | 
					 | 
				
			||||||
            ...relationship,
 | 
					 | 
				
			||||||
            diagramId,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const deleteDiagramRelationships: StorageContext['deleteDiagramRelationships'] =
 | 
					 | 
				
			||||||
        async (diagramId: string) => {
 | 
					 | 
				
			||||||
            await db.db_relationships
 | 
					 | 
				
			||||||
                .where('diagramId')
 | 
					 | 
				
			||||||
                .equals(diagramId)
 | 
					 | 
				
			||||||
                .delete();
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const getRelationship: StorageContext['getRelationship'] = async ({
 | 
					 | 
				
			||||||
        id,
 | 
					 | 
				
			||||||
        diagramId,
 | 
					 | 
				
			||||||
    }: {
 | 
					 | 
				
			||||||
        diagramId: string;
 | 
					 | 
				
			||||||
        id: string;
 | 
					 | 
				
			||||||
    }): Promise<DBRelationship | undefined> => {
 | 
					 | 
				
			||||||
        return await db.db_relationships.get({ id, diagramId });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const updateRelationship: StorageContext['updateRelationship'] = async ({
 | 
					 | 
				
			||||||
        id,
 | 
					 | 
				
			||||||
        attributes,
 | 
					 | 
				
			||||||
    }: {
 | 
					 | 
				
			||||||
        id: string;
 | 
					 | 
				
			||||||
        attributes: Partial<DBRelationship>;
 | 
					 | 
				
			||||||
    }) => {
 | 
					 | 
				
			||||||
        await db.db_relationships.update(id, attributes);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const deleteRelationship: StorageContext['deleteRelationship'] = async ({
 | 
					 | 
				
			||||||
        id,
 | 
					 | 
				
			||||||
        diagramId,
 | 
					 | 
				
			||||||
    }: {
 | 
					 | 
				
			||||||
        id: string;
 | 
					 | 
				
			||||||
        diagramId: string;
 | 
					 | 
				
			||||||
    }) => {
 | 
					 | 
				
			||||||
        await db.db_relationships.where({ id, diagramId }).delete();
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const listRelationships: StorageContext['listRelationships'] = async (
 | 
					 | 
				
			||||||
        diagramId: string
 | 
					 | 
				
			||||||
    ): Promise<DBRelationship[]> => {
 | 
					 | 
				
			||||||
        // Sort relationships alphabetically
 | 
					 | 
				
			||||||
        return (
 | 
					 | 
				
			||||||
            await db.db_relationships
 | 
					 | 
				
			||||||
                .where('diagramId')
 | 
					 | 
				
			||||||
                .equals(diagramId)
 | 
					 | 
				
			||||||
                .toArray()
 | 
					 | 
				
			||||||
        ).sort((a, b) => {
 | 
					 | 
				
			||||||
            return a.name.localeCompare(b.name);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const addDependency: StorageContext['addDependency'] = async ({
 | 
					 | 
				
			||||||
        diagramId,
 | 
					 | 
				
			||||||
        dependency,
 | 
					 | 
				
			||||||
    }) => {
 | 
					 | 
				
			||||||
        await db.db_dependencies.add({
 | 
					 | 
				
			||||||
            ...dependency,
 | 
					 | 
				
			||||||
            diagramId,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const getDependency: StorageContext['getDependency'] = async ({
 | 
					 | 
				
			||||||
        diagramId,
 | 
					 | 
				
			||||||
        id,
 | 
					 | 
				
			||||||
    }) => {
 | 
					 | 
				
			||||||
        return await db.db_dependencies.get({ id, diagramId });
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const updateDependency: StorageContext['updateDependency'] = async ({
 | 
					 | 
				
			||||||
        id,
 | 
					 | 
				
			||||||
        attributes,
 | 
					 | 
				
			||||||
    }) => {
 | 
					 | 
				
			||||||
        await db.db_dependencies.update(id, attributes);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const deleteDependency: StorageContext['deleteDependency'] = async ({
 | 
					 | 
				
			||||||
        diagramId,
 | 
					 | 
				
			||||||
        id,
 | 
					 | 
				
			||||||
    }) => {
 | 
					 | 
				
			||||||
        await db.db_dependencies.where({ id, diagramId }).delete();
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const listDependencies: StorageContext['listDependencies'] = async (
 | 
					 | 
				
			||||||
        diagramId
 | 
					 | 
				
			||||||
    ) => {
 | 
					 | 
				
			||||||
        return await db.db_dependencies
 | 
					 | 
				
			||||||
            .where('diagramId')
 | 
					 | 
				
			||||||
            .equals(diagramId)
 | 
					 | 
				
			||||||
            .toArray();
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const deleteDiagramDependencies: StorageContext['deleteDiagramDependencies'] =
 | 
					 | 
				
			||||||
        async (diagramId) => {
 | 
					 | 
				
			||||||
            await db.db_dependencies
 | 
					 | 
				
			||||||
                .where('diagramId')
 | 
					 | 
				
			||||||
                .equals(diagramId)
 | 
					 | 
				
			||||||
                .delete();
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <storageContext.Provider
 | 
					        <storageContext.Provider
 | 
				
			||||||
@@ -533,6 +744,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,7 @@
 | 
				
			|||||||
import { createContext } from 'react';
 | 
					import { createContext } from 'react';
 | 
				
			||||||
import { emptyFn } from '@/lib/utils';
 | 
					import { emptyFn } from '@/lib/utils';
 | 
				
			||||||
 | 
					import type { Theme, EffectiveTheme } from '@/lib/types';
 | 
				
			||||||
export type Theme = 'light' | 'dark' | 'system';
 | 
					export type { Theme, EffectiveTheme };
 | 
				
			||||||
export type EffectiveTheme = Exclude<Theme, 'system'>;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ThemeContext {
 | 
					export interface ThemeContext {
 | 
				
			||||||
    theme: Theme;
 | 
					    theme: Theme;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,25 @@ 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,
 | 
				
			||||||
 | 
					            enableOnFormTags: 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,78 @@ 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, IDisposable } from 'monaco-editor';
 | 
				
			||||||
 | 
					import { waitFor } from '@/lib/utils';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    validateSQL,
 | 
				
			||||||
 | 
					    type ValidationResult,
 | 
				
			||||||
 | 
					} from '@/lib/data/sql-import/sql-validator';
 | 
				
			||||||
 | 
					import { SQLValidationStatus } from './sql-validation-status';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const calculateContentSizeMB = (content: string): number => {
 | 
				
			||||||
 | 
					    return content.length / (1024 * 1024); // Convert to MB
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const calculateIsLargeFile = (content: string): boolean => {
 | 
				
			||||||
 | 
					    const contentSizeMB = calculateContentSizeMB(content);
 | 
				
			||||||
 | 
					    return contentSizeMB > 2; // Consider large if over 2MB
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const errorScriptOutputMessage =
 | 
					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 +111,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 +126,80 @@ 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 pasteDisposableRef = useRef<IDisposable | null>(null);
 | 
				
			||||||
    >();
 | 
					 | 
				
			||||||
    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(() => {
 | 
					    const [sqlValidation, setSqlValidation] = useState<ValidationResult | null>(
 | 
				
			||||||
        const loadScripts = async () => {
 | 
					        null
 | 
				
			||||||
            const { importMetadataScripts } = await import(
 | 
					 | 
				
			||||||
                '@/lib/data/import-metadata/scripts/scripts'
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
            setImportMetadataScripts(importMetadataScripts);
 | 
					    const [isAutoFixing, setIsAutoFixing] = useState(false);
 | 
				
			||||||
        };
 | 
					    const [showAutoFixButton, setShowAutoFixButton] = useState(false);
 | 
				
			||||||
        loadScripts();
 | 
					 | 
				
			||||||
    }, []);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() => {
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        setScriptResult('');
 | 
				
			||||||
 | 
					        setErrorMessage('');
 | 
				
			||||||
 | 
					        setShowCheckJsonButton(false);
 | 
				
			||||||
 | 
					    }, [importMethod, setScriptResult]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check if the ddl is valid
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        if (importMethod !== 'ddl') {
 | 
				
			||||||
 | 
					            setSqlValidation(null);
 | 
				
			||||||
 | 
					            setShowAutoFixButton(false);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!scriptResult.trim()) {
 | 
				
			||||||
 | 
					            setSqlValidation(null);
 | 
				
			||||||
 | 
					            setShowAutoFixButton(false);
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // First run our validation based on database type
 | 
				
			||||||
 | 
					        const validation = validateSQL(scriptResult, databaseType);
 | 
				
			||||||
 | 
					        setSqlValidation(validation);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // If we have auto-fixable errors, show the auto-fix button
 | 
				
			||||||
 | 
					        if (validation.fixedSQL && validation.errors.length > 0) {
 | 
				
			||||||
 | 
					            setShowAutoFixButton(true);
 | 
				
			||||||
 | 
					            // Don't try to parse invalid SQL
 | 
				
			||||||
 | 
					            setErrorMessage('SQL contains syntax errors');
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Hide auto-fix button if no fixes available
 | 
				
			||||||
 | 
					        setShowAutoFixButton(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Validate the SQL (either original or already fixed)
 | 
				
			||||||
 | 
					        parseSQLError({
 | 
				
			||||||
 | 
					            sqlContent: scriptResult,
 | 
				
			||||||
 | 
					            sourceDatabaseType: databaseType,
 | 
				
			||||||
 | 
					        }).then((result) => {
 | 
				
			||||||
 | 
					            if (result.success) {
 | 
				
			||||||
 | 
					                setErrorMessage('');
 | 
				
			||||||
 | 
					            } else if (!result.success && result.error) {
 | 
				
			||||||
 | 
					                setErrorMessage(result.error);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }, [importMethod, scriptResult, databaseType]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Check if the script result is a valid JSON
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        if (importMethod !== 'query') {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (scriptResult.trim().length === 0) {
 | 
					        if (scriptResult.trim().length === 0) {
 | 
				
			||||||
            setErrorMessage('');
 | 
					            setErrorMessage('');
 | 
				
			||||||
            setShowCheckJsonButton(false);
 | 
					            setShowCheckJsonButton(false);
 | 
				
			||||||
@@ -115,7 +219,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,30 +227,150 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }, [errorMessage.length, onImport, scriptResult]);
 | 
					    }, [errorMessage.length, onImport, scriptResult]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleInputChange = useCallback(
 | 
					    const handleAutoFix = useCallback(() => {
 | 
				
			||||||
        (e: React.ChangeEvent<HTMLTextAreaElement>) => {
 | 
					        if (sqlValidation?.fixedSQL) {
 | 
				
			||||||
            const inputValue = e.target.value;
 | 
					            setIsAutoFixing(true);
 | 
				
			||||||
            setScriptResult(inputValue);
 | 
					            setShowAutoFixButton(false);
 | 
				
			||||||
 | 
					            setErrorMessage('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Apply the fix with a delay so user sees the fixing message
 | 
				
			||||||
 | 
					            setTimeout(() => {
 | 
				
			||||||
 | 
					                setScriptResult(sqlValidation.fixedSQL!);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                setTimeout(() => {
 | 
				
			||||||
 | 
					                    setIsAutoFixing(false);
 | 
				
			||||||
 | 
					                }, 100);
 | 
				
			||||||
 | 
					            }, 1000);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }, [sqlValidation, setScriptResult]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleErrorClick = useCallback((line: number) => {
 | 
				
			||||||
 | 
					        if (editorRef.current) {
 | 
				
			||||||
 | 
					            // Set cursor to the error line
 | 
				
			||||||
 | 
					            editorRef.current.setPosition({ lineNumber: line, column: 1 });
 | 
				
			||||||
 | 
					            editorRef.current.revealLineInCenter(line);
 | 
				
			||||||
 | 
					            editorRef.current.focus();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const formatEditor = useCallback(() => {
 | 
				
			||||||
 | 
					        if (editorRef.current) {
 | 
				
			||||||
 | 
					            const model = editorRef.current.getModel();
 | 
				
			||||||
 | 
					            if (model) {
 | 
				
			||||||
 | 
					                const content = model.getValue();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Skip formatting for large files (> 2MB)
 | 
				
			||||||
 | 
					                if (calculateIsLargeFile(content)) {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            setTimeout(() => {
 | 
				
			||||||
 | 
					                editorRef.current
 | 
				
			||||||
 | 
					                    ?.getAction('editor.action.formatDocument')
 | 
				
			||||||
 | 
					                    ?.run();
 | 
				
			||||||
 | 
					            }, 50);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleInputChange: OnChange = useCallback(
 | 
				
			||||||
 | 
					        (inputValue) => {
 | 
				
			||||||
 | 
					            setScriptResult(inputValue ?? '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Automatically open SSMS info when input length is exactly 65535
 | 
				
			||||||
 | 
					            if ((inputValue ?? '').length === 65535) {
 | 
				
			||||||
 | 
					                setShowSSMSInfoDialog(true);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        [setScriptResult]
 | 
					        [setScriptResult]
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const debouncedHandleInputChange = useDebounce(handleInputChange, 500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleCheckJson = useCallback(async () => {
 | 
					    const handleCheckJson = useCallback(async () => {
 | 
				
			||||||
        setIsCheckingJson(true);
 | 
					        setIsCheckingJson(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const fixedJson = await fixMetadataJson(scriptResult);
 | 
					        await waitFor(1000);
 | 
				
			||||||
 | 
					        const fixedJson = fixMetadataJson(scriptResult);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        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]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        // Cleanup paste handler on unmount
 | 
				
			||||||
 | 
					        return () => {
 | 
				
			||||||
 | 
					            if (pasteDisposableRef.current) {
 | 
				
			||||||
 | 
					                pasteDisposableRef.current.dispose();
 | 
				
			||||||
 | 
					                pasteDisposableRef.current = null;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleEditorDidMount = useCallback(
 | 
				
			||||||
 | 
					        (editor: editor.IStandaloneCodeEditor) => {
 | 
				
			||||||
 | 
					            editorRef.current = editor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Cleanup previous disposable if it exists
 | 
				
			||||||
 | 
					            if (pasteDisposableRef.current) {
 | 
				
			||||||
 | 
					                pasteDisposableRef.current.dispose();
 | 
				
			||||||
 | 
					                pasteDisposableRef.current = null;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Add paste handler for all modes
 | 
				
			||||||
 | 
					            const disposable = editor.onDidPaste(() => {
 | 
				
			||||||
 | 
					                const model = editor.getModel();
 | 
				
			||||||
 | 
					                if (!model) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const content = model.getValue();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Skip formatting for large files (> 2MB) to prevent browser freezing
 | 
				
			||||||
 | 
					                const isLargeFile = calculateIsLargeFile(content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // First, detect content type to determine if we should switch modes
 | 
				
			||||||
 | 
					                const detectedType = detectContentType(content);
 | 
				
			||||||
 | 
					                if (detectedType && detectedType !== importMethod) {
 | 
				
			||||||
 | 
					                    // Switch to the detected mode immediately
 | 
				
			||||||
 | 
					                    setImportMethod(detectedType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // Only format if it's JSON (query mode) AND file is not too large
 | 
				
			||||||
 | 
					                    if (detectedType === 'query' && !isLargeFile) {
 | 
				
			||||||
 | 
					                        // For JSON mode, format after a short delay
 | 
				
			||||||
 | 
					                        setTimeout(() => {
 | 
				
			||||||
 | 
					                            editor
 | 
				
			||||||
 | 
					                                .getAction('editor.action.formatDocument')
 | 
				
			||||||
 | 
					                                ?.run();
 | 
				
			||||||
 | 
					                        }, 100);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    // For DDL mode, do NOT format as it can break the SQL
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    // Content type didn't change, apply formatting based on current mode
 | 
				
			||||||
 | 
					                    if (importMethod === 'query' && !isLargeFile) {
 | 
				
			||||||
 | 
					                        // Only format JSON content if not too large
 | 
				
			||||||
 | 
					                        setTimeout(() => {
 | 
				
			||||||
 | 
					                            editor
 | 
				
			||||||
 | 
					                                .getAction('editor.action.formatDocument')
 | 
				
			||||||
 | 
					                                ?.run();
 | 
				
			||||||
 | 
					                        }, 100);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    // For DDL mode or large files, do NOT format
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            pasteDisposableRef.current = disposable;
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [importMethod, setImportMethod]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const renderHeader = useCallback(() => {
 | 
					    const renderHeader = useCallback(() => {
 | 
				
			||||||
        return (
 | 
					        return (
 | 
				
			||||||
@@ -157,223 +381,137 @@ 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: false, // Never format on paste - we handle it manually
 | 
				
			||||||
 | 
					                                minimap: { enabled: false },
 | 
				
			||||||
 | 
					                                scrollBeyondLastLine: false,
 | 
				
			||||||
 | 
					                                automaticLayout: true,
 | 
				
			||||||
 | 
					                                glyphMargin: false,
 | 
				
			||||||
 | 
					                                lineNumbers: 'on',
 | 
				
			||||||
 | 
					                                guides: {
 | 
				
			||||||
 | 
					                                    indentation: false,
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                                folding: true,
 | 
				
			||||||
 | 
					                                lineNumbersMinChars: 3,
 | 
				
			||||||
 | 
					                                renderValidationDecorations: 'off',
 | 
				
			||||||
 | 
					                                lineDecorationsWidth: 0,
 | 
				
			||||||
 | 
					                                overviewRulerBorder: false,
 | 
				
			||||||
 | 
					                                overviewRulerLanes: 0,
 | 
				
			||||||
 | 
					                                hideCursorInOverviewRuler: true,
 | 
				
			||||||
 | 
					                                contextmenu: false,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                scrollbar: {
 | 
				
			||||||
 | 
					                                    vertical: 'hidden',
 | 
				
			||||||
 | 
					                                    horizontal: 'hidden',
 | 
				
			||||||
 | 
					                                    alwaysConsumeMouseWheel: false,
 | 
				
			||||||
 | 
					                                },
 | 
				
			||||||
 | 
					                            }}
 | 
				
			||||||
 | 
					                            className="size-full min-h-40"
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                    </Suspense>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                {errorMessage || (importMethod === 'ddl' && sqlValidation) ? (
 | 
				
			||||||
 | 
					                    <SQLValidationStatus
 | 
				
			||||||
 | 
					                        validation={sqlValidation}
 | 
				
			||||||
 | 
					                        errorMessage={errorMessage}
 | 
				
			||||||
 | 
					                        isAutoFixing={isAutoFixing}
 | 
				
			||||||
 | 
					                        onErrorClick={handleErrorClick}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                ) : null}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            errorMessage,
 | 
				
			||||||
 | 
					            scriptResult,
 | 
				
			||||||
 | 
					            importMethod,
 | 
				
			||||||
 | 
					            effectiveTheme,
 | 
				
			||||||
 | 
					            debouncedHandleInputChange,
 | 
				
			||||||
 | 
					            handleEditorDidMount,
 | 
				
			||||||
 | 
					            sqlValidation,
 | 
				
			||||||
 | 
					            isAutoFixing,
 | 
				
			||||||
 | 
					            handleErrorClick,
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const renderContent = useCallback(() => {
 | 
					    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'
 | 
					 | 
				
			||||||
                                )}
 | 
					 | 
				
			||||||
                            </p>
 | 
					 | 
				
			||||||
                            <ToggleGroup
 | 
					 | 
				
			||||||
                                type="single"
 | 
					 | 
				
			||||||
                                className="ml-1 flex-wrap gap-2"
 | 
					 | 
				
			||||||
                                value={
 | 
					 | 
				
			||||||
                                    !databaseEdition
 | 
					 | 
				
			||||||
                                        ? 'regular'
 | 
					 | 
				
			||||||
                                        : databaseEdition
 | 
					 | 
				
			||||||
                                }
 | 
					 | 
				
			||||||
                                onValueChange={(value) => {
 | 
					 | 
				
			||||||
                                    setDatabaseEdition(
 | 
					 | 
				
			||||||
                                        value === 'regular'
 | 
					 | 
				
			||||||
                                            ? undefined
 | 
					 | 
				
			||||||
                                            : (value as DatabaseEdition)
 | 
					 | 
				
			||||||
                                    );
 | 
					 | 
				
			||||||
                                }}
 | 
					 | 
				
			||||||
                    >
 | 
					                    >
 | 
				
			||||||
                                <ToggleGroupItem
 | 
					                        <ResizablePanel
 | 
				
			||||||
                                    value="regular"
 | 
					                            defaultSize={25}
 | 
				
			||||||
                                    variant="outline"
 | 
					                            minSize={25}
 | 
				
			||||||
                                    className="h-6 gap-1 p-0 px-2 shadow-none"
 | 
					                            maxSize={99}
 | 
				
			||||||
 | 
					                            className="min-h-fit rounded-md bg-gradient-to-b from-slate-50 to-slate-100 p-2 dark:from-slate-900 dark:to-slate-800 md:min-h-fit md:min-w-[350px] md:rounded-l-md md:p-2"
 | 
				
			||||||
                        >
 | 
					                        >
 | 
				
			||||||
                                    <Avatar className="size-4 rounded-none">
 | 
					                            {renderInstructions()}
 | 
				
			||||||
                                        <AvatarImage
 | 
					                        </ResizablePanel>
 | 
				
			||||||
                                            src={
 | 
					                        <ResizableHandle withHandle />
 | 
				
			||||||
                                                databaseSecondaryLogoMap[
 | 
					                        <ResizablePanel className="min-h-40 py-2 md:px-2 md:py-0">
 | 
				
			||||||
                                                    databaseType
 | 
					                            {renderOutputTextArea()}
 | 
				
			||||||
                                                ]
 | 
					                        </ResizablePanel>
 | 
				
			||||||
                                            }
 | 
					                    </ResizablePanelGroup>
 | 
				
			||||||
                                            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
 | 
					                    <div className="flex flex-col gap-2">
 | 
				
			||||||
                                className="h-40 w-full flex-auto"
 | 
					                        {renderInstructions()}
 | 
				
			||||||
                                loading={!importMetadataScripts}
 | 
					                        {renderOutputTextArea()}
 | 
				
			||||||
                                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,13 +545,43 @@ 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>
 | 
				
			||||||
 | 
					                    ) : showAutoFixButton && importMethod === 'ddl' ? (
 | 
				
			||||||
 | 
					                        <Button
 | 
				
			||||||
 | 
					                            type="button"
 | 
				
			||||||
 | 
					                            variant="secondary"
 | 
				
			||||||
 | 
					                            onClick={handleAutoFix}
 | 
				
			||||||
 | 
					                            disabled={isAutoFixing}
 | 
				
			||||||
 | 
					                            className="bg-sky-600 text-white hover:bg-sky-700"
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                            {isAutoFixing ? (
 | 
				
			||||||
 | 
					                                <Spinner size="small" />
 | 
				
			||||||
 | 
					                            ) : (
 | 
				
			||||||
 | 
					                                'Try auto-fix'
 | 
				
			||||||
 | 
					                            )}
 | 
				
			||||||
 | 
					                        </Button>
 | 
				
			||||||
 | 
					                    ) : keepDialogAfterImport ? (
 | 
				
			||||||
                        <Button
 | 
					                        <Button
 | 
				
			||||||
                            type="button"
 | 
					                            type="button"
 | 
				
			||||||
                            variant="default"
 | 
					                            variant="default"
 | 
				
			||||||
                            disabled={
 | 
					                            disabled={
 | 
				
			||||||
                                scriptResult.trim().length === 0 ||
 | 
					                                scriptResult.trim().length === 0 ||
 | 
				
			||||||
                                errorMessage.length > 0
 | 
					                                errorMessage.length > 0 ||
 | 
				
			||||||
 | 
					                                isAutoFixing
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                            onClick={handleImport}
 | 
					                            onClick={handleImport}
 | 
				
			||||||
                        >
 | 
					                        >
 | 
				
			||||||
@@ -425,9 +593,9 @@ 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 ||
 | 
				
			||||||
 | 
					                                    isAutoFixing
 | 
				
			||||||
                                }
 | 
					                                }
 | 
				
			||||||
                                onClick={handleImport}
 | 
					                                onClick={handleImport}
 | 
				
			||||||
                            >
 | 
					                            >
 | 
				
			||||||
@@ -456,8 +624,14 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
				
			|||||||
        errorMessage.length,
 | 
					        errorMessage.length,
 | 
				
			||||||
        scriptResult,
 | 
					        scriptResult,
 | 
				
			||||||
        showCheckJsonButton,
 | 
					        showCheckJsonButton,
 | 
				
			||||||
 | 
					        isCheckingJson,
 | 
				
			||||||
 | 
					        handleCheckJson,
 | 
				
			||||||
        goBack,
 | 
					        goBack,
 | 
				
			||||||
        t,
 | 
					        t,
 | 
				
			||||||
 | 
					        importMethod,
 | 
				
			||||||
 | 
					        isAutoFixing,
 | 
				
			||||||
 | 
					        showAutoFixButton,
 | 
				
			||||||
 | 
					        handleAutoFix,
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -0,0 +1,179 @@
 | 
				
			|||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import logo from '@/assets/logo-2.png';
 | 
				
			||||||
 | 
					import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
 | 
				
			||||||
 | 
					import { DatabaseType } from '@/lib/domain/database-type';
 | 
				
			||||||
 | 
					import { databaseSecondaryLogoMap } from '@/lib/databases';
 | 
				
			||||||
 | 
					import type { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    databaseEditionToImageMap,
 | 
				
			||||||
 | 
					    databaseEditionToLabelMap,
 | 
				
			||||||
 | 
					    databaseTypeToEditionMap,
 | 
				
			||||||
 | 
					} from '@/lib/domain/database-edition';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    Avatar,
 | 
				
			||||||
 | 
					    AvatarFallback,
 | 
				
			||||||
 | 
					    AvatarImage,
 | 
				
			||||||
 | 
					} from '@/components/avatar/avatar';
 | 
				
			||||||
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
 | 
					import { Code } from 'lucide-react';
 | 
				
			||||||
 | 
					import { SmartQueryInstructions } from './instructions/smart-query-instructions';
 | 
				
			||||||
 | 
					import { DDLInstructions } from './instructions/ddl-instructions';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DatabasesWithoutDDLInstructions: DatabaseType[] = [
 | 
				
			||||||
 | 
					    DatabaseType.CLICKHOUSE,
 | 
				
			||||||
 | 
					    DatabaseType.ORACLE,
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface InstructionsSectionProps {
 | 
				
			||||||
 | 
					    databaseType: DatabaseType;
 | 
				
			||||||
 | 
					    databaseEdition?: DatabaseEdition;
 | 
				
			||||||
 | 
					    setDatabaseEdition: React.Dispatch<
 | 
				
			||||||
 | 
					        React.SetStateAction<DatabaseEdition | undefined>
 | 
				
			||||||
 | 
					    >;
 | 
				
			||||||
 | 
					    importMethod: 'query' | 'ddl';
 | 
				
			||||||
 | 
					    setImportMethod: (method: 'query' | 'ddl') => void;
 | 
				
			||||||
 | 
					    showSSMSInfoDialog: boolean;
 | 
				
			||||||
 | 
					    setShowSSMSInfoDialog: (show: boolean) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const InstructionsSection: React.FC<InstructionsSectionProps> = ({
 | 
				
			||||||
 | 
					    databaseType,
 | 
				
			||||||
 | 
					    databaseEdition,
 | 
				
			||||||
 | 
					    setDatabaseEdition,
 | 
				
			||||||
 | 
					    importMethod,
 | 
				
			||||||
 | 
					    setImportMethod,
 | 
				
			||||||
 | 
					    setShowSSMSInfoDialog,
 | 
				
			||||||
 | 
					    showSSMSInfoDialog,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					    const { t } = useTranslation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div className="flex w-full flex-1 flex-col gap-4">
 | 
				
			||||||
 | 
					            {databaseTypeToEditionMap[databaseType].length > 0 ? (
 | 
				
			||||||
 | 
					                <div className="flex flex-col gap-1">
 | 
				
			||||||
 | 
					                    <p className="text-sm leading-6 text-primary">
 | 
				
			||||||
 | 
					                        {t(
 | 
				
			||||||
 | 
					                            'new_diagram_dialog.import_database.database_edition'
 | 
				
			||||||
 | 
					                        )}
 | 
				
			||||||
 | 
					                    </p>
 | 
				
			||||||
 | 
					                    <ToggleGroup
 | 
				
			||||||
 | 
					                        type="single"
 | 
				
			||||||
 | 
					                        className="ml-1 flex-wrap justify-start gap-2"
 | 
				
			||||||
 | 
					                        value={!databaseEdition ? 'regular' : databaseEdition}
 | 
				
			||||||
 | 
					                        onValueChange={(value) => {
 | 
				
			||||||
 | 
					                            setDatabaseEdition(
 | 
				
			||||||
 | 
					                                value === 'regular'
 | 
				
			||||||
 | 
					                                    ? undefined
 | 
				
			||||||
 | 
					                                    : (value as DatabaseEdition)
 | 
				
			||||||
 | 
					                            );
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                        <ToggleGroupItem
 | 
				
			||||||
 | 
					                            value="regular"
 | 
				
			||||||
 | 
					                            variant="outline"
 | 
				
			||||||
 | 
					                            className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                            <Avatar className="size-4 rounded-none">
 | 
				
			||||||
 | 
					                                <AvatarImage
 | 
				
			||||||
 | 
					                                    src={databaseSecondaryLogoMap[databaseType]}
 | 
				
			||||||
 | 
					                                    alt="Regular"
 | 
				
			||||||
 | 
					                                />
 | 
				
			||||||
 | 
					                                <AvatarFallback>Regular</AvatarFallback>
 | 
				
			||||||
 | 
					                            </Avatar>
 | 
				
			||||||
 | 
					                            Regular
 | 
				
			||||||
 | 
					                        </ToggleGroupItem>
 | 
				
			||||||
 | 
					                        {databaseTypeToEditionMap[databaseType].map(
 | 
				
			||||||
 | 
					                            (edition) => (
 | 
				
			||||||
 | 
					                                <ToggleGroupItem
 | 
				
			||||||
 | 
					                                    value={edition}
 | 
				
			||||||
 | 
					                                    key={edition}
 | 
				
			||||||
 | 
					                                    variant="outline"
 | 
				
			||||||
 | 
					                                    className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
 | 
				
			||||||
 | 
					                                >
 | 
				
			||||||
 | 
					                                    <Avatar className="size-4">
 | 
				
			||||||
 | 
					                                        <AvatarImage
 | 
				
			||||||
 | 
					                                            src={
 | 
				
			||||||
 | 
					                                                databaseEditionToImageMap[
 | 
				
			||||||
 | 
					                                                    edition
 | 
				
			||||||
 | 
					                                                ]
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                            alt={
 | 
				
			||||||
 | 
					                                                databaseEditionToLabelMap[
 | 
				
			||||||
 | 
					                                                    edition
 | 
				
			||||||
 | 
					                                                ]
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                        />
 | 
				
			||||||
 | 
					                                        <AvatarFallback>
 | 
				
			||||||
 | 
					                                            {databaseEditionToLabelMap[edition]}
 | 
				
			||||||
 | 
					                                        </AvatarFallback>
 | 
				
			||||||
 | 
					                                    </Avatar>
 | 
				
			||||||
 | 
					                                    {databaseEditionToLabelMap[edition]}
 | 
				
			||||||
 | 
					                                </ToggleGroupItem>
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
 | 
					                        )}
 | 
				
			||||||
 | 
					                    </ToggleGroup>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            ) : null}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {DatabasesWithoutDDLInstructions.includes(databaseType) ? null : (
 | 
				
			||||||
 | 
					                <div className="flex flex-col gap-1">
 | 
				
			||||||
 | 
					                    <p className="text-sm leading-6 text-primary">
 | 
				
			||||||
 | 
					                        How would you like to import?
 | 
				
			||||||
 | 
					                    </p>
 | 
				
			||||||
 | 
					                    <ToggleGroup
 | 
				
			||||||
 | 
					                        type="single"
 | 
				
			||||||
 | 
					                        className="ml-1 flex-wrap justify-start gap-2"
 | 
				
			||||||
 | 
					                        value={importMethod}
 | 
				
			||||||
 | 
					                        onValueChange={(value) => {
 | 
				
			||||||
 | 
					                            let selectedImportMethod: 'query' | 'ddl' = 'query';
 | 
				
			||||||
 | 
					                            if (value) {
 | 
				
			||||||
 | 
					                                selectedImportMethod = value as 'query' | 'ddl';
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            setImportMethod(selectedImportMethod);
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                        <ToggleGroupItem
 | 
				
			||||||
 | 
					                            value="query"
 | 
				
			||||||
 | 
					                            variant="outline"
 | 
				
			||||||
 | 
					                            className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                            <Avatar className="h-3 w-4 rounded-none">
 | 
				
			||||||
 | 
					                                <AvatarImage src={logo} alt="query" />
 | 
				
			||||||
 | 
					                                <AvatarFallback>Query</AvatarFallback>
 | 
				
			||||||
 | 
					                            </Avatar>
 | 
				
			||||||
 | 
					                            Smart Query
 | 
				
			||||||
 | 
					                        </ToggleGroupItem>
 | 
				
			||||||
 | 
					                        <ToggleGroupItem
 | 
				
			||||||
 | 
					                            value="ddl"
 | 
				
			||||||
 | 
					                            variant="outline"
 | 
				
			||||||
 | 
					                            className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                            <Avatar className="size-4 rounded-none">
 | 
				
			||||||
 | 
					                                <Code size={16} />
 | 
				
			||||||
 | 
					                            </Avatar>
 | 
				
			||||||
 | 
					                            SQL Script
 | 
				
			||||||
 | 
					                        </ToggleGroupItem>
 | 
				
			||||||
 | 
					                    </ToggleGroup>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            <div className="flex flex-col gap-2">
 | 
				
			||||||
 | 
					                <div className="text-sm font-semibold">Instructions:</div>
 | 
				
			||||||
 | 
					                {importMethod === 'query' ? (
 | 
				
			||||||
 | 
					                    <SmartQueryInstructions
 | 
				
			||||||
 | 
					                        databaseType={databaseType}
 | 
				
			||||||
 | 
					                        databaseEdition={databaseEdition}
 | 
				
			||||||
 | 
					                        showSSMSInfoDialog={showSSMSInfoDialog}
 | 
				
			||||||
 | 
					                        setShowSSMSInfoDialog={setShowSSMSInfoDialog}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                ) : (
 | 
				
			||||||
 | 
					                    <DDLInstructions
 | 
				
			||||||
 | 
					                        databaseType={databaseType}
 | 
				
			||||||
 | 
					                        databaseEdition={databaseEdition}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { CodeSnippet } from '@/components/code-snippet/code-snippet';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface DDLInstructionStepProps {
 | 
				
			||||||
 | 
					    index: number;
 | 
				
			||||||
 | 
					    text: string;
 | 
				
			||||||
 | 
					    code?: string;
 | 
				
			||||||
 | 
					    example?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DDLInstructionStep: React.FC<DDLInstructionStepProps> = ({
 | 
				
			||||||
 | 
					    index,
 | 
				
			||||||
 | 
					    text,
 | 
				
			||||||
 | 
					    code,
 | 
				
			||||||
 | 
					    example,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <div className="flex flex-col gap-1">
 | 
				
			||||||
 | 
					            <div className="flex flex-col gap-1 text-sm text-primary">
 | 
				
			||||||
 | 
					                <div>
 | 
				
			||||||
 | 
					                    <span className="font-medium">{index}.</span> {text}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                {code ? (
 | 
				
			||||||
 | 
					                    <div className="h-[60px]">
 | 
				
			||||||
 | 
					                        <CodeSnippet
 | 
				
			||||||
 | 
					                            className="h-full"
 | 
				
			||||||
 | 
					                            code={code}
 | 
				
			||||||
 | 
					                            language={'shell'}
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                ) : null}
 | 
				
			||||||
 | 
					                {example ? (
 | 
				
			||||||
 | 
					                    <>
 | 
				
			||||||
 | 
					                        <div className="my-2">Example:</div>
 | 
				
			||||||
 | 
					                        <div className="h-[60px]">
 | 
				
			||||||
 | 
					                            <CodeSnippet
 | 
				
			||||||
 | 
					                                className="h-full"
 | 
				
			||||||
 | 
					                                code={example}
 | 
				
			||||||
 | 
					                                language={'shell'}
 | 
				
			||||||
 | 
					                            />
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </>
 | 
				
			||||||
 | 
					                ) : null}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,118 @@
 | 
				
			|||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { DatabaseType } from '@/lib/domain/database-type';
 | 
				
			||||||
 | 
					import type { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
				
			||||||
 | 
					import { DDLInstructionStep } from './ddl-instruction-step';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface DDLInstruction {
 | 
				
			||||||
 | 
					    text: string;
 | 
				
			||||||
 | 
					    code?: string;
 | 
				
			||||||
 | 
					    example?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DDLInstructionsMap: Record<DatabaseType, DDLInstruction[]> = {
 | 
				
			||||||
 | 
					    [DatabaseType.GENERIC]: [],
 | 
				
			||||||
 | 
					    [DatabaseType.MYSQL]: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Install mysqldump.',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Execute the following command in your terminal (prefix with sudo on Linux if needed):',
 | 
				
			||||||
 | 
					            code: `mysqldump -h <host> -u <username>\n-P <port> -p --no-data\n<database_name> > <output_path>`,
 | 
				
			||||||
 | 
					            example: `mysqldump -h localhost -u root -P\n3306 -p --no-data my_db >\nschema_export.sql`,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Open the exported SQL file, copy its contents, and paste them here.',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    [DatabaseType.POSTGRESQL]: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Install pg_dump.',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Execute the following command in your terminal (prefix with sudo on Linux if needed):',
 | 
				
			||||||
 | 
					            code: `pg_dump -h <host> -p <port> -d <database_name> \n  -U <username> -s -F p -E UTF-8 \n  -f <output_file_path>`,
 | 
				
			||||||
 | 
					            example: `pg_dump -h localhost -p 5432 -d my_db \n  -U postgres -s -F p -E UTF-8 \n  -f schema_export.sql`,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Open the exported SQL file, copy its contents, and paste them here.',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    [DatabaseType.SQLITE]: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Install sqlite3.',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Execute the following command in your terminal:',
 | 
				
			||||||
 | 
					            code: `sqlite3 <database_file_path>\n.dump > <output_file_path>`,
 | 
				
			||||||
 | 
					            example: `sqlite3 my_db.db\n.dump > schema_export.sql`,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Open the exported SQL file, copy its contents, and paste them here.',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    [DatabaseType.SQL_SERVER]: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Download and install SQL Server Management Studio (SSMS).',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Connect to your SQL Server instance using SSMS.',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Right-click on the database you want to export and select Script Database as > CREATE To > New Query Editor Window.',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Copy the generated script and paste it here.',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    [DatabaseType.CLICKHOUSE]: [],
 | 
				
			||||||
 | 
					    [DatabaseType.COCKROACHDB]: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Install pg_dump.',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Execute the following command in your terminal (prefix with sudo on Linux if needed):',
 | 
				
			||||||
 | 
					            code: `pg_dump -h <host> -p <port> -d <database_name> \n  -U <username> -s -F p -E UTF-8 \n  -f <output_file_path>`,
 | 
				
			||||||
 | 
					            example: `pg_dump -h localhost -p 5432 -d my_db \n  -U postgres -s -F p -E UTF-8 \n  -f schema_export.sql`,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Open the exported SQL file, copy its contents, and paste them here.',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    [DatabaseType.MARIADB]: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Install mysqldump.',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Execute the following command in your terminal (prefix with sudo on Linux if needed):',
 | 
				
			||||||
 | 
					            code: `mysqldump -h <host> -u <username>\n-P <port> -p --no-data\n<database_name> > <output_path>`,
 | 
				
			||||||
 | 
					            example: `mysqldump -h localhost -u root -P\n3306 -p --no-data my_db >\nschema_export.sql`,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            text: 'Open the exported SQL file, copy its contents, and paste them here.',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					    [DatabaseType.ORACLE]: [],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface DDLInstructionsProps {
 | 
				
			||||||
 | 
					    databaseType: DatabaseType;
 | 
				
			||||||
 | 
					    databaseEdition?: DatabaseEdition;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DDLInstructions: React.FC<DDLInstructionsProps> = ({
 | 
				
			||||||
 | 
					    databaseType,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					            {DDLInstructionsMap[databaseType].map((instruction, index) => (
 | 
				
			||||||
 | 
					                <DDLInstructionStep
 | 
				
			||||||
 | 
					                    key={index}
 | 
				
			||||||
 | 
					                    index={index + 1}
 | 
				
			||||||
 | 
					                    text={instruction.text}
 | 
				
			||||||
 | 
					                    code={instruction.code}
 | 
				
			||||||
 | 
					                    example={instruction.example}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					            ))}
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -0,0 +1,147 @@
 | 
				
			|||||||
 | 
					import React, { useEffect, useMemo, useState } from 'react';
 | 
				
			||||||
 | 
					import { DatabaseType } from '@/lib/domain/database-type';
 | 
				
			||||||
 | 
					import { CodeSnippet } from '@/components/code-snippet/code-snippet';
 | 
				
			||||||
 | 
					import type { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
				
			||||||
 | 
					import { SSMSInfo } from './ssms-info/ssms-info';
 | 
				
			||||||
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
 | 
					import { Tabs, TabsList, TabsTrigger } from '@/components/tabs/tabs';
 | 
				
			||||||
 | 
					import type { DatabaseClient } from '@/lib/domain/database-clients';
 | 
				
			||||||
 | 
					import { minimizeQuery } from '@/lib/data/import-metadata/utils';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    databaseClientToLabelMap,
 | 
				
			||||||
 | 
					    databaseTypeToClientsMap,
 | 
				
			||||||
 | 
					    databaseEditionToClientsMap,
 | 
				
			||||||
 | 
					} from '@/lib/domain/database-clients';
 | 
				
			||||||
 | 
					import type { ImportMetadataScripts } from '@/lib/data/import-metadata/scripts/scripts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface SmartQueryInstructionsProps {
 | 
				
			||||||
 | 
					    databaseType: DatabaseType;
 | 
				
			||||||
 | 
					    databaseEdition?: DatabaseEdition;
 | 
				
			||||||
 | 
					    showSSMSInfoDialog: boolean;
 | 
				
			||||||
 | 
					    setShowSSMSInfoDialog: (show: boolean) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SmartQueryInstructions: React.FC<SmartQueryInstructionsProps> = ({
 | 
				
			||||||
 | 
					    databaseType,
 | 
				
			||||||
 | 
					    databaseEdition,
 | 
				
			||||||
 | 
					    showSSMSInfoDialog,
 | 
				
			||||||
 | 
					    setShowSSMSInfoDialog,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					    const databaseClients = useMemo(
 | 
				
			||||||
 | 
					        () => [
 | 
				
			||||||
 | 
					            ...databaseTypeToClientsMap[databaseType],
 | 
				
			||||||
 | 
					            ...(databaseEdition
 | 
				
			||||||
 | 
					                ? databaseEditionToClientsMap[databaseEdition]
 | 
				
			||||||
 | 
					                : []),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [databaseType, databaseEdition]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const [databaseClient, setDatabaseClient] = useState<
 | 
				
			||||||
 | 
					        DatabaseClient | undefined
 | 
				
			||||||
 | 
					    >();
 | 
				
			||||||
 | 
					    const { t } = useTranslation();
 | 
				
			||||||
 | 
					    const [importMetadataScripts, setImportMetadataScripts] =
 | 
				
			||||||
 | 
					        useState<ImportMetadataScripts | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const code = useMemo(
 | 
				
			||||||
 | 
					        () =>
 | 
				
			||||||
 | 
					            (databaseClients.length > 0
 | 
				
			||||||
 | 
					                ? importMetadataScripts?.[databaseType]?.({
 | 
				
			||||||
 | 
					                      databaseEdition,
 | 
				
			||||||
 | 
					                      databaseClient,
 | 
				
			||||||
 | 
					                  })
 | 
				
			||||||
 | 
					                : importMetadataScripts?.[databaseType]?.({
 | 
				
			||||||
 | 
					                      databaseEdition,
 | 
				
			||||||
 | 
					                  })) ?? '',
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            databaseType,
 | 
				
			||||||
 | 
					            databaseEdition,
 | 
				
			||||||
 | 
					            databaseClients,
 | 
				
			||||||
 | 
					            importMetadataScripts,
 | 
				
			||||||
 | 
					            databaseClient,
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        const loadScripts = async () => {
 | 
				
			||||||
 | 
					            const { importMetadataScripts } = await import(
 | 
				
			||||||
 | 
					                '@/lib/data/import-metadata/scripts/scripts'
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            setImportMetadataScripts(importMetadataScripts);
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        loadScripts();
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					            <div className="flex flex-col gap-1">
 | 
				
			||||||
 | 
					                <div className="flex flex-col gap-1 text-sm text-primary">
 | 
				
			||||||
 | 
					                    <div>
 | 
				
			||||||
 | 
					                        <span className="font-medium">1.</span>{' '}
 | 
				
			||||||
 | 
					                        {t('new_diagram_dialog.import_database.step_1')}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                    {databaseType === DatabaseType.SQL_SERVER && (
 | 
				
			||||||
 | 
					                        <SSMSInfo
 | 
				
			||||||
 | 
					                            open={showSSMSInfoDialog}
 | 
				
			||||||
 | 
					                            setOpen={setShowSSMSInfoDialog}
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                {databaseClients.length > 0 ? (
 | 
				
			||||||
 | 
					                    <Tabs
 | 
				
			||||||
 | 
					                        value={!databaseClient ? 'dbclient' : databaseClient}
 | 
				
			||||||
 | 
					                        onValueChange={(value) => {
 | 
				
			||||||
 | 
					                            setDatabaseClient(
 | 
				
			||||||
 | 
					                                value === 'dbclient'
 | 
				
			||||||
 | 
					                                    ? undefined
 | 
				
			||||||
 | 
					                                    : (value as DatabaseClient)
 | 
				
			||||||
 | 
					                            );
 | 
				
			||||||
 | 
					                        }}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                        <div className="flex flex-1">
 | 
				
			||||||
 | 
					                            <TabsList className="h-8 justify-start rounded-none rounded-t-sm ">
 | 
				
			||||||
 | 
					                                <TabsTrigger
 | 
				
			||||||
 | 
					                                    value="dbclient"
 | 
				
			||||||
 | 
					                                    className="h-6 w-20"
 | 
				
			||||||
 | 
					                                >
 | 
				
			||||||
 | 
					                                    DB Client
 | 
				
			||||||
 | 
					                                </TabsTrigger>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                {databaseClients?.map((client) => (
 | 
				
			||||||
 | 
					                                    <TabsTrigger
 | 
				
			||||||
 | 
					                                        key={client}
 | 
				
			||||||
 | 
					                                        value={client}
 | 
				
			||||||
 | 
					                                        className="h-6 !w-20"
 | 
				
			||||||
 | 
					                                    >
 | 
				
			||||||
 | 
					                                        {databaseClientToLabelMap[client]}
 | 
				
			||||||
 | 
					                                    </TabsTrigger>
 | 
				
			||||||
 | 
					                                )) ?? []}
 | 
				
			||||||
 | 
					                            </TabsList>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                        <CodeSnippet
 | 
				
			||||||
 | 
					                            className="h-40 w-full md:h-[200px]"
 | 
				
			||||||
 | 
					                            loading={!importMetadataScripts}
 | 
				
			||||||
 | 
					                            code={minimizeQuery(code)}
 | 
				
			||||||
 | 
					                            codeToCopy={code}
 | 
				
			||||||
 | 
					                            language={databaseClient ? 'shell' : 'sql'}
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                    </Tabs>
 | 
				
			||||||
 | 
					                ) : (
 | 
				
			||||||
 | 
					                    <CodeSnippet
 | 
				
			||||||
 | 
					                        className="h-40 w-full flex-auto md:h-[200px]"
 | 
				
			||||||
 | 
					                        loading={!importMetadataScripts}
 | 
				
			||||||
 | 
					                        code={minimizeQuery(code)}
 | 
				
			||||||
 | 
					                        codeToCopy={code}
 | 
				
			||||||
 | 
					                        language="sql"
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div className="flex flex-col gap-1">
 | 
				
			||||||
 | 
					                <p className="text-sm text-primary">
 | 
				
			||||||
 | 
					                    <span className="font-medium">2.</span>{' '}
 | 
				
			||||||
 | 
					                    {t('new_diagram_dialog.import_database.step_2')}
 | 
				
			||||||
 | 
					                </p>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -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(
 | 
				
			||||||
							
								
								
									
										179
									
								
								src/dialogs/common/import-database/sql-validation-status.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,179 @@
 | 
				
			|||||||
 | 
					import React, { useMemo } from 'react';
 | 
				
			||||||
 | 
					import { CheckCircle, AlertTriangle, MessageCircleWarning } from 'lucide-react';
 | 
				
			||||||
 | 
					import { Alert, AlertDescription } from '@/components/alert/alert';
 | 
				
			||||||
 | 
					import type { ValidationResult } from '@/lib/data/sql-import/sql-validator';
 | 
				
			||||||
 | 
					import { Separator } from '@/components/separator/separator';
 | 
				
			||||||
 | 
					import { ScrollArea } from '@/components/scroll-area/scroll-area';
 | 
				
			||||||
 | 
					import { Spinner } from '@/components/spinner/spinner';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SQLValidationStatusProps {
 | 
				
			||||||
 | 
					    validation?: ValidationResult | null;
 | 
				
			||||||
 | 
					    errorMessage: string;
 | 
				
			||||||
 | 
					    isAutoFixing?: boolean;
 | 
				
			||||||
 | 
					    onErrorClick?: (line: number) => void;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({
 | 
				
			||||||
 | 
					    validation,
 | 
				
			||||||
 | 
					    errorMessage,
 | 
				
			||||||
 | 
					    isAutoFixing = false,
 | 
				
			||||||
 | 
					    onErrorClick,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					    const hasErrors = useMemo(
 | 
				
			||||||
 | 
					        () => validation?.errors.length && validation.errors.length > 0,
 | 
				
			||||||
 | 
					        [validation?.errors]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const hasWarnings = useMemo(
 | 
				
			||||||
 | 
					        () => validation?.warnings && validation.warnings.length > 0,
 | 
				
			||||||
 | 
					        [validation?.warnings]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const wasAutoFixed = useMemo(
 | 
				
			||||||
 | 
					        () =>
 | 
				
			||||||
 | 
					            validation?.warnings?.some((w) =>
 | 
				
			||||||
 | 
					                w.message.includes('Auto-fixed')
 | 
				
			||||||
 | 
					            ) || false,
 | 
				
			||||||
 | 
					        [validation?.warnings]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!validation && !errorMessage && !isAutoFixing) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (isAutoFixing) {
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            <>
 | 
				
			||||||
 | 
					                <Separator className="mb-1 mt-2" />
 | 
				
			||||||
 | 
					                <div className="rounded-md border border-sky-200 bg-sky-50 dark:border-sky-800 dark:bg-sky-950">
 | 
				
			||||||
 | 
					                    <div className="space-y-3 p-3 pt-2 text-sky-700 dark:text-sky-300">
 | 
				
			||||||
 | 
					                        <div className="flex items-start gap-2">
 | 
				
			||||||
 | 
					                            <Spinner className="mt-0.5 size-4 shrink-0 text-sky-700 dark:text-sky-300" />
 | 
				
			||||||
 | 
					                            <div className="flex-1 text-sm text-sky-700 dark:text-sky-300">
 | 
				
			||||||
 | 
					                                Auto-fixing SQL syntax errors...
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // If we have parser errors (errorMessage) after validation
 | 
				
			||||||
 | 
					    if (errorMessage && !hasErrors) {
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            <>
 | 
				
			||||||
 | 
					                <Separator className="mb-1 mt-2" />
 | 
				
			||||||
 | 
					                <div className="mb-1 flex shrink-0 items-center gap-2">
 | 
				
			||||||
 | 
					                    <p className="text-xs text-red-700">{errorMessage}</p>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					            <Separator className="mb-1 mt-2" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {hasErrors ? (
 | 
				
			||||||
 | 
					                <div className="rounded-md border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950">
 | 
				
			||||||
 | 
					                    <ScrollArea className="h-24">
 | 
				
			||||||
 | 
					                        <div className="space-y-3 p-3 pt-2 text-red-700 dark:text-red-300">
 | 
				
			||||||
 | 
					                            {validation?.errors
 | 
				
			||||||
 | 
					                                .slice(0, 3)
 | 
				
			||||||
 | 
					                                .map((error, idx) => (
 | 
				
			||||||
 | 
					                                    <div
 | 
				
			||||||
 | 
					                                        key={idx}
 | 
				
			||||||
 | 
					                                        className="flex items-start gap-2"
 | 
				
			||||||
 | 
					                                    >
 | 
				
			||||||
 | 
					                                        <MessageCircleWarning className="mt-0.5 size-4 shrink-0 text-red-700 dark:text-red-300" />
 | 
				
			||||||
 | 
					                                        <div className="flex-1 text-sm text-red-700 dark:text-red-300">
 | 
				
			||||||
 | 
					                                            <button
 | 
				
			||||||
 | 
					                                                onClick={() =>
 | 
				
			||||||
 | 
					                                                    onErrorClick?.(error.line)
 | 
				
			||||||
 | 
					                                                }
 | 
				
			||||||
 | 
					                                                className="rounded font-medium underline hover:text-red-600 focus:outline-none focus:ring-1 focus:ring-red-500 dark:hover:text-red-200"
 | 
				
			||||||
 | 
					                                                type="button"
 | 
				
			||||||
 | 
					                                            >
 | 
				
			||||||
 | 
					                                                Line {error.line}
 | 
				
			||||||
 | 
					                                            </button>
 | 
				
			||||||
 | 
					                                            <span className="mx-1">:</span>
 | 
				
			||||||
 | 
					                                            <span className="text-xs">
 | 
				
			||||||
 | 
					                                                {error.message}
 | 
				
			||||||
 | 
					                                            </span>
 | 
				
			||||||
 | 
					                                            {error.suggestion && (
 | 
				
			||||||
 | 
					                                                <div className="mt-1 flex items-start gap-2">
 | 
				
			||||||
 | 
					                                                    <span className="text-xs font-medium ">
 | 
				
			||||||
 | 
					                                                        {error.suggestion}
 | 
				
			||||||
 | 
					                                                    </span>
 | 
				
			||||||
 | 
					                                                </div>
 | 
				
			||||||
 | 
					                                            )}
 | 
				
			||||||
 | 
					                                        </div>
 | 
				
			||||||
 | 
					                                    </div>
 | 
				
			||||||
 | 
					                                ))}
 | 
				
			||||||
 | 
					                            {validation?.errors &&
 | 
				
			||||||
 | 
					                            validation?.errors.length > 3 ? (
 | 
				
			||||||
 | 
					                                <div className="flex items-center gap-2">
 | 
				
			||||||
 | 
					                                    <MessageCircleWarning className="mt-0.5 size-4 shrink-0 text-red-700 dark:text-red-300" />
 | 
				
			||||||
 | 
					                                    <span className="text-xs font-medium">
 | 
				
			||||||
 | 
					                                        {validation.errors.length - 3} more
 | 
				
			||||||
 | 
					                                        error
 | 
				
			||||||
 | 
					                                        {validation.errors.length - 3 > 1
 | 
				
			||||||
 | 
					                                            ? 's'
 | 
				
			||||||
 | 
					                                            : ''}
 | 
				
			||||||
 | 
					                                    </span>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            ) : null}
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </ScrollArea>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            ) : null}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {wasAutoFixed && !hasErrors ? (
 | 
				
			||||||
 | 
					                <Alert className="border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950">
 | 
				
			||||||
 | 
					                    <CheckCircle className="size-4 text-green-600 dark:text-green-400" />
 | 
				
			||||||
 | 
					                    <AlertDescription className="text-sm text-green-700 dark:text-green-300">
 | 
				
			||||||
 | 
					                        SQL syntax errors were automatically fixed. Your SQL is
 | 
				
			||||||
 | 
					                        now ready to import.
 | 
				
			||||||
 | 
					                    </AlertDescription>
 | 
				
			||||||
 | 
					                </Alert>
 | 
				
			||||||
 | 
					            ) : null}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {hasWarnings && !hasErrors ? (
 | 
				
			||||||
 | 
					                <div className="rounded-md border border-sky-200 bg-sky-50 dark:border-sky-800 dark:bg-sky-950">
 | 
				
			||||||
 | 
					                    <ScrollArea className="h-24">
 | 
				
			||||||
 | 
					                        <div className="space-y-3 p-3 pt-2 text-sky-700 dark:text-sky-300">
 | 
				
			||||||
 | 
					                            <div className="flex items-start gap-2">
 | 
				
			||||||
 | 
					                                <AlertTriangle className="mt-0.5 size-4 shrink-0 text-sky-700 dark:text-sky-300" />
 | 
				
			||||||
 | 
					                                <div className="flex-1 text-sm text-sky-700 dark:text-sky-300">
 | 
				
			||||||
 | 
					                                    <div className="mb-1 font-medium">
 | 
				
			||||||
 | 
					                                        Import Info:
 | 
				
			||||||
 | 
					                                    </div>
 | 
				
			||||||
 | 
					                                    {validation?.warnings.map(
 | 
				
			||||||
 | 
					                                        (warning, idx) => (
 | 
				
			||||||
 | 
					                                            <div
 | 
				
			||||||
 | 
					                                                key={idx}
 | 
				
			||||||
 | 
					                                                className="ml-2 text-xs"
 | 
				
			||||||
 | 
					                                            >
 | 
				
			||||||
 | 
					                                                • {warning.message}
 | 
				
			||||||
 | 
					                                            </div>
 | 
				
			||||||
 | 
					                                        )
 | 
				
			||||||
 | 
					                                    )}
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </ScrollArea>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            ) : null}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {!hasErrors && !hasWarnings && !errorMessage && validation ? (
 | 
				
			||||||
 | 
					                <div className="rounded-md border border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950">
 | 
				
			||||||
 | 
					                    <div className="space-y-3 p-3 pt-2 text-green-700 dark:text-green-300">
 | 
				
			||||||
 | 
					                        <div className="flex items-start gap-2">
 | 
				
			||||||
 | 
					                            <CheckCircle className="mt-0.5 size-4 shrink-0 text-green-700 dark:text-green-300" />
 | 
				
			||||||
 | 
					                            <div className="flex-1 text-sm text-green-700 dark:text-green-300">
 | 
				
			||||||
 | 
					                                SQL syntax validated successfully
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            ) : null}
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
							
								
								
									
										2
									
								
								src/dialogs/common/select-tables/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					export const MAX_TABLES_IN_DIAGRAM = 500;
 | 
				
			||||||
 | 
					export const MAX_TABLES_WITHOUT_SHOWING_FILTER = 50;
 | 
				
			||||||
							
								
								
									
										665
									
								
								src/dialogs/common/select-tables/select-tables.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,665 @@
 | 
				
			|||||||
 | 
					import React, { useState, useMemo, useEffect, useCallback } from 'react';
 | 
				
			||||||
 | 
					import { Button } from '@/components/button/button';
 | 
				
			||||||
 | 
					import { Input } from '@/components/input/input';
 | 
				
			||||||
 | 
					import { Search, AlertCircle, Check, X, View, Table } from 'lucide-react';
 | 
				
			||||||
 | 
					import { Checkbox } from '@/components/checkbox/checkbox';
 | 
				
			||||||
 | 
					import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
 | 
				
			||||||
 | 
					import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
 | 
				
			||||||
 | 
					import { cn } from '@/lib/utils';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    DialogDescription,
 | 
				
			||||||
 | 
					    DialogFooter,
 | 
				
			||||||
 | 
					    DialogHeader,
 | 
				
			||||||
 | 
					    DialogInternalContent,
 | 
				
			||||||
 | 
					    DialogTitle,
 | 
				
			||||||
 | 
					} from '@/components/dialog/dialog';
 | 
				
			||||||
 | 
					import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
 | 
				
			||||||
 | 
					import { generateTableKey } from '@/lib/domain';
 | 
				
			||||||
 | 
					import { Spinner } from '@/components/spinner/spinner';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    Pagination,
 | 
				
			||||||
 | 
					    PaginationContent,
 | 
				
			||||||
 | 
					    PaginationItem,
 | 
				
			||||||
 | 
					    PaginationPrevious,
 | 
				
			||||||
 | 
					    PaginationNext,
 | 
				
			||||||
 | 
					} from '@/components/pagination/pagination';
 | 
				
			||||||
 | 
					import { MAX_TABLES_IN_DIAGRAM } from './constants';
 | 
				
			||||||
 | 
					import { useBreakpoint } from '@/hooks/use-breakpoint';
 | 
				
			||||||
 | 
					import { useTranslation } from 'react-i18next';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface SelectTablesProps {
 | 
				
			||||||
 | 
					    databaseMetadata?: DatabaseMetadata;
 | 
				
			||||||
 | 
					    onImport: ({
 | 
				
			||||||
 | 
					        selectedTables,
 | 
				
			||||||
 | 
					        databaseMetadata,
 | 
				
			||||||
 | 
					    }: {
 | 
				
			||||||
 | 
					        selectedTables?: SelectedTable[];
 | 
				
			||||||
 | 
					        databaseMetadata?: DatabaseMetadata;
 | 
				
			||||||
 | 
					    }) => Promise<void>;
 | 
				
			||||||
 | 
					    onBack: () => void;
 | 
				
			||||||
 | 
					    isLoading?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TABLES_PER_PAGE = 10;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface TableInfo {
 | 
				
			||||||
 | 
					    key: string;
 | 
				
			||||||
 | 
					    schema?: string;
 | 
				
			||||||
 | 
					    tableName: string;
 | 
				
			||||||
 | 
					    fullName: string;
 | 
				
			||||||
 | 
					    type: 'table' | 'view';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SelectTables: React.FC<SelectTablesProps> = ({
 | 
				
			||||||
 | 
					    databaseMetadata,
 | 
				
			||||||
 | 
					    onImport,
 | 
				
			||||||
 | 
					    onBack,
 | 
				
			||||||
 | 
					    isLoading = false,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					    const [searchTerm, setSearchTerm] = useState('');
 | 
				
			||||||
 | 
					    const [currentPage, setCurrentPage] = useState(1);
 | 
				
			||||||
 | 
					    const [showTables, setShowTables] = useState(true);
 | 
				
			||||||
 | 
					    const [showViews, setShowViews] = useState(false);
 | 
				
			||||||
 | 
					    const { t } = useTranslation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Prepare all tables and views with their metadata
 | 
				
			||||||
 | 
					    const allTables = useMemo(() => {
 | 
				
			||||||
 | 
					        const tables: TableInfo[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Add regular tables
 | 
				
			||||||
 | 
					        databaseMetadata?.tables.forEach((table) => {
 | 
				
			||||||
 | 
					            const schema = schemaNameToDomainSchemaName(table.schema);
 | 
				
			||||||
 | 
					            const tableName = table.table;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const key = `table:${generateTableKey({ tableName, schemaName: schema })}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            tables.push({
 | 
				
			||||||
 | 
					                key,
 | 
				
			||||||
 | 
					                schema,
 | 
				
			||||||
 | 
					                tableName,
 | 
				
			||||||
 | 
					                fullName: schema ? `${schema}.${tableName}` : tableName,
 | 
				
			||||||
 | 
					                type: 'table',
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Add views
 | 
				
			||||||
 | 
					        databaseMetadata?.views?.forEach((view) => {
 | 
				
			||||||
 | 
					            const schema = schemaNameToDomainSchemaName(view.schema);
 | 
				
			||||||
 | 
					            const viewName = view.view_name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!viewName) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const key = `view:${generateTableKey({
 | 
				
			||||||
 | 
					                tableName: viewName,
 | 
				
			||||||
 | 
					                schemaName: schema,
 | 
				
			||||||
 | 
					            })}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            tables.push({
 | 
				
			||||||
 | 
					                key,
 | 
				
			||||||
 | 
					                schema,
 | 
				
			||||||
 | 
					                tableName: viewName,
 | 
				
			||||||
 | 
					                fullName:
 | 
				
			||||||
 | 
					                    schema === 'default' ? viewName : `${schema}.${viewName}`,
 | 
				
			||||||
 | 
					                type: 'view',
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return tables.sort((a, b) => a.fullName.localeCompare(b.fullName));
 | 
				
			||||||
 | 
					    }, [databaseMetadata?.tables, databaseMetadata?.views]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Count tables and views separately
 | 
				
			||||||
 | 
					    const tableCount = useMemo(
 | 
				
			||||||
 | 
					        () => allTables.filter((t) => t.type === 'table').length,
 | 
				
			||||||
 | 
					        [allTables]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const viewCount = useMemo(
 | 
				
			||||||
 | 
					        () => allTables.filter((t) => t.type === 'view').length,
 | 
				
			||||||
 | 
					        [allTables]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Initialize selectedTables with all tables (not views) if less than 100 tables
 | 
				
			||||||
 | 
					    const [selectedTables, setSelectedTables] = useState<Set<string>>(() => {
 | 
				
			||||||
 | 
					        const tables = allTables.filter((t) => t.type === 'table');
 | 
				
			||||||
 | 
					        if (tables.length < MAX_TABLES_IN_DIAGRAM) {
 | 
				
			||||||
 | 
					            return new Set(tables.map((t) => t.key));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return new Set();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Filter tables based on search term and type filters
 | 
				
			||||||
 | 
					    const filteredTables = useMemo(() => {
 | 
				
			||||||
 | 
					        let filtered = allTables;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Filter by type
 | 
				
			||||||
 | 
					        filtered = filtered.filter((table) => {
 | 
				
			||||||
 | 
					            if (table.type === 'table' && !showTables) return false;
 | 
				
			||||||
 | 
					            if (table.type === 'view' && !showViews) return false;
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Filter by search term
 | 
				
			||||||
 | 
					        if (searchTerm.trim()) {
 | 
				
			||||||
 | 
					            const searchLower = searchTerm.toLowerCase();
 | 
				
			||||||
 | 
					            filtered = filtered.filter(
 | 
				
			||||||
 | 
					                (table) =>
 | 
				
			||||||
 | 
					                    table.tableName.toLowerCase().includes(searchLower) ||
 | 
				
			||||||
 | 
					                    table.schema?.toLowerCase().includes(searchLower) ||
 | 
				
			||||||
 | 
					                    table.fullName.toLowerCase().includes(searchLower)
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return filtered;
 | 
				
			||||||
 | 
					    }, [allTables, searchTerm, showTables, showViews]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Calculate pagination
 | 
				
			||||||
 | 
					    const totalPages = useMemo(
 | 
				
			||||||
 | 
					        () => Math.max(1, Math.ceil(filteredTables.length / TABLES_PER_PAGE)),
 | 
				
			||||||
 | 
					        [filteredTables.length]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const paginatedTables = useMemo(() => {
 | 
				
			||||||
 | 
					        const startIndex = (currentPage - 1) * TABLES_PER_PAGE;
 | 
				
			||||||
 | 
					        const endIndex = startIndex + TABLES_PER_PAGE;
 | 
				
			||||||
 | 
					        return filteredTables.slice(startIndex, endIndex);
 | 
				
			||||||
 | 
					    }, [filteredTables, currentPage]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Get currently visible selected tables
 | 
				
			||||||
 | 
					    const visibleSelectedTables = useMemo(() => {
 | 
				
			||||||
 | 
					        return paginatedTables.filter((table) => selectedTables.has(table.key));
 | 
				
			||||||
 | 
					    }, [paginatedTables, selectedTables]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const canAddMore = useMemo(
 | 
				
			||||||
 | 
					        () => selectedTables.size < MAX_TABLES_IN_DIAGRAM,
 | 
				
			||||||
 | 
					        [selectedTables.size]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const hasSearchResults = useMemo(
 | 
				
			||||||
 | 
					        () => filteredTables.length > 0,
 | 
				
			||||||
 | 
					        [filteredTables.length]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const allVisibleSelected = useMemo(
 | 
				
			||||||
 | 
					        () =>
 | 
				
			||||||
 | 
					            visibleSelectedTables.length === paginatedTables.length &&
 | 
				
			||||||
 | 
					            paginatedTables.length > 0,
 | 
				
			||||||
 | 
					        [visibleSelectedTables.length, paginatedTables.length]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    const canSelectAllFiltered = useMemo(
 | 
				
			||||||
 | 
					        () =>
 | 
				
			||||||
 | 
					            filteredTables.length > 0 &&
 | 
				
			||||||
 | 
					            filteredTables.some((table) => !selectedTables.has(table.key)) &&
 | 
				
			||||||
 | 
					            canAddMore,
 | 
				
			||||||
 | 
					        [filteredTables, selectedTables, canAddMore]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Reset to first page when search changes
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        setCurrentPage(1);
 | 
				
			||||||
 | 
					    }, [searchTerm]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleTableToggle = useCallback(
 | 
				
			||||||
 | 
					        (tableKey: string) => {
 | 
				
			||||||
 | 
					            const newSelected = new Set(selectedTables);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (newSelected.has(tableKey)) {
 | 
				
			||||||
 | 
					                newSelected.delete(tableKey);
 | 
				
			||||||
 | 
					            } else if (selectedTables.size < MAX_TABLES_IN_DIAGRAM) {
 | 
				
			||||||
 | 
					                newSelected.add(tableKey);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            setSelectedTables(newSelected);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        [selectedTables]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleTogglePageSelection = useCallback(() => {
 | 
				
			||||||
 | 
					        const newSelected = new Set(selectedTables);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (allVisibleSelected) {
 | 
				
			||||||
 | 
					            // Deselect all on current page
 | 
				
			||||||
 | 
					            for (const table of paginatedTables) {
 | 
				
			||||||
 | 
					                newSelected.delete(table.key);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            // Select all on current page
 | 
				
			||||||
 | 
					            for (const table of paginatedTables) {
 | 
				
			||||||
 | 
					                if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break;
 | 
				
			||||||
 | 
					                newSelected.add(table.key);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setSelectedTables(newSelected);
 | 
				
			||||||
 | 
					    }, [allVisibleSelected, paginatedTables, selectedTables]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleSelectAllFiltered = useCallback(() => {
 | 
				
			||||||
 | 
					        const newSelected = new Set(selectedTables);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (const table of filteredTables) {
 | 
				
			||||||
 | 
					            if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break;
 | 
				
			||||||
 | 
					            newSelected.add(table.key);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setSelectedTables(newSelected);
 | 
				
			||||||
 | 
					    }, [filteredTables, selectedTables]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleNextPage = useCallback(() => {
 | 
				
			||||||
 | 
					        if (currentPage < totalPages) {
 | 
				
			||||||
 | 
					            setCurrentPage(currentPage + 1);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }, [currentPage, totalPages]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handlePrevPage = useCallback(() => {
 | 
				
			||||||
 | 
					        if (currentPage > 1) {
 | 
				
			||||||
 | 
					            setCurrentPage(currentPage - 1);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }, [currentPage]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleClearSelection = useCallback(() => {
 | 
				
			||||||
 | 
					        setSelectedTables(new Set());
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleConfirm = useCallback(() => {
 | 
				
			||||||
 | 
					        const selectedTableObjects: SelectedTable[] = Array.from(selectedTables)
 | 
				
			||||||
 | 
					            .map((key): SelectedTable | null => {
 | 
				
			||||||
 | 
					                const table = allTables.find((t) => t.key === key);
 | 
				
			||||||
 | 
					                if (!table) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
 | 
					                    schema: table.schema,
 | 
				
			||||||
 | 
					                    table: table.tableName,
 | 
				
			||||||
 | 
					                    type: table.type,
 | 
				
			||||||
 | 
					                } satisfies SelectedTable;
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .filter((t): t is SelectedTable => t !== null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        onImport({ selectedTables: selectedTableObjects, databaseMetadata });
 | 
				
			||||||
 | 
					    }, [selectedTables, allTables, onImport, databaseMetadata]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { isMd: isDesktop } = useBreakpoint('md');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const renderPagination = useCallback(
 | 
				
			||||||
 | 
					        () => (
 | 
				
			||||||
 | 
					            <Pagination>
 | 
				
			||||||
 | 
					                <PaginationContent>
 | 
				
			||||||
 | 
					                    <PaginationItem>
 | 
				
			||||||
 | 
					                        <PaginationPrevious
 | 
				
			||||||
 | 
					                            onClick={handlePrevPage}
 | 
				
			||||||
 | 
					                            className={cn(
 | 
				
			||||||
 | 
					                                'cursor-pointer',
 | 
				
			||||||
 | 
					                                currentPage === 1 &&
 | 
				
			||||||
 | 
					                                    'pointer-events-none opacity-50'
 | 
				
			||||||
 | 
					                            )}
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                    </PaginationItem>
 | 
				
			||||||
 | 
					                    <PaginationItem>
 | 
				
			||||||
 | 
					                        <span className="px-3 text-sm text-muted-foreground">
 | 
				
			||||||
 | 
					                            Page {currentPage} of {totalPages}
 | 
				
			||||||
 | 
					                        </span>
 | 
				
			||||||
 | 
					                    </PaginationItem>
 | 
				
			||||||
 | 
					                    <PaginationItem>
 | 
				
			||||||
 | 
					                        <PaginationNext
 | 
				
			||||||
 | 
					                            onClick={handleNextPage}
 | 
				
			||||||
 | 
					                            className={cn(
 | 
				
			||||||
 | 
					                                'cursor-pointer',
 | 
				
			||||||
 | 
					                                (currentPage >= totalPages ||
 | 
				
			||||||
 | 
					                                    filteredTables.length === 0) &&
 | 
				
			||||||
 | 
					                                    'pointer-events-none opacity-50'
 | 
				
			||||||
 | 
					                            )}
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                    </PaginationItem>
 | 
				
			||||||
 | 
					                </PaginationContent>
 | 
				
			||||||
 | 
					            </Pagination>
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            currentPage,
 | 
				
			||||||
 | 
					            totalPages,
 | 
				
			||||||
 | 
					            handlePrevPage,
 | 
				
			||||||
 | 
					            handleNextPage,
 | 
				
			||||||
 | 
					            filteredTables.length,
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (isLoading) {
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            <div className="flex h-[400px] items-center justify-center">
 | 
				
			||||||
 | 
					                <div className="text-center">
 | 
				
			||||||
 | 
					                    <Spinner className="mb-4" />
 | 
				
			||||||
 | 
					                    <p className="text-sm text-muted-foreground">
 | 
				
			||||||
 | 
					                        Parsing database metadata...
 | 
				
			||||||
 | 
					                    </p>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
 | 
					            <DialogHeader>
 | 
				
			||||||
 | 
					                <DialogTitle>Select Tables to Import</DialogTitle>
 | 
				
			||||||
 | 
					                <DialogDescription>
 | 
				
			||||||
 | 
					                    {tableCount} {tableCount === 1 ? 'table' : 'tables'}
 | 
				
			||||||
 | 
					                    {viewCount > 0 && (
 | 
				
			||||||
 | 
					                        <>
 | 
				
			||||||
 | 
					                            {' and '}
 | 
				
			||||||
 | 
					                            {viewCount} {viewCount === 1 ? 'view' : 'views'}
 | 
				
			||||||
 | 
					                        </>
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                    {' found. '}
 | 
				
			||||||
 | 
					                    {allTables.length > MAX_TABLES_IN_DIAGRAM
 | 
				
			||||||
 | 
					                        ? `Select up to ${MAX_TABLES_IN_DIAGRAM} to import.`
 | 
				
			||||||
 | 
					                        : 'Choose which ones to import.'}
 | 
				
			||||||
 | 
					                </DialogDescription>
 | 
				
			||||||
 | 
					            </DialogHeader>
 | 
				
			||||||
 | 
					            <DialogInternalContent>
 | 
				
			||||||
 | 
					                <div className="flex h-full flex-col space-y-4">
 | 
				
			||||||
 | 
					                    {/* Warning/Info Banner */}
 | 
				
			||||||
 | 
					                    {allTables.length > MAX_TABLES_IN_DIAGRAM ? (
 | 
				
			||||||
 | 
					                        <div
 | 
				
			||||||
 | 
					                            className={cn(
 | 
				
			||||||
 | 
					                                'flex items-center gap-2 rounded-lg p-3 text-sm',
 | 
				
			||||||
 | 
					                                'bg-amber-50 text-amber-800 dark:bg-amber-950 dark:text-amber-200'
 | 
				
			||||||
 | 
					                            )}
 | 
				
			||||||
 | 
					                        >
 | 
				
			||||||
 | 
					                            <AlertCircle className="size-4 shrink-0" />
 | 
				
			||||||
 | 
					                            <span>
 | 
				
			||||||
 | 
					                                Due to performance limitations, you can import a
 | 
				
			||||||
 | 
					                                maximum of {MAX_TABLES_IN_DIAGRAM} tables.
 | 
				
			||||||
 | 
					                            </span>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    ) : null}
 | 
				
			||||||
 | 
					                    {/* Search Input */}
 | 
				
			||||||
 | 
					                    <div className="relative">
 | 
				
			||||||
 | 
					                        <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
 | 
				
			||||||
 | 
					                        <Input
 | 
				
			||||||
 | 
					                            placeholder="Search tables..."
 | 
				
			||||||
 | 
					                            value={searchTerm}
 | 
				
			||||||
 | 
					                            onChange={(e) => setSearchTerm(e.target.value)}
 | 
				
			||||||
 | 
					                            className="px-9"
 | 
				
			||||||
 | 
					                        />
 | 
				
			||||||
 | 
					                        {searchTerm && (
 | 
				
			||||||
 | 
					                            <button
 | 
				
			||||||
 | 
					                                onClick={() => setSearchTerm('')}
 | 
				
			||||||
 | 
					                                className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
 | 
				
			||||||
 | 
					                            >
 | 
				
			||||||
 | 
					                                <X className="size-4" />
 | 
				
			||||||
 | 
					                            </button>
 | 
				
			||||||
 | 
					                        )}
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    {/* Selection Status and Actions - Responsive layout */}
 | 
				
			||||||
 | 
					                    <div className="flex flex-col items-center gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
 | 
				
			||||||
 | 
					                        {/* Left side: selection count -> checkboxes -> results found */}
 | 
				
			||||||
 | 
					                        <div className="flex flex-col items-center gap-3 text-sm sm:flex-row sm:items-center sm:gap-4">
 | 
				
			||||||
 | 
					                            <div className="flex flex-col items-center gap-1 sm:flex-row sm:items-center sm:gap-4">
 | 
				
			||||||
 | 
					                                <span className="text-center font-medium">
 | 
				
			||||||
 | 
					                                    {selectedTables.size} /{' '}
 | 
				
			||||||
 | 
					                                    {Math.min(
 | 
				
			||||||
 | 
					                                        MAX_TABLES_IN_DIAGRAM,
 | 
				
			||||||
 | 
					                                        allTables.length
 | 
				
			||||||
 | 
					                                    )}{' '}
 | 
				
			||||||
 | 
					                                    items selected
 | 
				
			||||||
 | 
					                                </span>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            <div className="flex items-center gap-3 sm:border-x sm:px-4">
 | 
				
			||||||
 | 
					                                <div className="flex items-center gap-2">
 | 
				
			||||||
 | 
					                                    <Checkbox
 | 
				
			||||||
 | 
					                                        checked={showTables}
 | 
				
			||||||
 | 
					                                        onCheckedChange={(checked) => {
 | 
				
			||||||
 | 
					                                            // Prevent unchecking if it's the only one checked
 | 
				
			||||||
 | 
					                                            if (!checked && !showViews) return;
 | 
				
			||||||
 | 
					                                            setShowTables(!!checked);
 | 
				
			||||||
 | 
					                                        }}
 | 
				
			||||||
 | 
					                                    />
 | 
				
			||||||
 | 
					                                    <Table
 | 
				
			||||||
 | 
					                                        className="size-4"
 | 
				
			||||||
 | 
					                                        strokeWidth={1.5}
 | 
				
			||||||
 | 
					                                    />
 | 
				
			||||||
 | 
					                                    <span>tables</span>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                                <div className="flex items-center gap-2">
 | 
				
			||||||
 | 
					                                    <Checkbox
 | 
				
			||||||
 | 
					                                        checked={showViews}
 | 
				
			||||||
 | 
					                                        onCheckedChange={(checked) => {
 | 
				
			||||||
 | 
					                                            // Prevent unchecking if it's the only one checked
 | 
				
			||||||
 | 
					                                            if (!checked && !showTables) return;
 | 
				
			||||||
 | 
					                                            setShowViews(!!checked);
 | 
				
			||||||
 | 
					                                        }}
 | 
				
			||||||
 | 
					                                    />
 | 
				
			||||||
 | 
					                                    <View
 | 
				
			||||||
 | 
					                                        className="size-4"
 | 
				
			||||||
 | 
					                                        strokeWidth={1.5}
 | 
				
			||||||
 | 
					                                    />
 | 
				
			||||||
 | 
					                                    <span>views</span>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            <span className="hidden text-muted-foreground sm:inline">
 | 
				
			||||||
 | 
					                                {filteredTables.length}{' '}
 | 
				
			||||||
 | 
					                                {filteredTables.length === 1
 | 
				
			||||||
 | 
					                                    ? 'result'
 | 
				
			||||||
 | 
					                                    : 'results'}{' '}
 | 
				
			||||||
 | 
					                                found
 | 
				
			||||||
 | 
					                            </span>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        {/* Right side: action buttons */}
 | 
				
			||||||
 | 
					                        <div className="flex flex-wrap items-center justify-center gap-2">
 | 
				
			||||||
 | 
					                            {hasSearchResults && (
 | 
				
			||||||
 | 
					                                <>
 | 
				
			||||||
 | 
					                                    {/* Show page selection button when not searching and no selection */}
 | 
				
			||||||
 | 
					                                    {!searchTerm &&
 | 
				
			||||||
 | 
					                                        selectedTables.size === 0 && (
 | 
				
			||||||
 | 
					                                            <Button
 | 
				
			||||||
 | 
					                                                variant="outline"
 | 
				
			||||||
 | 
					                                                size="sm"
 | 
				
			||||||
 | 
					                                                onClick={
 | 
				
			||||||
 | 
					                                                    handleTogglePageSelection
 | 
				
			||||||
 | 
					                                                }
 | 
				
			||||||
 | 
					                                                disabled={
 | 
				
			||||||
 | 
					                                                    paginatedTables.length === 0
 | 
				
			||||||
 | 
					                                                }
 | 
				
			||||||
 | 
					                                            >
 | 
				
			||||||
 | 
					                                                {allVisibleSelected
 | 
				
			||||||
 | 
					                                                    ? 'Deselect'
 | 
				
			||||||
 | 
					                                                    : 'Select'}{' '}
 | 
				
			||||||
 | 
					                                                page
 | 
				
			||||||
 | 
					                                            </Button>
 | 
				
			||||||
 | 
					                                        )}
 | 
				
			||||||
 | 
					                                    {/* Show Select all button when there are unselected tables */}
 | 
				
			||||||
 | 
					                                    {canSelectAllFiltered &&
 | 
				
			||||||
 | 
					                                        selectedTables.size === 0 && (
 | 
				
			||||||
 | 
					                                            <Button
 | 
				
			||||||
 | 
					                                                variant="outline"
 | 
				
			||||||
 | 
					                                                size="sm"
 | 
				
			||||||
 | 
					                                                onClick={
 | 
				
			||||||
 | 
					                                                    handleSelectAllFiltered
 | 
				
			||||||
 | 
					                                                }
 | 
				
			||||||
 | 
					                                                disabled={!canSelectAllFiltered}
 | 
				
			||||||
 | 
					                                                title={(() => {
 | 
				
			||||||
 | 
					                                                    const unselectedCount =
 | 
				
			||||||
 | 
					                                                        filteredTables.filter(
 | 
				
			||||||
 | 
					                                                            (table) =>
 | 
				
			||||||
 | 
					                                                                !selectedTables.has(
 | 
				
			||||||
 | 
					                                                                    table.key
 | 
				
			||||||
 | 
					                                                                )
 | 
				
			||||||
 | 
					                                                        ).length;
 | 
				
			||||||
 | 
					                                                    const remainingCapacity =
 | 
				
			||||||
 | 
					                                                        MAX_TABLES_IN_DIAGRAM -
 | 
				
			||||||
 | 
					                                                        selectedTables.size;
 | 
				
			||||||
 | 
					                                                    if (
 | 
				
			||||||
 | 
					                                                        unselectedCount >
 | 
				
			||||||
 | 
					                                                        remainingCapacity
 | 
				
			||||||
 | 
					                                                    ) {
 | 
				
			||||||
 | 
					                                                        return `Can only select ${remainingCapacity} more tables (${MAX_TABLES_IN_DIAGRAM} max limit)`;
 | 
				
			||||||
 | 
					                                                    }
 | 
				
			||||||
 | 
					                                                    return undefined;
 | 
				
			||||||
 | 
					                                                })()}
 | 
				
			||||||
 | 
					                                            >
 | 
				
			||||||
 | 
					                                                {(() => {
 | 
				
			||||||
 | 
					                                                    const unselectedCount =
 | 
				
			||||||
 | 
					                                                        filteredTables.filter(
 | 
				
			||||||
 | 
					                                                            (table) =>
 | 
				
			||||||
 | 
					                                                                !selectedTables.has(
 | 
				
			||||||
 | 
					                                                                    table.key
 | 
				
			||||||
 | 
					                                                                )
 | 
				
			||||||
 | 
					                                                        ).length;
 | 
				
			||||||
 | 
					                                                    const remainingCapacity =
 | 
				
			||||||
 | 
					                                                        MAX_TABLES_IN_DIAGRAM -
 | 
				
			||||||
 | 
					                                                        selectedTables.size;
 | 
				
			||||||
 | 
					                                                    if (
 | 
				
			||||||
 | 
					                                                        unselectedCount >
 | 
				
			||||||
 | 
					                                                        remainingCapacity
 | 
				
			||||||
 | 
					                                                    ) {
 | 
				
			||||||
 | 
					                                                        return `Select ${remainingCapacity} of ${unselectedCount}`;
 | 
				
			||||||
 | 
					                                                    }
 | 
				
			||||||
 | 
					                                                    return `Select all ${unselectedCount}`;
 | 
				
			||||||
 | 
					                                                })()}
 | 
				
			||||||
 | 
					                                            </Button>
 | 
				
			||||||
 | 
					                                        )}
 | 
				
			||||||
 | 
					                                </>
 | 
				
			||||||
 | 
					                            )}
 | 
				
			||||||
 | 
					                            {selectedTables.size > 0 && (
 | 
				
			||||||
 | 
					                                <>
 | 
				
			||||||
 | 
					                                    {/* Show page selection/deselection button when user has selections */}
 | 
				
			||||||
 | 
					                                    {paginatedTables.length > 0 && (
 | 
				
			||||||
 | 
					                                        <Button
 | 
				
			||||||
 | 
					                                            variant="outline"
 | 
				
			||||||
 | 
					                                            size="sm"
 | 
				
			||||||
 | 
					                                            onClick={handleTogglePageSelection}
 | 
				
			||||||
 | 
					                                        >
 | 
				
			||||||
 | 
					                                            {allVisibleSelected
 | 
				
			||||||
 | 
					                                                ? 'Deselect'
 | 
				
			||||||
 | 
					                                                : 'Select'}{' '}
 | 
				
			||||||
 | 
					                                            page
 | 
				
			||||||
 | 
					                                        </Button>
 | 
				
			||||||
 | 
					                                    )}
 | 
				
			||||||
 | 
					                                    <Button
 | 
				
			||||||
 | 
					                                        variant="outline"
 | 
				
			||||||
 | 
					                                        size="sm"
 | 
				
			||||||
 | 
					                                        onClick={handleClearSelection}
 | 
				
			||||||
 | 
					                                    >
 | 
				
			||||||
 | 
					                                        Clear selection
 | 
				
			||||||
 | 
					                                    </Button>
 | 
				
			||||||
 | 
					                                </>
 | 
				
			||||||
 | 
					                            )}
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                {/* Table List */}
 | 
				
			||||||
 | 
					                <div className="flex min-h-[428px] flex-1 flex-col">
 | 
				
			||||||
 | 
					                    {hasSearchResults ? (
 | 
				
			||||||
 | 
					                        <>
 | 
				
			||||||
 | 
					                            <div className="flex-1 py-4">
 | 
				
			||||||
 | 
					                                <div className="space-y-1">
 | 
				
			||||||
 | 
					                                    {paginatedTables.map((table) => {
 | 
				
			||||||
 | 
					                                        const isSelected = selectedTables.has(
 | 
				
			||||||
 | 
					                                            table.key
 | 
				
			||||||
 | 
					                                        );
 | 
				
			||||||
 | 
					                                        const isDisabled =
 | 
				
			||||||
 | 
					                                            !isSelected &&
 | 
				
			||||||
 | 
					                                            selectedTables.size >=
 | 
				
			||||||
 | 
					                                                MAX_TABLES_IN_DIAGRAM;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                        return (
 | 
				
			||||||
 | 
					                                            <div
 | 
				
			||||||
 | 
					                                                key={table.key}
 | 
				
			||||||
 | 
					                                                className={cn(
 | 
				
			||||||
 | 
					                                                    'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
 | 
				
			||||||
 | 
					                                                    {
 | 
				
			||||||
 | 
					                                                        'cursor-not-allowed':
 | 
				
			||||||
 | 
					                                                            isDisabled,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                                        'bg-muted hover:bg-muted/80':
 | 
				
			||||||
 | 
					                                                            isSelected,
 | 
				
			||||||
 | 
					                                                        'hover:bg-accent':
 | 
				
			||||||
 | 
					                                                            !isSelected &&
 | 
				
			||||||
 | 
					                                                            !isDisabled,
 | 
				
			||||||
 | 
					                                                    }
 | 
				
			||||||
 | 
					                                                )}
 | 
				
			||||||
 | 
					                                            >
 | 
				
			||||||
 | 
					                                                <Checkbox
 | 
				
			||||||
 | 
					                                                    checked={isSelected}
 | 
				
			||||||
 | 
					                                                    disabled={isDisabled}
 | 
				
			||||||
 | 
					                                                    onCheckedChange={() =>
 | 
				
			||||||
 | 
					                                                        handleTableToggle(
 | 
				
			||||||
 | 
					                                                            table.key
 | 
				
			||||||
 | 
					                                                        )
 | 
				
			||||||
 | 
					                                                    }
 | 
				
			||||||
 | 
					                                                />
 | 
				
			||||||
 | 
					                                                {table.type === 'view' ? (
 | 
				
			||||||
 | 
					                                                    <View
 | 
				
			||||||
 | 
					                                                        className="size-4"
 | 
				
			||||||
 | 
					                                                        strokeWidth={1.5}
 | 
				
			||||||
 | 
					                                                    />
 | 
				
			||||||
 | 
					                                                ) : (
 | 
				
			||||||
 | 
					                                                    <Table
 | 
				
			||||||
 | 
					                                                        className="size-4"
 | 
				
			||||||
 | 
					                                                        strokeWidth={1.5}
 | 
				
			||||||
 | 
					                                                    />
 | 
				
			||||||
 | 
					                                                )}
 | 
				
			||||||
 | 
					                                                <span className="flex-1">
 | 
				
			||||||
 | 
					                                                    {table.schema ? (
 | 
				
			||||||
 | 
					                                                        <span className="text-muted-foreground">
 | 
				
			||||||
 | 
					                                                            {table.schema}.
 | 
				
			||||||
 | 
					                                                        </span>
 | 
				
			||||||
 | 
					                                                    ) : null}
 | 
				
			||||||
 | 
					                                                    <span className="font-medium">
 | 
				
			||||||
 | 
					                                                        {table.tableName}
 | 
				
			||||||
 | 
					                                                    </span>
 | 
				
			||||||
 | 
					                                                    {table.type === 'view' && (
 | 
				
			||||||
 | 
					                                                        <span className="ml-2 text-xs text-muted-foreground">
 | 
				
			||||||
 | 
					                                                            (view)
 | 
				
			||||||
 | 
					                                                        </span>
 | 
				
			||||||
 | 
					                                                    )}
 | 
				
			||||||
 | 
					                                                </span>
 | 
				
			||||||
 | 
					                                                {isSelected && (
 | 
				
			||||||
 | 
					                                                    <Check className="size-4 text-pink-600" />
 | 
				
			||||||
 | 
					                                                )}
 | 
				
			||||||
 | 
					                                            </div>
 | 
				
			||||||
 | 
					                                        );
 | 
				
			||||||
 | 
					                                    })}
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            </div>
 | 
				
			||||||
 | 
					                        </>
 | 
				
			||||||
 | 
					                    ) : (
 | 
				
			||||||
 | 
					                        <div className="flex h-full items-center justify-center py-4">
 | 
				
			||||||
 | 
					                            <p className="text-sm text-muted-foreground">
 | 
				
			||||||
 | 
					                                {searchTerm
 | 
				
			||||||
 | 
					                                    ? 'No tables found matching your search.'
 | 
				
			||||||
 | 
					                                    : 'Start typing to search for tables...'}
 | 
				
			||||||
 | 
					                            </p>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
 | 
					                {isDesktop ? renderPagination() : null}
 | 
				
			||||||
 | 
					            </DialogInternalContent>
 | 
				
			||||||
 | 
					            <DialogFooter
 | 
				
			||||||
 | 
					                // className={cn(
 | 
				
			||||||
 | 
					                //     'gap-2',
 | 
				
			||||||
 | 
					                //     isDesktop
 | 
				
			||||||
 | 
					                //         ? 'flex items-center justify-between'
 | 
				
			||||||
 | 
					                //         : 'flex flex-col'
 | 
				
			||||||
 | 
					                // )}
 | 
				
			||||||
 | 
					                className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2 md:justify-between md:gap-0"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					                {/* Desktop layout */}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <Button type="button" variant="secondary" onClick={onBack}>
 | 
				
			||||||
 | 
					                    {t('new_diagram_dialog.back')}
 | 
				
			||||||
 | 
					                </Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <Button
 | 
				
			||||||
 | 
					                    onClick={handleConfirm}
 | 
				
			||||||
 | 
					                    disabled={selectedTables.size === 0}
 | 
				
			||||||
 | 
					                    className="bg-pink-500 text-white hover:bg-pink-600"
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                    Import {selectedTables.size} Tables
 | 
				
			||||||
 | 
					                </Button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                {!isDesktop ? renderPagination() : null}
 | 
				
			||||||
 | 
					            </DialogFooter>
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
export enum CreateDiagramDialogStep {
 | 
					export enum CreateDiagramDialogStep {
 | 
				
			||||||
    SELECT_DATABASE = 'SELECT_DATABASE',
 | 
					    SELECT_DATABASE = 'SELECT_DATABASE',
 | 
				
			||||||
    IMPORT_DATABASE = 'IMPORT_DATABASE',
 | 
					    IMPORT_DATABASE = 'IMPORT_DATABASE',
 | 
				
			||||||
 | 
					    SELECT_TABLES = 'SELECT_TABLES',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,8 +15,13 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
				
			|||||||
import { SelectDatabase } from './select-database/select-database';
 | 
					import { SelectDatabase } from './select-database/select-database';
 | 
				
			||||||
import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
 | 
					import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
 | 
				
			||||||
import { ImportDatabase } from '../common/import-database/import-database';
 | 
					import { ImportDatabase } from '../common/import-database/import-database';
 | 
				
			||||||
 | 
					import { SelectTables } from '../common/select-tables/select-tables';
 | 
				
			||||||
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';
 | 
				
			||||||
 | 
					import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
 | 
				
			||||||
 | 
					import { filterMetadataByTables } from '@/lib/data/import-metadata/filter-metadata';
 | 
				
			||||||
 | 
					import { MAX_TABLES_WITHOUT_SHOWING_FILTER } from '../common/select-tables/constants';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface CreateDiagramDialogProps extends BaseDialogProps {}
 | 
					export interface CreateDiagramDialogProps extends BaseDialogProps {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -25,10 +30,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<
 | 
				
			||||||
@@ -40,6 +46,13 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
				
			|||||||
    const { listDiagrams, addDiagram } = useStorage();
 | 
					    const { listDiagrams, addDiagram } = useStorage();
 | 
				
			||||||
    const [diagramNumber, setDiagramNumber] = useState<number>(1);
 | 
					    const [diagramNumber, setDiagramNumber] = useState<number>(1);
 | 
				
			||||||
    const navigate = useNavigate();
 | 
					    const navigate = useNavigate();
 | 
				
			||||||
 | 
					    const [parsedMetadata, setParsedMetadata] = useState<DatabaseMetadata>();
 | 
				
			||||||
 | 
					    const [isParsingMetadata, setIsParsingMetadata] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() => {
 | 
				
			||||||
 | 
					        setDatabaseEdition(undefined);
 | 
				
			||||||
 | 
					        setImportMethod('query');
 | 
				
			||||||
 | 
					    }, [databaseType]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() => {
 | 
					    useEffect(() => {
 | 
				
			||||||
        const fetchDiagrams = async () => {
 | 
					        const fetchDiagrams = async () => {
 | 
				
			||||||
@@ -54,29 +67,63 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
				
			|||||||
        setDatabaseType(DatabaseType.GENERIC);
 | 
					        setDatabaseType(DatabaseType.GENERIC);
 | 
				
			||||||
        setDatabaseEdition(undefined);
 | 
					        setDatabaseEdition(undefined);
 | 
				
			||||||
        setScriptResult('');
 | 
					        setScriptResult('');
 | 
				
			||||||
 | 
					        setImportMethod('query');
 | 
				
			||||||
 | 
					        setParsedMetadata(undefined);
 | 
				
			||||||
    }, [dialog.open]);
 | 
					    }, [dialog.open]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
 | 
					    const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const importNewDiagram = useCallback(async () => {
 | 
					    const importNewDiagram = useCallback(
 | 
				
			||||||
        const databaseMetadata: DatabaseMetadata =
 | 
					        async ({
 | 
				
			||||||
            loadDatabaseMetadata(scriptResult);
 | 
					            selectedTables,
 | 
				
			||||||
 | 
					 | 
				
			||||||
        const diagram = await loadFromDatabaseMetadata({
 | 
					 | 
				
			||||||
            databaseType,
 | 
					 | 
				
			||||||
            databaseMetadata,
 | 
					            databaseMetadata,
 | 
				
			||||||
 | 
					        }: {
 | 
				
			||||||
 | 
					            selectedTables?: SelectedTable[];
 | 
				
			||||||
 | 
					            databaseMetadata?: DatabaseMetadata;
 | 
				
			||||||
 | 
					        } = {}) => {
 | 
				
			||||||
 | 
					            let diagram: Diagram | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (importMethod === 'ddl') {
 | 
				
			||||||
 | 
					                diagram = await sqlImportToDiagram({
 | 
				
			||||||
 | 
					                    sqlContent: scriptResult,
 | 
				
			||||||
 | 
					                    sourceDatabaseType: databaseType,
 | 
				
			||||||
 | 
					                    targetDatabaseType: databaseType,
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                let metadata: DatabaseMetadata | undefined = databaseMetadata;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (!metadata) {
 | 
				
			||||||
 | 
					                    metadata = loadDatabaseMetadata(scriptResult);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (selectedTables && selectedTables.length > 0) {
 | 
				
			||||||
 | 
					                    metadata = filterMetadataByTables({
 | 
				
			||||||
 | 
					                        metadata,
 | 
				
			||||||
 | 
					                        selectedTables,
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                diagram = await loadFromDatabaseMetadata({
 | 
				
			||||||
 | 
					                    databaseType,
 | 
				
			||||||
 | 
					                    databaseMetadata: metadata,
 | 
				
			||||||
                    diagramNumber,
 | 
					                    diagramNumber,
 | 
				
			||||||
                    databaseEdition:
 | 
					                    databaseEdition:
 | 
				
			||||||
                        databaseEdition?.trim().length === 0
 | 
					                        databaseEdition?.trim().length === 0
 | 
				
			||||||
                            ? undefined
 | 
					                            ? undefined
 | 
				
			||||||
                            : databaseEdition,
 | 
					                            : 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,
 | 
				
			||||||
@@ -85,7 +132,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
				
			|||||||
            updateConfig,
 | 
					            updateConfig,
 | 
				
			||||||
            scriptResult,
 | 
					            scriptResult,
 | 
				
			||||||
            diagramNumber,
 | 
					            diagramNumber,
 | 
				
			||||||
    ]);
 | 
					        ]
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const createEmptyDiagram = useCallback(async () => {
 | 
					    const createEmptyDiagram = useCallback(async () => {
 | 
				
			||||||
        const diagram: Diagram = {
 | 
					        const diagram: Diagram = {
 | 
				
			||||||
@@ -101,9 +149,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,12 +164,59 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
				
			|||||||
        navigate,
 | 
					        navigate,
 | 
				
			||||||
        updateConfig,
 | 
					        updateConfig,
 | 
				
			||||||
        diagramNumber,
 | 
					        diagramNumber,
 | 
				
			||||||
 | 
					        openImportDBMLDialog,
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const importNewDiagramOrFilterTables = useCallback(async () => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            setIsParsingMetadata(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (importMethod === 'ddl') {
 | 
				
			||||||
 | 
					                await importNewDiagram();
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // Parse metadata asynchronously to avoid blocking the UI
 | 
				
			||||||
 | 
					                const metadata = await new Promise<DatabaseMetadata>(
 | 
				
			||||||
 | 
					                    (resolve, reject) => {
 | 
				
			||||||
 | 
					                        setTimeout(() => {
 | 
				
			||||||
 | 
					                            try {
 | 
				
			||||||
 | 
					                                const result =
 | 
				
			||||||
 | 
					                                    loadDatabaseMetadata(scriptResult);
 | 
				
			||||||
 | 
					                                resolve(result);
 | 
				
			||||||
 | 
					                            } catch (err) {
 | 
				
			||||||
 | 
					                                reject(err);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }, 0);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                const totalTablesAndViews =
 | 
				
			||||||
 | 
					                    metadata.tables.length + (metadata.views?.length || 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                setParsedMetadata(metadata);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Check if it's a large database that needs table selection
 | 
				
			||||||
 | 
					                if (totalTablesAndViews > MAX_TABLES_WITHOUT_SHOWING_FILTER) {
 | 
				
			||||||
 | 
					                    setStep(CreateDiagramDialogStep.SELECT_TABLES);
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    await importNewDiagram({
 | 
				
			||||||
 | 
					                        databaseMetadata: metadata,
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } finally {
 | 
				
			||||||
 | 
					            setIsParsingMetadata(false);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }, [importMethod, scriptResult, importNewDiagram]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <Dialog
 | 
					        <Dialog
 | 
				
			||||||
            {...dialog}
 | 
					            {...dialog}
 | 
				
			||||||
            onOpenChange={(open) => {
 | 
					            onOpenChange={(open) => {
 | 
				
			||||||
 | 
					                // Don't allow closing while parsing metadata
 | 
				
			||||||
 | 
					                if (isParsingMetadata) {
 | 
				
			||||||
 | 
					                    return;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (!hasExistingDiagram) {
 | 
					                if (!hasExistingDiagram) {
 | 
				
			||||||
                    return;
 | 
					                    return;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@@ -128,8 +227,10 @@ 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}
 | 
				
			||||||
 | 
					                onInteractOutside={(e) => e.preventDefault()}
 | 
				
			||||||
 | 
					                onEscapeKeyDown={(e) => e.preventDefault()}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
                {step === CreateDiagramDialogStep.SELECT_DATABASE ? (
 | 
					                {step === CreateDiagramDialogStep.SELECT_DATABASE ? (
 | 
				
			||||||
                    <SelectDatabase
 | 
					                    <SelectDatabase
 | 
				
			||||||
@@ -141,9 +242,9 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
				
			|||||||
                            setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
 | 
					                            setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    />
 | 
					                    />
 | 
				
			||||||
                ) : (
 | 
					                ) : step === CreateDiagramDialogStep.IMPORT_DATABASE ? (
 | 
				
			||||||
                    <ImportDatabase
 | 
					                    <ImportDatabase
 | 
				
			||||||
                        onImport={importNewDiagram}
 | 
					                        onImport={importNewDiagramOrFilterTables}
 | 
				
			||||||
                        onCreateEmptyDiagram={createEmptyDiagram}
 | 
					                        onCreateEmptyDiagram={createEmptyDiagram}
 | 
				
			||||||
                        databaseEdition={databaseEdition}
 | 
					                        databaseEdition={databaseEdition}
 | 
				
			||||||
                        databaseType={databaseType}
 | 
					                        databaseType={databaseType}
 | 
				
			||||||
@@ -154,8 +255,20 @@ 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}
 | 
				
			||||||
 | 
					                        keepDialogAfterImport={true}
 | 
				
			||||||
                    />
 | 
					                    />
 | 
				
			||||||
                )}
 | 
					                ) : step === CreateDiagramDialogStep.SELECT_TABLES ? (
 | 
				
			||||||
 | 
					                    <SelectTables
 | 
				
			||||||
 | 
					                        isLoading={isParsingMetadata || !parsedMetadata}
 | 
				
			||||||
 | 
					                        databaseMetadata={parsedMetadata}
 | 
				
			||||||
 | 
					                        onImport={importNewDiagram}
 | 
				
			||||||
 | 
					                        onBack={() =>
 | 
				
			||||||
 | 
					                            setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                ) : null}
 | 
				
			||||||
            </DialogContent>
 | 
					            </DialogContent>
 | 
				
			||||||
        </Dialog>
 | 
					        </Dialog>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 | 
					                                        setPrimaryTableId(newTableId);
 | 
				
			||||||
 | 
					                                        if (
 | 
				
			||||||
 | 
					                                            newTableId !==
 | 
				
			||||||
 | 
					                                            preSelectedSourceTableId
 | 
				
			||||||
 | 
					                                        ) {
 | 
				
			||||||
                                            setPrimaryFieldId(undefined);
 | 
					                                            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,16 +85,80 @@ 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">
 | 
				
			||||||
 | 
					                        <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>
 | 
					                                    </div>
 | 
				
			||||||
 | 
					                                    <div className="flex items-start gap-3">
 | 
				
			||||||
 | 
					                                        <Checkbox
 | 
				
			||||||
 | 
					                                            id="transparent-checkbox"
 | 
				
			||||||
 | 
					                                            className="mt-1 data-[state=checked]:border-pink-600 data-[state=checked]:bg-pink-600 data-[state=checked]:text-white"
 | 
				
			||||||
 | 
					                                            checked={transparent}
 | 
				
			||||||
 | 
					                                            onCheckedChange={(value) =>
 | 
				
			||||||
 | 
					                                                setTransparent(value as boolean)
 | 
				
			||||||
 | 
					                                            }
 | 
				
			||||||
 | 
					                                        />
 | 
				
			||||||
 | 
					                                        <div className="flex flex-col">
 | 
				
			||||||
 | 
					                                            <label
 | 
				
			||||||
 | 
					                                                htmlFor="transparent-checkbox"
 | 
				
			||||||
 | 
					                                                className="cursor-pointer font-medium"
 | 
				
			||||||
 | 
					                                            >
 | 
				
			||||||
 | 
					                                                {t(
 | 
				
			||||||
 | 
					                                                    'export_image_dialog.transparent'
 | 
				
			||||||
 | 
					                                                )}
 | 
				
			||||||
 | 
					                                            </label>
 | 
				
			||||||
 | 
					                                            <span className="text-sm text-muted-foreground">
 | 
				
			||||||
 | 
					                                                {t(
 | 
				
			||||||
 | 
					                                                    'export_image_dialog.transparent_description'
 | 
				
			||||||
 | 
					                                                )}
 | 
				
			||||||
 | 
					                                            </span>
 | 
				
			||||||
 | 
					                                        </div>
 | 
				
			||||||
 | 
					                                    </div>
 | 
				
			||||||
 | 
					                                </div>
 | 
				
			||||||
 | 
					                            </AccordionContent>
 | 
				
			||||||
 | 
					                        </AccordionItem>
 | 
				
			||||||
 | 
					                    </Accordion>
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
                <DialogFooter className="flex gap-1 md:justify-between">
 | 
					                <DialogFooter className="flex gap-1 md:justify-between">
 | 
				
			||||||
                    <DialogClose asChild>
 | 
					                    <DialogClose asChild>
 | 
				
			||||||
                        <Button variant="secondary">
 | 
					                        <Button variant="secondary">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,10 +57,19 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
 | 
				
			|||||||
    }, [dialog.open]);
 | 
					    }, [dialog.open]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const importDatabase = useCallback(async () => {
 | 
					    const importDatabase = useCallback(async () => {
 | 
				
			||||||
 | 
					        let diagram: Diagram | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (importMethod === 'ddl') {
 | 
				
			||||||
 | 
					            diagram = await sqlImportToDiagram({
 | 
				
			||||||
 | 
					                sqlContent: scriptResult,
 | 
				
			||||||
 | 
					                sourceDatabaseType: databaseType,
 | 
				
			||||||
 | 
					                targetDatabaseType: databaseType,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
            const databaseMetadata: DatabaseMetadata =
 | 
					            const databaseMetadata: DatabaseMetadata =
 | 
				
			||||||
                loadDatabaseMetadata(scriptResult);
 | 
					                loadDatabaseMetadata(scriptResult);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const diagram = await loadFromDatabaseMetadata({
 | 
					            diagram = await loadFromDatabaseMetadata({
 | 
				
			||||||
                databaseType,
 | 
					                databaseType,
 | 
				
			||||||
                databaseMetadata,
 | 
					                databaseMetadata,
 | 
				
			||||||
                databaseEdition:
 | 
					                databaseEdition:
 | 
				
			||||||
@@ -61,6 +77,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
 | 
				
			|||||||
                        ? undefined
 | 
					                        ? undefined
 | 
				
			||||||
                        : databaseEdition,
 | 
					                        : 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>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										419
									
								
								src/dialogs/import-dbml-dialog/import-dbml-dialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,419 @@
 | 
				
			|||||||
 | 
					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,
 | 
				
			||||||
 | 
					    preprocessDBML,
 | 
				
			||||||
 | 
					} from '@/lib/dbml/dbml-import/dbml-import';
 | 
				
			||||||
 | 
					import { useChartDB } from '@/hooks/use-chartdb';
 | 
				
			||||||
 | 
					import { Parser } from '@dbml/core';
 | 
				
			||||||
 | 
					import { useCanvas } from '@/hooks/use-canvas';
 | 
				
			||||||
 | 
					import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
 | 
				
			||||||
 | 
					import type { DBTable } from '@/lib/domain/db-table';
 | 
				
			||||||
 | 
					import { useToast } from '@/components/toast/use-toast';
 | 
				
			||||||
 | 
					import { Spinner } from '@/components/spinner/spinner';
 | 
				
			||||||
 | 
					import { debounce } from '@/lib/utils';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface DBMLError {
 | 
				
			||||||
 | 
					    message: string;
 | 
				
			||||||
 | 
					    line: number;
 | 
				
			||||||
 | 
					    column: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function parseDBMLError(error: unknown): DBMLError | null {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        if (typeof error === 'string') {
 | 
				
			||||||
 | 
					            const parsed = JSON.parse(error);
 | 
				
			||||||
 | 
					            if (parsed.diags?.[0]) {
 | 
				
			||||||
 | 
					                const diag = parsed.diags[0];
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
 | 
					                    message: diag.message,
 | 
				
			||||||
 | 
					                    line: diag.location.start.line,
 | 
				
			||||||
 | 
					                    column: diag.location.start.column,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else if (error && typeof error === 'object' && 'diags' in error) {
 | 
				
			||||||
 | 
					            const parsed = error as {
 | 
				
			||||||
 | 
					                diags: Array<{
 | 
				
			||||||
 | 
					                    message: string;
 | 
				
			||||||
 | 
					                    location: { start: { line: number; column: number } };
 | 
				
			||||||
 | 
					                }>;
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            if (parsed.diags?.[0]) {
 | 
				
			||||||
 | 
					                return {
 | 
				
			||||||
 | 
					                    message: parsed.diags[0].message,
 | 
				
			||||||
 | 
					                    line: parsed.diags[0].location.start.line,
 | 
				
			||||||
 | 
					                    column: parsed.diags[0].location.start.column,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					        console.error('Error parsing DBML error:', e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 preprocessedContent = preprocessDBML(content);
 | 
				
			||||||
 | 
					                const sanitizedContent = sanitizeDBML(preprocessedContent);
 | 
				
			||||||
 | 
					                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 {
 | 
				
			||||||
 | 
					            const importedDiagram = await importDBMLToDiagram(dbmlContent);
 | 
				
			||||||
 | 
					            const tableIdsToRemove = tables
 | 
				
			||||||
 | 
					                .filter((table) =>
 | 
				
			||||||
 | 
					                    importedDiagram.tables?.some(
 | 
				
			||||||
 | 
					                        (t: DBTable) =>
 | 
				
			||||||
 | 
					                            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: DBTable) =>
 | 
				
			||||||
 | 
					                            table.id === relationship.sourceTableId
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    const targetTable = tables.find(
 | 
				
			||||||
 | 
					                        (table: DBTable) =>
 | 
				
			||||||
 | 
					                            table.id === relationship.targetTableId
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    if (!sourceTable || !targetTable) return true;
 | 
				
			||||||
 | 
					                    const replacementSourceTable = importedDiagram.tables?.find(
 | 
				
			||||||
 | 
					                        (table: DBTable) =>
 | 
				
			||||||
 | 
					                            table.name === sourceTable.name &&
 | 
				
			||||||
 | 
					                            table.schema === sourceTable.schema
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                    const replacementTargetTable = importedDiagram.tables?.find(
 | 
				
			||||||
 | 
					                        (table: DBTable) =>
 | 
				
			||||||
 | 
					                            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(
 | 
				
			||||||
 | 
					        (diagramId: string) => {
 | 
				
			||||||
            if (diagramId) {
 | 
					            if (diagramId) {
 | 
				
			||||||
            updateConfig({ defaultDiagramId: diagramId });
 | 
					                updateConfig({ config: { defaultDiagramId: diagramId } });
 | 
				
			||||||
                navigate(`/diagrams/${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">
 | 
				
			||||||
 | 
					                    {canClose ? (
 | 
				
			||||||
                        <DialogClose asChild>
 | 
					                        <DialogClose asChild>
 | 
				
			||||||
                            <Button type="button" variant="secondary">
 | 
					                            <Button type="button" variant="secondary">
 | 
				
			||||||
                                {t('open_diagram_dialog.cancel')}
 | 
					                                {t('open_diagram_dialog.cancel')}
 | 
				
			||||||
                            </Button>
 | 
					                            </Button>
 | 
				
			||||||
                        </DialogClose>
 | 
					                        </DialogClose>
 | 
				
			||||||
 | 
					                    ) : (
 | 
				
			||||||
 | 
					                        <div />
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
                    <DialogClose asChild>
 | 
					                    <DialogClose asChild>
 | 
				
			||||||
                        <Button
 | 
					                        <Button
 | 
				
			||||||
                            type="submit"
 | 
					                            type="submit"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,7 @@ import { useTranslation } from 'react-i18next';
 | 
				
			|||||||
export interface TableSchemaDialogProps extends BaseDialogProps {
 | 
					export interface TableSchemaDialogProps extends BaseDialogProps {
 | 
				
			||||||
    table?: DBTable;
 | 
					    table?: DBTable;
 | 
				
			||||||
    schemas: DBSchema[];
 | 
					    schemas: DBSchema[];
 | 
				
			||||||
    onConfirm: (schema: string) => void;
 | 
					    onConfirm: ({ schema }: { schema: DBSchema }) => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
					export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
				
			||||||
@@ -31,7 +31,7 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
				
			|||||||
    onConfirm,
 | 
					    onConfirm,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
    const { t } = useTranslation();
 | 
					    const { t } = useTranslation();
 | 
				
			||||||
    const [selectedSchema, setSelectedSchema] = React.useState<string>(
 | 
					    const [selectedSchemaId, setSelectedSchemaId] = React.useState<string>(
 | 
				
			||||||
        table?.schema
 | 
					        table?.schema
 | 
				
			||||||
            ? schemaNameToSchemaId(table.schema)
 | 
					            ? schemaNameToSchemaId(table.schema)
 | 
				
			||||||
            : (schemas?.[0]?.id ?? '')
 | 
					            : (schemas?.[0]?.id ?? '')
 | 
				
			||||||
@@ -39,7 +39,7 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    useEffect(() => {
 | 
					    useEffect(() => {
 | 
				
			||||||
        if (!dialog.open) return;
 | 
					        if (!dialog.open) return;
 | 
				
			||||||
        setSelectedSchema(
 | 
					        setSelectedSchemaId(
 | 
				
			||||||
            table?.schema
 | 
					            table?.schema
 | 
				
			||||||
                ? schemaNameToSchemaId(table.schema)
 | 
					                ? schemaNameToSchemaId(table.schema)
 | 
				
			||||||
                : (schemas?.[0]?.id ?? '')
 | 
					                : (schemas?.[0]?.id ?? '')
 | 
				
			||||||
@@ -48,8 +48,11 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
				
			|||||||
    const { closeTableSchemaDialog } = useDialog();
 | 
					    const { closeTableSchemaDialog } = useDialog();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const handleConfirm = useCallback(() => {
 | 
					    const handleConfirm = useCallback(() => {
 | 
				
			||||||
        onConfirm(selectedSchema);
 | 
					        const schema = schemas.find((s) => s.id === selectedSchemaId);
 | 
				
			||||||
    }, [onConfirm, selectedSchema]);
 | 
					        if (!schema) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        onConfirm({ schema });
 | 
				
			||||||
 | 
					    }, [onConfirm, selectedSchemaId, schemas]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const schemaOptions: SelectBoxOption[] = useMemo(
 | 
					    const schemaOptions: SelectBoxOption[] = useMemo(
 | 
				
			||||||
        () =>
 | 
					        () =>
 | 
				
			||||||
@@ -89,9 +92,9 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
				
			|||||||
                        <SelectBox
 | 
					                        <SelectBox
 | 
				
			||||||
                            options={schemaOptions}
 | 
					                            options={schemaOptions}
 | 
				
			||||||
                            multiple={false}
 | 
					                            multiple={false}
 | 
				
			||||||
                            value={selectedSchema}
 | 
					                            value={selectedSchemaId}
 | 
				
			||||||
                            onChange={(value) =>
 | 
					                            onChange={(value) =>
 | 
				
			||||||
                                setSelectedSchema(value as string)
 | 
					                                setSelectedSchemaId(value as string)
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        />
 | 
					                        />
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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: '!تحدث معنا',
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -75,8 +74,8 @@ export const ar: LanguageTranslation = {
 | 
				
			|||||||
            title: 'مخططات متعددة',
 | 
					            title: 'مخططات متعددة',
 | 
				
			||||||
            description:
 | 
					            description:
 | 
				
			||||||
                '{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
 | 
					                '{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
 | 
				
			||||||
            dont_show_again: 'لا تظهره مجدداً',
 | 
					            // TODO: Translate
 | 
				
			||||||
            change_schema: 'تغيير',
 | 
					            show_me: 'Show me',
 | 
				
			||||||
            none: 'لا شيء',
 | 
					            none: 'لا شيء',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -127,6 +126,9 @@ export const ar: LanguageTranslation = {
 | 
				
			|||||||
                // TODO: Translate
 | 
					                // TODO: Translate
 | 
				
			||||||
                clear: 'Clear Filter',
 | 
					                clear: 'Clear Filter',
 | 
				
			||||||
                no_results: 'No tables found matching your 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 +149,10 @@ export const ar: LanguageTranslation = {
 | 
				
			|||||||
                        comments: 'تعليقات',
 | 
					                        comments: 'تعليقات',
 | 
				
			||||||
                        no_comments: 'لا يوجد تعليقات',
 | 
					                        no_comments: 'لا يوجد تعليقات',
 | 
				
			||||||
                        delete_field: 'حذف الحقل',
 | 
					                        delete_field: 'حذف الحقل',
 | 
				
			||||||
 | 
					                        // TODO: Translate
 | 
				
			||||||
 | 
					                        character_length: 'Max Length',
 | 
				
			||||||
 | 
					                        default_value: 'Default Value',
 | 
				
			||||||
 | 
					                        no_default: 'No default',
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    index_actions: {
 | 
					                    index_actions: {
 | 
				
			||||||
                        title: 'خصائص الفهرس',
 | 
					                        title: 'خصائص الفهرس',
 | 
				
			||||||
@@ -206,6 +212,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: {
 | 
				
			||||||
@@ -217,6 +271,8 @@ export const ar: LanguageTranslation = {
 | 
				
			|||||||
            redo: 'إعادة',
 | 
					            redo: 'إعادة',
 | 
				
			||||||
            reorder_diagram: 'إعادة ترتيب الرسم البياني',
 | 
					            reorder_diagram: 'إعادة ترتيب الرسم البياني',
 | 
				
			||||||
            highlight_overlapping_tables: 'تمييز الجداول المتداخلة',
 | 
					            highlight_overlapping_tables: 'تمييز الجداول المتداخلة',
 | 
				
			||||||
 | 
					            // TODO: Translate
 | 
				
			||||||
 | 
					            filter: 'Filter Tables',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        new_diagram_dialog: {
 | 
					        new_diagram_dialog: {
 | 
				
			||||||
@@ -232,7 +288,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 تعليمات',
 | 
				
			||||||
@@ -326,6 +382,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: {
 | 
				
			||||||
@@ -358,10 +420,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 قم بلصق',
 | 
				
			||||||
@@ -370,7 +431,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: {
 | 
				
			||||||
@@ -383,12 +458,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: 'আমাদের সাথে কথা বলুন!',
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -76,8 +75,8 @@ export const bn: LanguageTranslation = {
 | 
				
			|||||||
            title: 'বহু স্কিমা',
 | 
					            title: 'বহু স্কিমা',
 | 
				
			||||||
            description:
 | 
					            description:
 | 
				
			||||||
                '{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।',
 | 
					                '{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।',
 | 
				
			||||||
            dont_show_again: 'পুনরায় দেখাবেন না',
 | 
					            // TODO: Translate
 | 
				
			||||||
            change_schema: 'পরিবর্তন করুন',
 | 
					            show_me: 'Show me',
 | 
				
			||||||
            none: 'কিছুই না',
 | 
					            none: 'কিছুই না',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -128,6 +127,9 @@ export const bn: LanguageTranslation = {
 | 
				
			|||||||
                // TODO: Translate
 | 
					                // TODO: Translate
 | 
				
			||||||
                clear: 'Clear Filter',
 | 
					                clear: 'Clear Filter',
 | 
				
			||||||
                no_results: 'No tables found matching your 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 +150,11 @@ export const bn: LanguageTranslation = {
 | 
				
			|||||||
                        comments: 'মন্তব্য',
 | 
					                        comments: 'মন্তব্য',
 | 
				
			||||||
                        no_comments: 'কোনো মন্তব্য নেই',
 | 
					                        no_comments: 'কোনো মন্তব্য নেই',
 | 
				
			||||||
                        delete_field: 'ফিল্ড মুছুন',
 | 
					                        delete_field: 'ফিল্ড মুছুন',
 | 
				
			||||||
 | 
					                        // TODO: Translate
 | 
				
			||||||
 | 
					                        default_value: 'Default Value',
 | 
				
			||||||
 | 
					                        no_default: 'No default',
 | 
				
			||||||
 | 
					                        // TODO: Translate
 | 
				
			||||||
 | 
					                        character_length: 'Max Length',
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    index_actions: {
 | 
					                    index_actions: {
 | 
				
			||||||
                        title: 'ইনডেক্স কর্ম',
 | 
					                        title: 'ইনডেক্স কর্ম',
 | 
				
			||||||
@@ -207,6 +214,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: {
 | 
				
			||||||
@@ -218,6 +272,8 @@ export const bn: LanguageTranslation = {
 | 
				
			|||||||
            redo: 'পুনরায় করুন',
 | 
					            redo: 'পুনরায় করুন',
 | 
				
			||||||
            reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন',
 | 
					            reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন',
 | 
				
			||||||
            highlight_overlapping_tables: 'ওভারল্যাপিং টেবিল হাইলাইট করুন',
 | 
					            highlight_overlapping_tables: 'ওভারল্যাপিং টেবিল হাইলাইট করুন',
 | 
				
			||||||
 | 
					            // TODO: Translate
 | 
				
			||||||
 | 
					            filter: 'Filter Tables',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        new_diagram_dialog: {
 | 
					        new_diagram_dialog: {
 | 
				
			||||||
@@ -233,7 +289,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 নির্দেশনা',
 | 
				
			||||||
@@ -327,6 +383,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: {
 | 
				
			||||||
@@ -361,7 +423,7 @@ export const bn: LanguageTranslation = {
 | 
				
			|||||||
            error: {
 | 
					            error: {
 | 
				
			||||||
                title: 'চিত্র রপ্তানিতে ত্রুটি',
 | 
					                title: 'চিত্র রপ্তানিতে ত্রুটি',
 | 
				
			||||||
                description:
 | 
					                description:
 | 
				
			||||||
                    'কিছু ভুল হয়েছে। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
 | 
					                    'কিছু ভুল হয়েছে। সাহায্যের প্রয়োজন? support@chartdb.io-এ যোগাযোগ করুন।',
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -373,7 +435,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: {
 | 
				
			||||||
@@ -386,12 +462,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',
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -76,8 +75,8 @@ export const de: LanguageTranslation = {
 | 
				
			|||||||
            title: 'Mehrere Schemas',
 | 
					            title: 'Mehrere Schemas',
 | 
				
			||||||
            description:
 | 
					            description:
 | 
				
			||||||
                '{{schemasCount}} Schemas in diesem Diagramm. Derzeit angezeigt: {{formattedSchemas}}.',
 | 
					                '{{schemasCount}} Schemas in diesem Diagramm. Derzeit angezeigt: {{formattedSchemas}}.',
 | 
				
			||||||
            dont_show_again: 'Nicht erneut anzeigen',
 | 
					            // TODO: Translate
 | 
				
			||||||
            change_schema: 'Schema ändern',
 | 
					            show_me: 'Show me',
 | 
				
			||||||
            none: 'Keine',
 | 
					            none: 'Keine',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -129,6 +128,9 @@ export const de: LanguageTranslation = {
 | 
				
			|||||||
                // TODO: Translate
 | 
					                // TODO: Translate
 | 
				
			||||||
                clear: 'Clear Filter',
 | 
					                clear: 'Clear Filter',
 | 
				
			||||||
                no_results: 'No tables found matching your 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',
 | 
				
			||||||
@@ -149,6 +151,11 @@ 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
 | 
				
			||||||
 | 
					                        default_value: 'Default Value',
 | 
				
			||||||
 | 
					                        no_default: 'No default',
 | 
				
			||||||
 | 
					                        // TODO: Translate
 | 
				
			||||||
 | 
					                        character_length: 'Max Length',
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    index_actions: {
 | 
					                    index_actions: {
 | 
				
			||||||
                        title: 'Indexattribute',
 | 
					                        title: 'Indexattribute',
 | 
				
			||||||
@@ -209,6 +216,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: {
 | 
				
			||||||
@@ -220,6 +274,8 @@ export const de: LanguageTranslation = {
 | 
				
			|||||||
            redo: 'Wiederholen',
 | 
					            redo: 'Wiederholen',
 | 
				
			||||||
            reorder_diagram: 'Diagramm neu anordnen',
 | 
					            reorder_diagram: 'Diagramm neu anordnen',
 | 
				
			||||||
            highlight_overlapping_tables: 'Überlappende Tabellen hervorheben',
 | 
					            highlight_overlapping_tables: 'Überlappende Tabellen hervorheben',
 | 
				
			||||||
 | 
					            // TODO: Translate
 | 
				
			||||||
 | 
					            filter: 'Filter Tables',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        new_diagram_dialog: {
 | 
					        new_diagram_dialog: {
 | 
				
			||||||
@@ -235,7 +291,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',
 | 
				
			||||||
@@ -330,6 +386,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: {
 | 
				
			||||||
@@ -364,7 +426,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
 | 
				
			||||||
@@ -376,7 +438,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: {
 | 
				
			||||||
@@ -389,12 +465,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!',
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -74,8 +73,7 @@ export const en = {
 | 
				
			|||||||
            title: 'Multiple Schemas',
 | 
					            title: 'Multiple Schemas',
 | 
				
			||||||
            description:
 | 
					            description:
 | 
				
			||||||
                '{{schemasCount}} schemas in this diagram. Currently displaying: {{formattedSchemas}}.',
 | 
					                '{{schemasCount}} schemas in this diagram. Currently displaying: {{formattedSchemas}}.',
 | 
				
			||||||
            dont_show_again: "Don't show again",
 | 
					            show_me: 'Show me',
 | 
				
			||||||
            change_schema: 'Change',
 | 
					 | 
				
			||||||
            none: 'none',
 | 
					            none: 'none',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -125,6 +123,8 @@ export const en = {
 | 
				
			|||||||
                collapse: 'Collapse All',
 | 
					                collapse: 'Collapse All',
 | 
				
			||||||
                clear: 'Clear Filter',
 | 
					                clear: 'Clear Filter',
 | 
				
			||||||
                no_results: 'No tables found matching your 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',
 | 
				
			||||||
@@ -142,8 +142,11 @@ 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',
 | 
				
			||||||
 | 
					                        default_value: 'Default Value',
 | 
				
			||||||
 | 
					                        no_default: 'No default',
 | 
				
			||||||
                        delete_field: 'Delete Field',
 | 
					                        delete_field: 'Delete Field',
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    index_actions: {
 | 
					                    index_actions: {
 | 
				
			||||||
@@ -204,6 +207,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: {
 | 
				
			||||||
@@ -215,6 +264,7 @@ export const en = {
 | 
				
			|||||||
            redo: 'Redo',
 | 
					            redo: 'Redo',
 | 
				
			||||||
            reorder_diagram: 'Reorder Diagram',
 | 
					            reorder_diagram: 'Reorder Diagram',
 | 
				
			||||||
            highlight_overlapping_tables: 'Highlight Overlapping Tables',
 | 
					            highlight_overlapping_tables: 'Highlight Overlapping Tables',
 | 
				
			||||||
 | 
					            filter: 'Filter Tables',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        new_diagram_dialog: {
 | 
					        new_diagram_dialog: {
 | 
				
			||||||
@@ -230,7 +280,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',
 | 
				
			||||||
@@ -324,6 +374,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: {
 | 
				
			||||||
@@ -357,19 +412,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: {
 | 
				
			||||||
@@ -382,12 +451,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}})',
 | 
				
			||||||
 
 | 
				
			|||||||