Compare commits
	
		
			213 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					c9ac8929c5 | ||
| 
						 | 
					c567c0a5f3 | ||
| 
						 | 
					2dc1a6fc75 | ||
| 
						 | 
					98f6edd5c8 | ||
| 
						 | 
					47a7a73a13 | ||
| 
						 | 
					d71b46e8b5 | ||
| 
						 | 
					e4c4a3b354 | ||
| 
						 | 
					1b8d51b73c | ||
| 
						 | 
					93d72a896b | ||
| 
						 | 
					9991077978 | ||
| 
						 | 
					bc82f9d6a8 | ||
| 
						 | 
					26dc299cd2 | ||
| 
						 | 
					d6ba4a4074 | ||
| 
						 | 
					d09379e8be | ||
| 
						 | 
					bdc41c0b74 | ||
| 
						 | 
					d3dbf41894 | ||
| 
						 | 
					e6783a89cc | ||
| 
						 | 
					af3638da7a | ||
| 
						 | 
					8954d893bb | ||
| 
						 | 
					1a6688e85e | ||
| 
						 | 
					5e81c1848a | ||
| 
						 | 
					2bd9ca25b2 | ||
| 
						 | 
					b016a70691 | ||
| 
						 | 
					a0fb1ed08b | ||
| 
						 | 
					ffddcdcc98 | ||
| 
						 | 
					fe9ef275b8 | ||
| 
						 | 
					df89f0b6b9 | ||
| 
						 | 
					534d2858af | ||
| 
						 | 
					2a64deebb8 | ||
| 
						 | 
					e5e1d59327 | ||
| 
						 | 
					aa290615ca | ||
| 
						 | 
					ec6e46fe81 | ||
| 
						 | 
					ac128d67de | ||
| 
						 | 
					07937a2f51 | ||
| 
						 | 
					d8e0bc7db8 | ||
| 
						 | 
					1ce265781b | ||
| 
						 | 
					60c5675cbf | ||
| 
						 | 
					66b086378c | ||
| 
						 | 
					abd2a6ccbe | ||
| 
						 | 
					459c5f1ce3 | ||
| 
						 | 
					44be48ff3a | ||
| 
						 | 
					ad8e34483f | ||
| 
						 | 
					215d57979d | ||
| 
						 | 
					ec3719ebce | ||
| 
						 | 
					0a5874a69b | ||
| 
						 | 
					7e0fdd1595 | ||
| 
						 | 
					2531a7023f | ||
| 
						 | 
					73daf0df21 | ||
| 
						 | 
					c77c983989 | ||
| 
						 | 
					0aaa451479 | ||
| 
						 | 
					b697e26170 | ||
| 
						 | 
					04d91c67b1 | ||
| 
						 | 
					d0dee84970 | ||
| 
						 | 
					b4ccfcdcde | ||
| 
						 | 
					1759b0b9f2 | ||
| 
						 | 
					ab4845c772 | ||
| 
						 | 
					0545b41140 | ||
| 
						 | 
					4520f8b1f7 | ||
| 
						 | 
					712bdf5b95 | ||
| 
						 | 
					d7c9536272 | ||
| 
						 | 
					815a52f192 | ||
| 
						 | 
					f1a4298362 | ||
| 
						 | 
					b8f2141bd2 | ||
| 
						 | 
					eaebe34768 | ||
| 
						 | 
					0d623a86b1 | ||
| 
						 | 
					19fd94c6bd | ||
| 
						 | 
					0da3caeeac | ||
| 
						 | 
					cb2ba66233 | ||
| 
						 | 
					8a2267281b | ||
| 
						 | 
					41ba251377 | ||
| 
						 | 
					e9c5442d9d | ||
| 
						 | 
					4f1d3295c0 | ||
| 
						 | 
					5936500ca0 | ||
| 
						 | 
					43fc1d7fc2 | ||
| 
						 | 
					8dfa7cc62e | ||
| 
						 | 
					23e93bfd01 | ||
| 
						 | 
					16f9f4671e | ||
| 
						 | 
					0c300e5e72 | ||
| 
						 | 
					b9a1e78b53 | ||
| 
						 | 
					337f7cdab4 | ||
| 
						 | 
					1b0390f0b7 | ||
| 
						 | 
					bc52933b58 | ||
| 
						 | 
					2fdad2344c | ||
| 
						 | 
					0c7eaa2df2 | ||
| 
						 | 
					a5f8e56b3c | ||
| 
						 | 
					8ffde62c1a | ||
| 
						 | 
					39247b77a2 | ||
| 
						 | 
					984b2aeee2 | ||
| 
						 | 
					eed104be5b | ||
| 
						 | 
					00bd535b3c | ||
| 
						 | 
					18e914242f | ||
| 
						 | 
					e68837a34a | ||
| 
						 | 
					b30162d98b | ||
| 
						 | 
					dba372d25a | ||
| 
						 | 
					2eb48e75d3 | ||
| 
						 | 
					867903cd5f | ||
| 
						 | 
					8aeb1df0ad | ||
| 
						 | 
					6bea827293 | ||
| 
						 | 
					a119854da7 | ||
| 
						 | 
					bfbfd7b843 | ||
| 
						 | 
					0ca7008735 | ||
| 
						 | 
					4bc71c52ff | ||
| 
						 | 
					8f27f10dec | ||
| 
						 | 
					a93ec2cab9 | ||
| 
						 | 
					386e40a0bf | ||
| 
						 | 
					bda150d4b6 | ||
| 
						 | 
					87836e53d1 | ||
| 
						 | 
					7e0483f1a5 | ||
| 
						 | 
					309ee9cb0f | ||
| 
						 | 
					79b885502e | ||
| 
						 | 
					745bdee86d | ||
| 
						 | 
					08eb9cc55f | ||
| 
						 | 
					778f85d492 | ||
| 
						 | 
					fb92be7d3e | ||
| 
						 | 
					6df588f40e | ||
| 
						 | 
					b46ed58dff | ||
| 
						 | 
					0d9f57a9c9 | ||
| 
						 | 
					b7dbe54c83 | ||
| 
						 | 
					43d1dfff71 | ||
| 
						 | 
					9949a46ee3 | ||
| 
						 | 
					dfbcf05b2f | ||
| 
						 | 
					f56fab9876 | ||
| 
						 | 
					c9ea7da092 | ||
| 
						 | 
					22d46e1e90 | ||
| 
						 | 
					6af94afc56 | ||
| 
						 | 
					f7f92903de | ||
| 
						 | 
					b35e17526b | ||
| 
						 | 
					bf32c08d37 | ||
| 
						 | 
					5d337409d6 | ||
| 
						 | 
					67f5ac303e | ||
| 
						 | 
					578546a171 | ||
| 
						 | 
					aa0b629a3e | ||
| 
						 | 
					69beaa0a83 | ||
| 
						 | 
					4fcc49d49a | ||
| 
						 | 
					d15985e399 | ||
| 
						 | 
					d429128e65 | ||
| 
						 | 
					2fce8326b6 | ||
| 
						 | 
					433c68a33d | ||
| 
						 | 
					58acb65f12 | ||
| 
						 | 
					7978955819 | ||
| 
						 | 
					c6118e0cdb | ||
| 
						 | 
					7d063b905f | ||
| 
						 | 
					e0ff198c3f | ||
| 
						 | 
					8b86e1c229 | ||
| 
						 | 
					24be28a662 | ||
| 
						 | 
					c6788b4917 | ||
| 
						 | 
					4a52bf02e6 | ||
| 
						 | 
					08b627cb8c | ||
| 
						 | 
					73f542adad | ||
| 
						 | 
					0d11b0c55a | ||
| 
						 | 
					5b9d2bd1e3 | ||
| 
						 | 
					cf1e141837 | ||
| 
						 | 
					3894a22174 | ||
| 
						 | 
					cad155e655 | ||
| 
						 | 
					4477b1ca1f | ||
| 
						 | 
					cd443466c7 | ||
| 
						 | 
					18012ddab1 | ||
| 
						 | 
					beb015194f | ||
| 
						 | 
					c3904d9fdd | ||
| 
						 | 
					aee5779983 | ||
| 
						 | 
					765a1c4354 | ||
| 
						 | 
					86840a8822 | ||
| 
						 | 
					487fb2d5c1 | ||
| 
						 | 
					54d5e96a6d | ||
| 
						 | 
					481ad3c844 | ||
| 
						 | 
					0ce85cf76b | ||
| 
						 | 
					5849e4586c | ||
| 
						 | 
					34c0a7163f | ||
| 
						 | 
					89e3ceab00 | ||
| 
						 | 
					5a5e64abef | ||
| 
						 | 
					2368e0d263 | ||
| 
						 | 
					547149da44 | ||
| 
						 | 
					a1144bbf76 | ||
| 
						 | 
					6b8d637b75 | ||
| 
						 | 
					fd47eb7f4b | ||
| 
						 | 
					7db86dcf8c | ||
| 
						 | 
					e75323c16e | ||
| 
						 | 
					97d01d7201 | ||
| 
						 | 
					90b42a4bb7 | ||
| 
						 | 
					fbf2fe919c | ||
| 
						 | 
					d3ddf7c51e | ||
| 
						 | 
					5759241573 | ||
| 
						 | 
					3747abbc3b | ||
| 
						 | 
					226e6cf1ce | ||
| 
						 | 
					1778abb683 | ||
| 
						 | 
					90a20dd1b0 | ||
| 
						 | 
					21c9129e14 | ||
| 
						 | 
					19d2d0bddd | ||
| 
						 | 
					83c43332d4 | ||
| 
						 | 
					3a1b8d1db1 | ||
| 
						 | 
					46426e27b4 | ||
| 
						 | 
					9402822fa3 | ||
| 
						 | 
					651fe361fc | ||
| 
						 | 
					aee1713aec | ||
| 
						 | 
					ecfa14829b | ||
| 
						 | 
					92e3ec785c | ||
| 
						 | 
					8102f19f79 | ||
| 
						 | 
					840a00ebcd | ||
| 
						 | 
					181f96d250 | ||
| 
						 | 
					ce2389f135 | ||
| 
						 | 
					f15dc77f33 | ||
| 
						 | 
					caa81c24a6 | ||
| 
						 | 
					e3cb62788c | ||
| 
						 | 
					fc46cbb893 | ||
| 
						 | 
					d94a71e9e1 | ||
| 
						 | 
					cf81253535 | ||
| 
						 | 
					25c4b42538 | ||
| 
						 | 
					f7a6e0cb5e | ||
| 
						 | 
					85275e5dd6 | ||
| 
						 | 
					4e5b467ce5 | ||
| 
						 | 
					874aa5ab75 | ||
| 
						 | 
					0940d72d5d | ||
| 
						 | 
					0d1739d70f | 
							
								
								
									
										5
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -24,4 +24,7 @@ jobs:
 | 
			
		||||
        run: npm run lint
 | 
			
		||||
 | 
			
		||||
      - name: Build
 | 
			
		||||
        run: npm run build
 | 
			
		||||
        run: npm run build
 | 
			
		||||
 | 
			
		||||
      - name: Run tests
 | 
			
		||||
        run: npm run test:ci
 | 
			
		||||
							
								
								
									
										33
									
								
								.github/workflows/cla.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,33 @@
 | 
			
		||||
name: "CLA Assistant"
 | 
			
		||||
on:
 | 
			
		||||
  issue_comment:
 | 
			
		||||
    types: [created]
 | 
			
		||||
  pull_request_target:
 | 
			
		||||
    types: [opened,closed,synchronize]
 | 
			
		||||
 | 
			
		||||
permissions:
 | 
			
		||||
  actions: write
 | 
			
		||||
  contents: read
 | 
			
		||||
  pull-requests: write
 | 
			
		||||
  statuses: write
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  CLAAssistant:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: "CLA Assistant"
 | 
			
		||||
        if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
 | 
			
		||||
        # Beta Release
 | 
			
		||||
        uses: contributor-assistant/github-action@v2.6.1
 | 
			
		||||
        env:
 | 
			
		||||
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
          PERSONAL_ACCESS_TOKEN: ${{ secrets.CHARTDB_CLA_SIGNATURES_PAT }}
 | 
			
		||||
        with:
 | 
			
		||||
          remote-organization-name: 'chartdb'
 | 
			
		||||
          remote-repository-name: 'cla-signatures'
 | 
			
		||||
          path-to-signatures: 'signatures/version1/cla.json'
 | 
			
		||||
          path-to-document: 'https://github.com/chartdb/chartdb/blob/main/CLA.md'
 | 
			
		||||
          # branch should not be protected
 | 
			
		||||
          branch: 'main'
 | 
			
		||||
          allowlist: 
 | 
			
		||||
							
								
								
									
										11
									
								
								.github/workflows/publish.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -32,7 +32,7 @@ jobs:
 | 
			
		||||
          registry: ${{ env.REGISTRY }}
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
          password: ${{ secrets.GITHUB_TOKEN }}
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: npm ci
 | 
			
		||||
 | 
			
		||||
@@ -42,6 +42,12 @@ jobs:
 | 
			
		||||
      - name: Build project
 | 
			
		||||
        run: npm run build
 | 
			
		||||
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        uses: docker/setup-qemu-action@v3
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3
 | 
			
		||||
 | 
			
		||||
      - name: Extract metadata (tags, labels) for Docker
 | 
			
		||||
        id: meta
 | 
			
		||||
        uses: docker/metadata-action@v4
 | 
			
		||||
@@ -50,10 +56,11 @@ jobs:
 | 
			
		||||
          tags: |
 | 
			
		||||
            type=semver,pattern={{version}}
 | 
			
		||||
 | 
			
		||||
      - name: Build and push Docker image
 | 
			
		||||
      - name: Build and push multi-arch Docker image
 | 
			
		||||
        uses: docker/build-push-action@v6
 | 
			
		||||
        with:
 | 
			
		||||
          context: .
 | 
			
		||||
          push: true
 | 
			
		||||
          platforms: linux/amd64,linux/arm64
 | 
			
		||||
          tags: ${{ steps.meta.outputs.tags }}
 | 
			
		||||
          labels: ${{ steps.meta.outputs.labels }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										256
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						@@ -1,5 +1,261 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## [1.16.0](https://github.com/chartdb/chartdb/compare/v1.15.1...v1.16.0) (2025-09-24)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* add area context menu and UI improvements ([#918](https://github.com/chartdb/chartdb/issues/918)) ([d09379e](https://github.com/chartdb/chartdb/commit/d09379e8be0fa3c83ca77ff62ae815fe4db9869b))
 | 
			
		||||
* add quick table mode on canvas ([#915](https://github.com/chartdb/chartdb/issues/915)) ([8954d89](https://github.com/chartdb/chartdb/commit/8954d893bbfee45bb311380115fb14ebbf3a3133))
 | 
			
		||||
* add zoom navigation buttons to canvas filter for tables and areas ([#903](https://github.com/chartdb/chartdb/issues/903)) ([a0fb1ed](https://github.com/chartdb/chartdb/commit/a0fb1ed08ba18b66354fa3498d610097a83d4afc))
 | 
			
		||||
* **import-db:** add DBML syntax to import database dialog ([#768](https://github.com/chartdb/chartdb/issues/768)) ([af3638d](https://github.com/chartdb/chartdb/commit/af3638da7a9b70f281ceaddbc2f712a713d90cda))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* add areas width and height + table width to diff check ([#931](https://github.com/chartdb/chartdb/issues/931)) ([98f6edd](https://github.com/chartdb/chartdb/commit/98f6edd5c8a8e9130e892b2d841744e0cf63a7bf))
 | 
			
		||||
* add diff x,y ([#928](https://github.com/chartdb/chartdb/issues/928)) ([e4c4a3b](https://github.com/chartdb/chartdb/commit/e4c4a3b35484d9ece955a5aec577603dde73d634))
 | 
			
		||||
* add support for ALTER TABLE ADD COLUMN in PostgreSQL importer ([#892](https://github.com/chartdb/chartdb/issues/892)) ([ec6e46f](https://github.com/chartdb/chartdb/commit/ec6e46fe81ea1806c179c50a4c5779d8596008aa))
 | 
			
		||||
* add tests for diff ([#930](https://github.com/chartdb/chartdb/issues/930)) ([47a7a73](https://github.com/chartdb/chartdb/commit/47a7a73a137b87dfa6e67aff5f939cf64ccf4601))
 | 
			
		||||
* dbml edit mode glitch ([#925](https://github.com/chartdb/chartdb/issues/925)) ([93d72a8](https://github.com/chartdb/chartdb/commit/93d72a896bab9aa79d8ea2f876126887e432214c))
 | 
			
		||||
* dbml export default time bug ([#922](https://github.com/chartdb/chartdb/issues/922)) ([bc82f9d](https://github.com/chartdb/chartdb/commit/bc82f9d6a8fe4de2f7e0fc465e0a20c5dbf8f41d))
 | 
			
		||||
* dbml export renaming fields bug ([#921](https://github.com/chartdb/chartdb/issues/921)) ([26dc299](https://github.com/chartdb/chartdb/commit/26dc299cd28e9890d191c13f84a15ac38ae48b11))
 | 
			
		||||
* **dbml:** export array fields without quotes ([#911](https://github.com/chartdb/chartdb/issues/911)) ([5e81c18](https://github.com/chartdb/chartdb/commit/5e81c1848aaa911990e1e881d62525f5254d6d34))
 | 
			
		||||
* diff logic ([#927](https://github.com/chartdb/chartdb/issues/927)) ([1b8d51b](https://github.com/chartdb/chartdb/commit/1b8d51b73c4ed4b7c5929adcb17a44927c7defca))
 | 
			
		||||
* export dbml issues after upgrade version ([#883](https://github.com/chartdb/chartdb/issues/883)) ([07937a2](https://github.com/chartdb/chartdb/commit/07937a2f51708b1c10b45c2bd1f9a9acf5c3f708))
 | 
			
		||||
* export sql + import metadata lib ([#902](https://github.com/chartdb/chartdb/issues/902)) ([ffddcdc](https://github.com/chartdb/chartdb/commit/ffddcdcc987bacb0e0d7e8dea27d08d3a8c5a8c8))
 | 
			
		||||
* handle bidirectional relationships in DBML export ([#924](https://github.com/chartdb/chartdb/issues/924)) ([9991077](https://github.com/chartdb/chartdb/commit/99910779789a9c6ef113d06bc3de31e35b9b04d1))
 | 
			
		||||
* import dbml set pk field unique ([#920](https://github.com/chartdb/chartdb/issues/920)) ([d6ba4a4](https://github.com/chartdb/chartdb/commit/d6ba4a40749d85d2703f120600df4345dab3c561))
 | 
			
		||||
* improve SQL default value parsing for PostgreSQL, MySQL, and SQL Server with proper type handling and casting support ([#900](https://github.com/chartdb/chartdb/issues/900)) ([fe9ef27](https://github.com/chartdb/chartdb/commit/fe9ef275b8619dcfd7e57541a62a6237a16d29a8))
 | 
			
		||||
* move area utils ([#932](https://github.com/chartdb/chartdb/issues/932)) ([2dc1a6f](https://github.com/chartdb/chartdb/commit/2dc1a6fc7519e0a455b0e1306601195deb156c96))
 | 
			
		||||
* move auto arrange to toolbar ([#904](https://github.com/chartdb/chartdb/issues/904)) ([b016a70](https://github.com/chartdb/chartdb/commit/b016a70691bc22af5720b4de683e8c9353994fcc))
 | 
			
		||||
* remove general db creation ([#901](https://github.com/chartdb/chartdb/issues/901)) ([df89f0b](https://github.com/chartdb/chartdb/commit/df89f0b6b9ba3fcc8b05bae4f60c0dc4ad1d2215))
 | 
			
		||||
* remove many to many rel option ([#933](https://github.com/chartdb/chartdb/issues/933)) ([c567c0a](https://github.com/chartdb/chartdb/commit/c567c0a5f39157b2c430e92192b6750304d7a834))
 | 
			
		||||
* reset increment and default when change field ([#896](https://github.com/chartdb/chartdb/issues/896)) ([e5e1d59](https://github.com/chartdb/chartdb/commit/e5e1d5932762422ea63acfd6cf9fe4f03aa822f7))
 | 
			
		||||
* **sql-import:** handle SQL Server DDL with multiple tables, inline foreign keys, and case-insensitive field matching ([#897](https://github.com/chartdb/chartdb/issues/897)) ([2a64dee](https://github.com/chartdb/chartdb/commit/2a64deebb87a11ee3892024c3273d682bb86f7ef))
 | 
			
		||||
* **sql-import:** support ALTER TABLE ALTER COLUMN TYPE in PostgreSQL importer ([#895](https://github.com/chartdb/chartdb/issues/895)) ([aa29061](https://github.com/chartdb/chartdb/commit/aa290615caf806d7d0374c848d50b4636fde7e96))
 | 
			
		||||
* **sqlite:** improve parser to handle tables without column types and fix column detection ([#914](https://github.com/chartdb/chartdb/issues/914)) ([d3dbf41](https://github.com/chartdb/chartdb/commit/d3dbf41894d74f0ffce9afe3bd810f065aa53017))
 | 
			
		||||
* trigger edit table on canvas from context menu ([#919](https://github.com/chartdb/chartdb/issues/919)) ([bdc41c0](https://github.com/chartdb/chartdb/commit/bdc41c0b74d9d9918e7b6cd2152fa07c0c58ce60))
 | 
			
		||||
* update deps vulns ([#909](https://github.com/chartdb/chartdb/issues/909)) ([2bd9ca2](https://github.com/chartdb/chartdb/commit/2bd9ca25b2c7b1f053ff4fdc8c5cfc1b0e65901d))
 | 
			
		||||
* upgrade dbml lib ([#880](https://github.com/chartdb/chartdb/issues/880)) ([d8e0bc7](https://github.com/chartdb/chartdb/commit/d8e0bc7db8881971ddaea7177bcebee13cc865f6))
 | 
			
		||||
 | 
			
		||||
## [1.15.1](https://github.com/chartdb/chartdb/compare/v1.15.0...v1.15.1) (2025-08-27)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* add actions menu to diagram list + add duplicate diagram ([#876](https://github.com/chartdb/chartdb/issues/876)) ([abd2a6c](https://github.com/chartdb/chartdb/commit/abd2a6ccbe1aa63db44ec28b3eff525cc5d3f8b0))
 | 
			
		||||
* **custom-types:** Make schema optional ([#866](https://github.com/chartdb/chartdb/issues/866)) ([60c5675](https://github.com/chartdb/chartdb/commit/60c5675cbfe205859d2d0c9848d8345a0a854671))
 | 
			
		||||
* handle quoted identifiers with special characters in SQL import/export and DBML generation ([#877](https://github.com/chartdb/chartdb/issues/877)) ([66b0863](https://github.com/chartdb/chartdb/commit/66b086378cd63347acab5fc7f13db7db4feaa872))
 | 
			
		||||
 | 
			
		||||
## [1.15.0](https://github.com/chartdb/chartdb/compare/v1.14.0...v1.15.0) (2025-08-26)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* add auto increment support for fields with database-specific export ([#851](https://github.com/chartdb/chartdb/issues/851)) ([c77c983](https://github.com/chartdb/chartdb/commit/c77c983989ae38a6b1139dd9015f4f3178d4e103))
 | 
			
		||||
* **filter:** filter tables by areas ([#836](https://github.com/chartdb/chartdb/issues/836)) ([e9c5442](https://github.com/chartdb/chartdb/commit/e9c5442d9df2beadad78187da3363bb6406636c4))
 | 
			
		||||
* include foreign keys inline in SQLite CREATE TABLE statements ([#833](https://github.com/chartdb/chartdb/issues/833)) ([43fc1d7](https://github.com/chartdb/chartdb/commit/43fc1d7fc26876b22c61405f6c3df89fc66b7992))
 | 
			
		||||
* **postgres:** add support hash index types ([#812](https://github.com/chartdb/chartdb/issues/812)) ([0d623a8](https://github.com/chartdb/chartdb/commit/0d623a86b1cb7cbd223e10ad23d09fc0e106c006))
 | 
			
		||||
* support create views ([#868](https://github.com/chartdb/chartdb/issues/868)) ([0a5874a](https://github.com/chartdb/chartdb/commit/0a5874a69b6323145430c1fb4e3482ac7da4916c))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* area filter logic ([#861](https://github.com/chartdb/chartdb/issues/861)) ([73daf0d](https://github.com/chartdb/chartdb/commit/73daf0df2142a29c2eeebe60b43198bcca869026))
 | 
			
		||||
* **area filter:** fix dragging tables over filtered areas ([#842](https://github.com/chartdb/chartdb/issues/842)) ([19fd94c](https://github.com/chartdb/chartdb/commit/19fd94c6bde3a9ec749cd1ccacbedb6abc96d037))
 | 
			
		||||
* **canvas:** delete table + area together bug ([#859](https://github.com/chartdb/chartdb/issues/859)) ([b697e26](https://github.com/chartdb/chartdb/commit/b697e26170da95dcb427ff6907b6f663c98ba59f))
 | 
			
		||||
* **cla:** Harden action ([#867](https://github.com/chartdb/chartdb/issues/867)) ([ad8e344](https://github.com/chartdb/chartdb/commit/ad8e34483fdf4226de76c9e7768bc2ba9bf154de))
 | 
			
		||||
* DBML export error with multi-line table comments for SQL Server ([#852](https://github.com/chartdb/chartdb/issues/852)) ([0545b41](https://github.com/chartdb/chartdb/commit/0545b411407b2449220d10981a04c3e368a90ca3))
 | 
			
		||||
* filter to default schema on load new diagram ([#849](https://github.com/chartdb/chartdb/issues/849)) ([712bdf5](https://github.com/chartdb/chartdb/commit/712bdf5b958919d940c4f2a1c3b7c7e969990f02))
 | 
			
		||||
* **filter:** filter toggle issues with no schemas dbs ([#856](https://github.com/chartdb/chartdb/issues/856)) ([d0dee84](https://github.com/chartdb/chartdb/commit/d0dee849702161d979b4f589a7e6579fbaade22d))
 | 
			
		||||
* **filters:** refactor diagram filters - remove schema filter ([#832](https://github.com/chartdb/chartdb/issues/832)) ([4f1d329](https://github.com/chartdb/chartdb/commit/4f1d3295c09782ab46d82ce21b662032aa094f22))
 | 
			
		||||
* for sqlite import - add more types & include type parameters ([#834](https://github.com/chartdb/chartdb/issues/834)) ([5936500](https://github.com/chartdb/chartdb/commit/5936500ca00a57b3f161616264c26152a13c36d2))
 | 
			
		||||
* improve creating view to table dependency ([#874](https://github.com/chartdb/chartdb/issues/874)) ([44be48f](https://github.com/chartdb/chartdb/commit/44be48ff3ad1361279331c17364090b13af471a1))
 | 
			
		||||
* initially show filter when filter active ([#853](https://github.com/chartdb/chartdb/issues/853)) ([ab4845c](https://github.com/chartdb/chartdb/commit/ab4845c7728e6e0b2d852f8005921fd90630eef9))
 | 
			
		||||
* **menu:** clear file menu ([#843](https://github.com/chartdb/chartdb/issues/843)) ([eaebe34](https://github.com/chartdb/chartdb/commit/eaebe3476824af779214a354b3e991923a22f195))
 | 
			
		||||
* merge relationship & dependency sections to ref section ([#870](https://github.com/chartdb/chartdb/issues/870)) ([ec3719e](https://github.com/chartdb/chartdb/commit/ec3719ebce4664b2aa6e3322fb3337e72bc21015))
 | 
			
		||||
* move dbml into sections menu ([#862](https://github.com/chartdb/chartdb/issues/862)) ([2531a70](https://github.com/chartdb/chartdb/commit/2531a7023f36ef29e67c0da6bca4fd0346b18a51))
 | 
			
		||||
* open filter by default ([#863](https://github.com/chartdb/chartdb/issues/863)) ([7e0fdd1](https://github.com/chartdb/chartdb/commit/7e0fdd1595bffe29e769d29602d04f42edfe417e))
 | 
			
		||||
* preserve composite primary key constraint names across import/export workflows ([#869](https://github.com/chartdb/chartdb/issues/869)) ([215d579](https://github.com/chartdb/chartdb/commit/215d57979df2e91fa61988acff590daad2f4e771))
 | 
			
		||||
* prevent false change detection in DBML editor by stripping public schema on import ([#858](https://github.com/chartdb/chartdb/issues/858)) ([0aaa451](https://github.com/chartdb/chartdb/commit/0aaa451479911d047e4cc83f063afa68a122ba9b))
 | 
			
		||||
* remove unnecessary space ([#845](https://github.com/chartdb/chartdb/issues/845)) ([f1a4298](https://github.com/chartdb/chartdb/commit/f1a429836221aacdda73b91665bf33ffb011164c))
 | 
			
		||||
* reorder with areas ([#846](https://github.com/chartdb/chartdb/issues/846)) ([d7c9536](https://github.com/chartdb/chartdb/commit/d7c9536272cf1d42104b7064ea448d128d091a20))
 | 
			
		||||
* **select-box:** fix select box issue in dialog ([#840](https://github.com/chartdb/chartdb/issues/840)) ([cb2ba66](https://github.com/chartdb/chartdb/commit/cb2ba66233c8c04e2d963cf2d210499d8512a268))
 | 
			
		||||
* set default filter only if has more than 1 schemas ([#855](https://github.com/chartdb/chartdb/issues/855)) ([b4ccfcd](https://github.com/chartdb/chartdb/commit/b4ccfcdcde2f3565b0d3bbc46fa1715feb6cd925))
 | 
			
		||||
* show default schema first ([#854](https://github.com/chartdb/chartdb/issues/854)) ([1759b0b](https://github.com/chartdb/chartdb/commit/1759b0b9f271ed25f7c71f26c344e3f1d97bc5fb))
 | 
			
		||||
* **sidebar:** add titles to sidebar ([#844](https://github.com/chartdb/chartdb/issues/844)) ([b8f2141](https://github.com/chartdb/chartdb/commit/b8f2141bd2e67272030896fb4009a7925f9f09e4))
 | 
			
		||||
* **sql-import:** fix SQL Server foreign key parsing for tables without schema prefix ([#857](https://github.com/chartdb/chartdb/issues/857)) ([04d91c6](https://github.com/chartdb/chartdb/commit/04d91c67b1075e94948f75186878e633df7abbca))
 | 
			
		||||
* **table colors:** switch to default table color ([#841](https://github.com/chartdb/chartdb/issues/841)) ([0da3cae](https://github.com/chartdb/chartdb/commit/0da3caeeac37926dd22f38d98423611f39c0412a))
 | 
			
		||||
* update filter on adding table ([#838](https://github.com/chartdb/chartdb/issues/838)) ([41ba251](https://github.com/chartdb/chartdb/commit/41ba25137789dda25266178cd7c96ecbb37e62a4))
 | 
			
		||||
 | 
			
		||||
## [1.14.0](https://github.com/chartdb/chartdb/compare/v1.13.2...v1.14.0) (2025-08-04)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* add floating "Show All" button when tables are out of view ([#787](https://github.com/chartdb/chartdb/issues/787)) ([bda150d](https://github.com/chartdb/chartdb/commit/bda150d4b6d6fb90beb423efba69349d21a037a5))
 | 
			
		||||
* add table selection for large database imports ([#776](https://github.com/chartdb/chartdb/issues/776)) ([0d9f57a](https://github.com/chartdb/chartdb/commit/0d9f57a9c969a67e350d6bf25e07c3a9ef5bba39))
 | 
			
		||||
* **canvas:** Add filter tables on canvas ([#774](https://github.com/chartdb/chartdb/issues/774)) ([dfbcf05](https://github.com/chartdb/chartdb/commit/dfbcf05b2f595f5b7b77dd61abf77e6e07acaf8f))
 | 
			
		||||
* **custom-types:** add highlight fields option for custom types ([#726](https://github.com/chartdb/chartdb/issues/726)) ([7e0483f](https://github.com/chartdb/chartdb/commit/7e0483f1a5512a6a737baf61caf7513e043f2e96))
 | 
			
		||||
* **datatypes:** Add decimal / numeric attribute support + organize field row ([#715](https://github.com/chartdb/chartdb/issues/715)) ([778f85d](https://github.com/chartdb/chartdb/commit/778f85d49214232a39710e47bb5d4ec41b75d427))
 | 
			
		||||
* **dbml:** Edit Diagram Directly from DBML ([#819](https://github.com/chartdb/chartdb/issues/819)) ([1b0390f](https://github.com/chartdb/chartdb/commit/1b0390f0b7652fe415540b7942cf53ec87143f08))
 | 
			
		||||
* **default value:** add default value option to table field settings ([#770](https://github.com/chartdb/chartdb/issues/770)) ([c9ea7da](https://github.com/chartdb/chartdb/commit/c9ea7da0923ff991cb936235674d9a52b8186137))
 | 
			
		||||
* enhance primary key and unique field handling logic ([#817](https://github.com/chartdb/chartdb/issues/817)) ([39247b7](https://github.com/chartdb/chartdb/commit/39247b77a299caa4f29ea434af3028155c6d37ed))
 | 
			
		||||
* implement area grouping with parent-child relationships ([#762](https://github.com/chartdb/chartdb/issues/762)) ([b35e175](https://github.com/chartdb/chartdb/commit/b35e17526b3c9b918928ae5f3f89711ea7b2529c))
 | 
			
		||||
* **schema:** support create new schema ([#801](https://github.com/chartdb/chartdb/issues/801)) ([867903c](https://github.com/chartdb/chartdb/commit/867903cd5f24d96ce1fe718dc9b562e2f2b75276))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* add open and create diagram to side menu ([#757](https://github.com/chartdb/chartdb/issues/757)) ([67f5ac3](https://github.com/chartdb/chartdb/commit/67f5ac303ebf5ada97d5c80fb08a2815ca205a91))
 | 
			
		||||
* add PostgreSQL tests and fix parsing SQL ([#760](https://github.com/chartdb/chartdb/issues/760)) ([5d33740](https://github.com/chartdb/chartdb/commit/5d337409d64d1078b538350016982a98e684c06c))
 | 
			
		||||
* area resizers size ([#830](https://github.com/chartdb/chartdb/issues/830)) ([23e93bf](https://github.com/chartdb/chartdb/commit/23e93bfd01d741dd3d11aa5c479cef97e1a86fa6))
 | 
			
		||||
* **area:** redo/undo after dragging an area with tables ([#767](https://github.com/chartdb/chartdb/issues/767)) ([6af94af](https://github.com/chartdb/chartdb/commit/6af94afc56cf8987b8fc9e3f0a9bfa966de35408))
 | 
			
		||||
* **canvas filter:** improve scroller on canvas filter ([#799](https://github.com/chartdb/chartdb/issues/799)) ([6bea827](https://github.com/chartdb/chartdb/commit/6bea82729362a8c7b73dc089ddd9e52bae176aa2))
 | 
			
		||||
* **canvas:** fix filter eye button ([#780](https://github.com/chartdb/chartdb/issues/780)) ([b7dbe54](https://github.com/chartdb/chartdb/commit/b7dbe54c83c75cfe3c556f7a162055dcfe2de23d))
 | 
			
		||||
* clone of custom types ([#804](https://github.com/chartdb/chartdb/issues/804)) ([b30162d](https://github.com/chartdb/chartdb/commit/b30162d98bc659a61aae023cdeaead4ce25c7ae9))
 | 
			
		||||
* **cockroachdb:** support schema creation for cockroachdb ([#803](https://github.com/chartdb/chartdb/issues/803)) ([dba372d](https://github.com/chartdb/chartdb/commit/dba372d25a8c642baf8600d05aa154882729d446))
 | 
			
		||||
* **dbml actions:** set dbml tooltips side ([#798](https://github.com/chartdb/chartdb/issues/798)) ([a119854](https://github.com/chartdb/chartdb/commit/a119854da7c935eb595984ea9398e04136ce60c4))
 | 
			
		||||
* **dbml editor:** move tooltips button to be on the right ([#797](https://github.com/chartdb/chartdb/issues/797)) ([bfbfd7b](https://github.com/chartdb/chartdb/commit/bfbfd7b843f96c894b1966ad95393b866c927466))
 | 
			
		||||
* **dbml export:** fix handle tables with same name under different schemas ([#807](https://github.com/chartdb/chartdb/issues/807)) ([18e9142](https://github.com/chartdb/chartdb/commit/18e914242faccd6376fe5a7cd5a4478667f065ee))
 | 
			
		||||
* **dbml export:** handle tables with same name under different schemas ([#806](https://github.com/chartdb/chartdb/issues/806)) ([e68837a](https://github.com/chartdb/chartdb/commit/e68837a34aa635fb6fc02c7f1289495e5c448242))
 | 
			
		||||
* **dbml field comments:** support export field comments in dbml ([#796](https://github.com/chartdb/chartdb/issues/796)) ([0ca7008](https://github.com/chartdb/chartdb/commit/0ca700873577bbfbf1dd3f8088c258fc89b10c53))
 | 
			
		||||
* **dbml import:** fix dbml import types + schemas ([#808](https://github.com/chartdb/chartdb/issues/808)) ([00bd535](https://github.com/chartdb/chartdb/commit/00bd535b3c62d26d25a6276d52beb10e26afad76))
 | 
			
		||||
* **dbml-export:** merge field attributes into single brackets and fix schema syntax ([#790](https://github.com/chartdb/chartdb/issues/790)) ([309ee9c](https://github.com/chartdb/chartdb/commit/309ee9cb0ff1f5a68ed183e3919e1a11a8410909))
 | 
			
		||||
* **dbml-import:** handle unsupported DBML features and add comprehensive tests ([#766](https://github.com/chartdb/chartdb/issues/766)) ([22d46e1](https://github.com/chartdb/chartdb/commit/22d46e1e90729730cc25dd6961bfe8c3d2ae0c98))
 | 
			
		||||
* **dbml:** dbml indentation ([#829](https://github.com/chartdb/chartdb/issues/829)) ([16f9f46](https://github.com/chartdb/chartdb/commit/16f9f4671e011eb66ba9594bed47570eda3eed66))
 | 
			
		||||
* **dbml:** dbml note syntax ([#826](https://github.com/chartdb/chartdb/issues/826)) ([337f7cd](https://github.com/chartdb/chartdb/commit/337f7cdab4759d15cb4d25a8c0e9394e99ba33d4))
 | 
			
		||||
* **dbml:** fix dbml output format ([#815](https://github.com/chartdb/chartdb/issues/815)) ([eed104b](https://github.com/chartdb/chartdb/commit/eed104be5ba2b7d9940ffac38e7877722ad764fc))
 | 
			
		||||
* **dbml:** fix schemas with same table names ([#828](https://github.com/chartdb/chartdb/issues/828)) ([0c300e5](https://github.com/chartdb/chartdb/commit/0c300e5e72cc5ff22cac42f8dbaed167061157c6))
 | 
			
		||||
* **dbml:** import dbml notes (table + fields) ([#827](https://github.com/chartdb/chartdb/issues/827)) ([b9a1e78](https://github.com/chartdb/chartdb/commit/b9a1e78b53c932c0b1a12ee38b62494a5c2f9348))
 | 
			
		||||
* **dbml:** support multiple relationships on same field in inline DBML ([#822](https://github.com/chartdb/chartdb/issues/822)) ([a5f8e56](https://github.com/chartdb/chartdb/commit/a5f8e56b3ca97b851b6953481644d3a3ff7ce882))
 | 
			
		||||
* **dbml:** support spaces in names ([#794](https://github.com/chartdb/chartdb/issues/794)) ([8f27f10](https://github.com/chartdb/chartdb/commit/8f27f10dec96af400dc2c12a30b22b3a346803a9))
 | 
			
		||||
* fix hotkeys on form elements ([#778](https://github.com/chartdb/chartdb/issues/778)) ([43d1dff](https://github.com/chartdb/chartdb/commit/43d1dfff71f2b960358a79b0112b78d11df91fb7))
 | 
			
		||||
* fix screen freeze after schema select ([#800](https://github.com/chartdb/chartdb/issues/800)) ([8aeb1df](https://github.com/chartdb/chartdb/commit/8aeb1df0ad353c49e91243453f24bfa5921a89ab))
 | 
			
		||||
* **i18n:** add Croatian (hr) language support ([#802](https://github.com/chartdb/chartdb/issues/802)) ([2eb48e7](https://github.com/chartdb/chartdb/commit/2eb48e75d303d622f51327d22502a6f78e7fb32d))
 | 
			
		||||
* improve SQL export formatting and add schema-aware FK grouping ([#783](https://github.com/chartdb/chartdb/issues/783)) ([6df588f](https://github.com/chartdb/chartdb/commit/6df588f40e6e7066da6125413b94466429d48767))
 | 
			
		||||
* lost in canvas button animation ([#793](https://github.com/chartdb/chartdb/issues/793)) ([a93ec2c](https://github.com/chartdb/chartdb/commit/a93ec2cab906d0e4431d8d1668adcf2dbfc3c80f))
 | 
			
		||||
* **readonly:** fix zoom out on readonly ([#818](https://github.com/chartdb/chartdb/issues/818)) ([8ffde62](https://github.com/chartdb/chartdb/commit/8ffde62c1a00893c4bf6b4dd39068df530375416))
 | 
			
		||||
* remove error lag after autofix ([#764](https://github.com/chartdb/chartdb/issues/764)) ([bf32c08](https://github.com/chartdb/chartdb/commit/bf32c08d37c02ee6d7946a41633bb97b2271fcb7))
 | 
			
		||||
* remove unnecessary import ([#791](https://github.com/chartdb/chartdb/issues/791)) ([87836e5](https://github.com/chartdb/chartdb/commit/87836e53d145b825f9c4f80abca72f418df50e6c))
 | 
			
		||||
* **scroll:** disable scroll x behavior ([#795](https://github.com/chartdb/chartdb/issues/795)) ([4bc71c5](https://github.com/chartdb/chartdb/commit/4bc71c52ff5c462800d8530b72a5aadb7d7f85ed))
 | 
			
		||||
* set focus on filter search ([#775](https://github.com/chartdb/chartdb/issues/775)) ([9949a46](https://github.com/chartdb/chartdb/commit/9949a46ee3ba7f46a2ea7f2c0d7101cc9336df4f))
 | 
			
		||||
* solve issue with multiple render of tables ([#823](https://github.com/chartdb/chartdb/issues/823)) ([0c7eaa2](https://github.com/chartdb/chartdb/commit/0c7eaa2df20cfb6994b7e6251c760a2d4581c879))
 | 
			
		||||
* **sql-export:** escape newlines and quotes in multi-line comments ([#765](https://github.com/chartdb/chartdb/issues/765)) ([f7f9290](https://github.com/chartdb/chartdb/commit/f7f92903def84a94ac0c66f625f96a6681383945))
 | 
			
		||||
* **sql-server:** improvment for sql-server import via sql script ([#789](https://github.com/chartdb/chartdb/issues/789)) ([79b8855](https://github.com/chartdb/chartdb/commit/79b885502e3385e996a52093a3ccd5f6e469993a))
 | 
			
		||||
* **table-node:** fix comment icon on field ([#786](https://github.com/chartdb/chartdb/issues/786)) ([745bdee](https://github.com/chartdb/chartdb/commit/745bdee86d07f1e9c3a2d24237c48c25b9a8eeea))
 | 
			
		||||
* **table-node:** improve field spacing ([#785](https://github.com/chartdb/chartdb/issues/785)) ([08eb9cc](https://github.com/chartdb/chartdb/commit/08eb9cc55f0077f53afea6f9ce720341e1a583c2))
 | 
			
		||||
* **table-select:** add loading indication for import ([#782](https://github.com/chartdb/chartdb/issues/782)) ([b46ed58](https://github.com/chartdb/chartdb/commit/b46ed58dff1ec74579fb1544dba46b0f77730c52))
 | 
			
		||||
* **ui:** reduce spacing between primary key icon and short field types ([#816](https://github.com/chartdb/chartdb/issues/816)) ([984b2ae](https://github.com/chartdb/chartdb/commit/984b2aeee22c43cb9bda77df2c22087973079af4))
 | 
			
		||||
* update MariaDB database import smart query ([#792](https://github.com/chartdb/chartdb/issues/792)) ([386e40a](https://github.com/chartdb/chartdb/commit/386e40a0bf93d9aef1486bb1e729d8f485e675eb))
 | 
			
		||||
* update multiple schemas toast to require user action ([#771](https://github.com/chartdb/chartdb/issues/771)) ([f56fab9](https://github.com/chartdb/chartdb/commit/f56fab9876fb9fc46c6c708231324a90d8a7851d))
 | 
			
		||||
* update relationship when table width changes via expand/shrink ([#825](https://github.com/chartdb/chartdb/issues/825)) ([bc52933](https://github.com/chartdb/chartdb/commit/bc52933b58bfe6bc73779d9401128254cbf497d5))
 | 
			
		||||
 | 
			
		||||
## [1.13.2](https://github.com/chartdb/chartdb/compare/v1.13.1...v1.13.2) (2025-07-06)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* add DISABLE_ANALYTICS flag to opt-out of Fathom analytics ([#750](https://github.com/chartdb/chartdb/issues/750)) ([aa0b629](https://github.com/chartdb/chartdb/commit/aa0b629a3eaf8e8b60473ea3f28f769270c7714c))
 | 
			
		||||
 | 
			
		||||
## [1.13.1](https://github.com/chartdb/chartdb/compare/v1.13.0...v1.13.1) (2025-07-04)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* **custom_types:** fix display custom types in select box ([#737](https://github.com/chartdb/chartdb/issues/737)) ([24be28a](https://github.com/chartdb/chartdb/commit/24be28a662c48fc5bc62e76446b9669d83d7d3e0))
 | 
			
		||||
* **dbml-editor:** for some cases that the dbml had issues ([#739](https://github.com/chartdb/chartdb/issues/739)) ([e0ff198](https://github.com/chartdb/chartdb/commit/e0ff198c3fd416498dac5680bb323ec88c54b65c))
 | 
			
		||||
* **dbml:** Filter duplicate tables at diagram level before export dbml ([#746](https://github.com/chartdb/chartdb/issues/746)) ([d429128](https://github.com/chartdb/chartdb/commit/d429128e65aa28c500eac2487356e4869506e948))
 | 
			
		||||
* **export-sql:** conditionally show generic option and reorder by diagram type ([#708](https://github.com/chartdb/chartdb/issues/708)) ([c6118e0](https://github.com/chartdb/chartdb/commit/c6118e0cdb0e5caaf73447d33db2fde1a98efe60))
 | 
			
		||||
* general performance improvements on canvas ([#751](https://github.com/chartdb/chartdb/issues/751)) ([4fcc49d](https://github.com/chartdb/chartdb/commit/4fcc49d49a76a4b886ffd6cf0b40cf2fc49952ec))
 | 
			
		||||
* **import-database:** for custom types query to import supabase & timescale ([#745](https://github.com/chartdb/chartdb/issues/745)) ([2fce832](https://github.com/chartdb/chartdb/commit/2fce8326b67b751d38dd34f409fea574449d0298))
 | 
			
		||||
* **import-db:** fix mariadb import ([#740](https://github.com/chartdb/chartdb/issues/740)) ([7d063b9](https://github.com/chartdb/chartdb/commit/7d063b905f19f51501468bd0bd794a25cf65e1be))
 | 
			
		||||
* **performance:** improve storage provider performance ([#734](https://github.com/chartdb/chartdb/issues/734)) ([c6788b4](https://github.com/chartdb/chartdb/commit/c6788b49173d9cce23571daeb460285cb7cffb11))
 | 
			
		||||
* resolve unresponsive cursor and input glitches when editing field comments ([#749](https://github.com/chartdb/chartdb/issues/749)) ([d15985e](https://github.com/chartdb/chartdb/commit/d15985e3999a0cd54213b2fb08c55d48a1b8b3b2))
 | 
			
		||||
* **table name:** updates table name value when its updated from canvas/sidebar ([#716](https://github.com/chartdb/chartdb/issues/716)) ([8b86e1c](https://github.com/chartdb/chartdb/commit/8b86e1c22992aaadcce7ad5fc1d267c5a57a99f0))
 | 
			
		||||
 | 
			
		||||
## [1.13.0](https://github.com/chartdb/chartdb/compare/v1.12.0...v1.13.0) (2025-05-28)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* **custom-types:** add enums and composite types for Postgres ([#714](https://github.com/chartdb/chartdb/issues/714)) ([c3904d9](https://github.com/chartdb/chartdb/commit/c3904d9fdd63ef5b76a44e73582d592f2c418687))
 | 
			
		||||
* **export-sql:** add custom types to export sql script ([#720](https://github.com/chartdb/chartdb/issues/720)) ([cad155e](https://github.com/chartdb/chartdb/commit/cad155e6550f171b8faecbfdff27032798ecea43))
 | 
			
		||||
* **oracle:** support oracle in ChartDB ([#709](https://github.com/chartdb/chartdb/issues/709)) ([765a1c4](https://github.com/chartdb/chartdb/commit/765a1c43547a29bd3428c942c7afb56f63aaf046))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* **canvas:** prevent canvas blink and lag on field edit ([#723](https://github.com/chartdb/chartdb/issues/723)) ([cd44346](https://github.com/chartdb/chartdb/commit/cd443466c7952f1cdc3739645c12130b9231e3a1))
 | 
			
		||||
* **canvas:** prevent canvas blink and lag on primary field edit ([#725](https://github.com/chartdb/chartdb/issues/725)) ([4477b1c](https://github.com/chartdb/chartdb/commit/4477b1ca1fe6b282b604739a23e31181acd4d7bc))
 | 
			
		||||
* **custom_types:** fix custom types on storage provider ([#721](https://github.com/chartdb/chartdb/issues/721)) ([beb0151](https://github.com/chartdb/chartdb/commit/beb015194f917c0ba644458410162d2b7599918c))
 | 
			
		||||
* **custom_types:** fix custom types on storage provider ([#722](https://github.com/chartdb/chartdb/issues/722)) ([18012dd](https://github.com/chartdb/chartdb/commit/18012ddab1718bcce3432aea626adf6fc9be25d9))
 | 
			
		||||
* **custom-types:** fetch directly via the smart-query the custom types ([#729](https://github.com/chartdb/chartdb/issues/729)) ([cf1e141](https://github.com/chartdb/chartdb/commit/cf1e141837eda77d717ad87489ce9946b688e226))
 | 
			
		||||
* **dbml-editor:** export comments with schema if existsed ([#728](https://github.com/chartdb/chartdb/issues/728)) ([73f542a](https://github.com/chartdb/chartdb/commit/73f542adad2d66a1e84fc656a0c34d9b1f39f33c))
 | 
			
		||||
* **dbml-editor:** fix export dbml - to show enums ([#724](https://github.com/chartdb/chartdb/issues/724)) ([3894a22](https://github.com/chartdb/chartdb/commit/3894a221745d32c13160bedcb1bcf53d89897698))
 | 
			
		||||
* **import-database:** remove the default fetch from import database ([#718](https://github.com/chartdb/chartdb/issues/718)) ([0d11b0c](https://github.com/chartdb/chartdb/commit/0d11b0c55a94a12a764785cfdcf2ba10437241d6))
 | 
			
		||||
* **menu:** add oracle to import menu ([#713](https://github.com/chartdb/chartdb/issues/713)) ([aee5779](https://github.com/chartdb/chartdb/commit/aee577998342eb4a2b05b3e03181992a435712d8))
 | 
			
		||||
* **relationship:** fix creating of relationships ([#732](https://github.com/chartdb/chartdb/issues/732)) ([08b627c](https://github.com/chartdb/chartdb/commit/08b627cb8ca8fdf08d8ed2ff7e89104887deffb7))
 | 
			
		||||
 | 
			
		||||
## [1.12.0](https://github.com/chartdb/chartdb/compare/v1.11.0...v1.12.0) (2025-05-20)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* **areas:** implement area to enable logical diagram arrangement ([#661](https://github.com/chartdb/chartdb/issues/661)) ([92e3ec7](https://github.com/chartdb/chartdb/commit/92e3ec785c91f7f19881c6d9d0692257af4651bc))
 | 
			
		||||
* **examples:** update examples to have areas ([#677](https://github.com/chartdb/chartdb/issues/677)) ([21c9129](https://github.com/chartdb/chartdb/commit/21c9129e14670c744950cd43a5cbdd4b7d47c639))
 | 
			
		||||
* **image-export:** add transparent and pattern export image toggles ([#671](https://github.com/chartdb/chartdb/issues/671)) ([6b8d637](https://github.com/chartdb/chartdb/commit/6b8d637b757b94630ecd7521b4a2c99634afae69))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* add sorting based on how common the datatype on side-panel ([#651](https://github.com/chartdb/chartdb/issues/651)) ([3a1b8d1](https://github.com/chartdb/chartdb/commit/3a1b8d1db13d8dd7cb6cbe5ef8c5a60faccfeae5))
 | 
			
		||||
* **canvas:** disable edit area name on read only ([#666](https://github.com/chartdb/chartdb/issues/666)) ([9402822](https://github.com/chartdb/chartdb/commit/9402822fa31f8cd94fe7971277839ee5425e29bf))
 | 
			
		||||
* **canvas:** read only mode ([#665](https://github.com/chartdb/chartdb/issues/665)) ([651fe36](https://github.com/chartdb/chartdb/commit/651fe361fce61fe0577d2593f268131e9ca359d0))
 | 
			
		||||
* **clone:** add areas to clone diagram ([#664](https://github.com/chartdb/chartdb/issues/664)) ([aee1713](https://github.com/chartdb/chartdb/commit/aee1713aecdd5e54228a16cbc3c4fc184661c56b))
 | 
			
		||||
* **dbml-editor:** add inline refs mode + fix issues with DBML syntax ([#687](https://github.com/chartdb/chartdb/issues/687)) ([fbf2fe9](https://github.com/chartdb/chartdb/commit/fbf2fe919c2168c715f8231c0246753b19635f14))
 | 
			
		||||
* **dbml-editor:** remove invalid fields before showing DBML + warning ([#683](https://github.com/chartdb/chartdb/issues/683)) ([5759241](https://github.com/chartdb/chartdb/commit/5759241573db204183c92599588d59f4aadaeafb))
 | 
			
		||||
* **ddl-import:** fix datatypes when importing via ddl ([#696](https://github.com/chartdb/chartdb/issues/696)) ([a1144bb](https://github.com/chartdb/chartdb/commit/a1144bbf761a0daedd546b5d9b92300be59e0157))
 | 
			
		||||
* **ddl:** inline fks ddl script ([#701](https://github.com/chartdb/chartdb/issues/701)) ([5849e45](https://github.com/chartdb/chartdb/commit/5849e4586c7c2a7cd86bd064df8916b130fc6234))
 | 
			
		||||
* **dependencies:** hide icon when diagram has no dependencies ([#684](https://github.com/chartdb/chartdb/issues/684)) ([547149d](https://github.com/chartdb/chartdb/commit/547149da44db6d3d1e36d619d475fe52ff83a472))
 | 
			
		||||
* **examples:** add loader ([#678](https://github.com/chartdb/chartdb/issues/678)) ([90a20dd](https://github.com/chartdb/chartdb/commit/90a20dd1b0277c4aee848fae5ed7a8347c5ba77d))
 | 
			
		||||
* **examples:** fix clone examples ([#679](https://github.com/chartdb/chartdb/issues/679)) ([1778abb](https://github.com/chartdb/chartdb/commit/1778abb683d575af244edcd9a11f8d03f903f719))
 | 
			
		||||
* **expanded-table:** persist expanded state across renders ([#707](https://github.com/chartdb/chartdb/issues/707)) ([54d5e96](https://github.com/chartdb/chartdb/commit/54d5e96a6db1e3abd52229a89ac503ff31885386))
 | 
			
		||||
* **export image:** Fix usage of advanced options accordion ([#703](https://github.com/chartdb/chartdb/issues/703)) ([0ce85cf](https://github.com/chartdb/chartdb/commit/0ce85cf76b733f441f661608278c0db3122c5074))
 | 
			
		||||
* **import-database:** auto detect when user try to import ddl script ([#698](https://github.com/chartdb/chartdb/issues/698)) ([5a5e64a](https://github.com/chartdb/chartdb/commit/5a5e64abef510cff28b3d8972520d0b9df29b024))
 | 
			
		||||
* **import-database:** remove view_definition when importing via query ([#702](https://github.com/chartdb/chartdb/issues/702)) ([481ad3c](https://github.com/chartdb/chartdb/commit/481ad3c8449f469bf2b4418e4cdcc5b5608dfd36))
 | 
			
		||||
* **import-json:** for broken json imports ([#697](https://github.com/chartdb/chartdb/issues/697)) ([2368e0d](https://github.com/chartdb/chartdb/commit/2368e0d2639021c4a11a8e5131d6af44fb6a47db))
 | 
			
		||||
* **import-json:** simplify import script for fixing invalid JSON ([#681](https://github.com/chartdb/chartdb/issues/681)) ([226e6cf](https://github.com/chartdb/chartdb/commit/226e6cf1ce4d2edcfbee6a4de7ab0bc0cfeb17fe))
 | 
			
		||||
* **import:** dbml and query - senetize before import ([#699](https://github.com/chartdb/chartdb/issues/699)) ([34c0a71](https://github.com/chartdb/chartdb/commit/34c0a7163f47bde7ddfaa8f044341e3c971b7e03))
 | 
			
		||||
* **navbar:** open diagram directly from diagram icon ([#694](https://github.com/chartdb/chartdb/issues/694)) ([7db86dc](https://github.com/chartdb/chartdb/commit/7db86dcf8c97d34b056e4b5b85a0dda0438322ea))
 | 
			
		||||
* **performance:** Only render visible ([#672](https://github.com/chartdb/chartdb/issues/672)) ([83c4333](https://github.com/chartdb/chartdb/commit/83c43332d497e9fc148a18b9cb4d9ecc85e44183))
 | 
			
		||||
* **performance:** update field only when changed ([#685](https://github.com/chartdb/chartdb/issues/685)) ([d3ddf7c](https://github.com/chartdb/chartdb/commit/d3ddf7c51eaa4b9cddb961defd52d423f39f281d))
 | 
			
		||||
* **postgres:** fix import of postgres fks ([#700](https://github.com/chartdb/chartdb/issues/700)) ([89e3cea](https://github.com/chartdb/chartdb/commit/89e3ceab00defaabc079e165fc90e92ca00722cf))
 | 
			
		||||
* **schema:** add areas to diagram schema ([#663](https://github.com/chartdb/chartdb/issues/663)) ([ecfa148](https://github.com/chartdb/chartdb/commit/ecfa14829bcb1b813c7b154b4bd59f24e3032d8f))
 | 
			
		||||
* **sql-script:** change ddl to be sql-script ([#710](https://github.com/chartdb/chartdb/issues/710)) ([487fb2d](https://github.com/chartdb/chartdb/commit/487fb2d5c17b70ac54aa17af9a2ac9aded6b40ba))
 | 
			
		||||
* **table:** enhance field focus behavior to include table hover state ([#676](https://github.com/chartdb/chartdb/issues/676)) ([19d2d0b](https://github.com/chartdb/chartdb/commit/19d2d0bddd3a464995b79e97e6caf6e652836081))
 | 
			
		||||
* **translations:** Add some translations for ru-RU language ([#690](https://github.com/chartdb/chartdb/issues/690)) ([97d01d7](https://github.com/chartdb/chartdb/commit/97d01d72014e473c42348c9ebcbe7a0b973d31aa))
 | 
			
		||||
 | 
			
		||||
## [1.11.0](https://github.com/chartdb/chartdb/compare/v1.10.0...v1.11.0) (2025-04-17)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* add sidebar footer help buttons ([#650](https://github.com/chartdb/chartdb/issues/650)) ([fc46cbb](https://github.com/chartdb/chartdb/commit/fc46cbb8933761c7bac3604664f7de812f6f5b6b))
 | 
			
		||||
* **import-sql:** import postgresql via SQL (DDL script) ([#639](https://github.com/chartdb/chartdb/issues/639)) ([f7a6e0c](https://github.com/chartdb/chartdb/commit/f7a6e0cb5e4921dd9540739f9da269858e7ca7be))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* **import:** display query result formatted ([#644](https://github.com/chartdb/chartdb/issues/644)) ([caa81c2](https://github.com/chartdb/chartdb/commit/caa81c24a6535bc87129c38622aac5a62a6d479d))
 | 
			
		||||
* **import:** strict parse of database metadata ([#635](https://github.com/chartdb/chartdb/issues/635)) ([0940d72](https://github.com/chartdb/chartdb/commit/0940d72d5d3726650213257639f24ba47e729854))
 | 
			
		||||
* **mobile:** fix create diagram modal on mobile ([#646](https://github.com/chartdb/chartdb/issues/646)) ([25c4b42](https://github.com/chartdb/chartdb/commit/25c4b4253849575d7a781ed197281e2a35e7184a))
 | 
			
		||||
* **mysql-ddl:** update the script to import - for create fks ([#642](https://github.com/chartdb/chartdb/issues/642)) ([cf81253](https://github.com/chartdb/chartdb/commit/cf81253535ca5a3b8a65add78287c1bdb283a1c7))
 | 
			
		||||
* **performance:** Import deps dynamically ([#652](https://github.com/chartdb/chartdb/issues/652)) ([e3cb627](https://github.com/chartdb/chartdb/commit/e3cb62788c13f149e35e1a5020191bd43d14b52f))
 | 
			
		||||
* remove unused links from help menu ([#623](https://github.com/chartdb/chartdb/issues/623)) ([85275e5](https://github.com/chartdb/chartdb/commit/85275e5dd6e7845f06f682eeceda7932fc87e875))
 | 
			
		||||
* **sidebar:** turn sidebar to responsive for mobile ([#658](https://github.com/chartdb/chartdb/issues/658)) ([ce2389f](https://github.com/chartdb/chartdb/commit/ce2389f135d399d82c9848335d31174bac8a3791))
 | 
			
		||||
 | 
			
		||||
## [1.10.0](https://github.com/chartdb/chartdb/compare/v1.9.0...v1.10.0) (2025-03-25)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								CLA.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,45 @@
 | 
			
		||||
# ChartDB Contributors License Agreement
 | 
			
		||||
 | 
			
		||||
This Contributors License Agreement ("CLA") is entered into between the Contributor, and ChartDB, Inc. ("ChartDB"), collectively referred to as the "Parties."
 | 
			
		||||
 | 
			
		||||
## Background:
 | 
			
		||||
 | 
			
		||||
ChartDB is an open-source project aimed at providing an open-source database diagramming and visualization tool for all parties.This CLA governs the rights and contributions made by the Contributor to the ChartDB project.
 | 
			
		||||
 | 
			
		||||
## Agreement:
 | 
			
		||||
 | 
			
		||||
**Contributor Grant of License:**
 | 
			
		||||
 | 
			
		||||
By submitting code, documentation, or any other materials (collectively, "Contributions") to the ChartDB project, the Contributor grants ChartDB a perpetual, worldwide, non-exclusive, royalty-free, sublicensable license to use, modify, distribute, and otherwise exploit the Contributions, including any intellectual property rights therein, for the purposes of the ChartDB project.
 | 
			
		||||
 | 
			
		||||
**Representation of Ownership and Right to Contribute:**
 | 
			
		||||
 | 
			
		||||
The Contributor represents that they have the legal right to grant the license stated in Section 1, and that the Contributions do not infringe upon the intellectual property rights of any third party. The Contributor also represents that they have the authority to submit the Contributions on their own behalf or, if applicable, on behalf of their employer or any other entity.
 | 
			
		||||
 | 
			
		||||
**Patent Grant:**
 | 
			
		||||
 | 
			
		||||
If the Contributions include any method, process, or apparatus that is covered by a patent, the Contributor agrees to grant ChartDB a non-exclusive, worldwide, royalty-free license under any patent claims necessary to use, modify, distribute, and otherwise exploit the Contributions for the purposes of the ChartDB project.
 | 
			
		||||
 | 
			
		||||
**No Implied Warranties or Support:**
 | 
			
		||||
 | 
			
		||||
The Contributor acknowledges that the Contributions are provided "as is," without any warranties or support of any kind. ChartDB shall have no obligation to provide maintenance, updates, bug fixes, or support for the Contributions.
 | 
			
		||||
 | 
			
		||||
**Retention of Contributor Rights:**
 | 
			
		||||
 | 
			
		||||
The Contributor retains all right, title, and interest in and to their Contributions. This CLA does not restrict the Contributor from using their own Contributions for any other purpose.
 | 
			
		||||
 | 
			
		||||
**Governing Law:**
 | 
			
		||||
 | 
			
		||||
This CLA shall be governed by and construed in accordance with the laws of Delaware (DE), without regard to its conflict of laws principles.
 | 
			
		||||
 | 
			
		||||
**Entire Agreement:**
 | 
			
		||||
 | 
			
		||||
This CLA constitutes the entire agreement between the Parties with respect to the subject matter hereof and supersedes all prior and contemporaneous understandings, agreements, representations, and warranties.
 | 
			
		||||
 | 
			
		||||
**Acceptance:**
 | 
			
		||||
 | 
			
		||||
By submitting Contributions to the ChartDB project, the Contributor acknowledges and agrees to the terms and conditions of this CLA. If the Contributor is agreeing to this CLA on behalf of an entity, they represent that they have the necessary authority to bind that entity to these terms.
 | 
			
		||||
 | 
			
		||||
**Effective Date:**
 | 
			
		||||
 | 
			
		||||
This CLA is effective as of the date of the first Contribution made by the Contributor to the ChartDB project.
 | 
			
		||||
@@ -60,7 +60,7 @@ representative at an online or offline event.
 | 
			
		||||
 | 
			
		||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
 | 
			
		||||
reported to the community leaders responsible for enforcement at
 | 
			
		||||
chartdb.io@gmail.com.
 | 
			
		||||
support@chartdb.io.
 | 
			
		||||
All complaints will be reviewed and investigated promptly and fairly.
 | 
			
		||||
 | 
			
		||||
All community leaders are obligated to respect the privacy and security of the
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ To submit a pull request:
 | 
			
		||||
 | 
			
		||||
If you find a bug, check [GitHub issues](https://github.com/chartdb/chartdb/issues) to see if it’s already reported. If not, feel free to [report it](https://github.com/chartdb/chartdb/issues/new?labels=bug).
 | 
			
		||||
 | 
			
		||||
For questions about using ChartDB, reach out to us via Email (chartdb.io@gmail.com) or [Discord](https://discord.gg/QeFwyWSKwC). For feature requests, create a [new feature](https://github.com/chartdb/chartdb/issues/new?labels=enhancement).
 | 
			
		||||
For questions about using ChartDB, reach out to us via Email (support@chartdb.io) or [Discord](https://discord.gg/QeFwyWSKwC). For feature requests, create a [new feature](https://github.com/chartdb/chartdb/issues/new?labels=enhancement).
 | 
			
		||||
 | 
			
		||||
### Creating a Branch
 | 
			
		||||
 | 
			
		||||
@@ -35,7 +35,7 @@ By contributing, you agree that your work will be licensed under ChartDB's [lice
 | 
			
		||||
## Questions?
 | 
			
		||||
 | 
			
		||||
Feel free to ask in `#contributing` on [Discord](https://discord.gg/QeFwyWSKwC) if you have questions about our process, how to proceed, etc.
 | 
			
		||||
or [Email](chartdb.io@gmail.com)
 | 
			
		||||
or [Email](support@chartdb.io)
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,8 @@ FROM node:22-alpine AS builder
 | 
			
		||||
ARG VITE_OPENAI_API_KEY
 | 
			
		||||
ARG VITE_OPENAI_API_ENDPOINT
 | 
			
		||||
ARG VITE_LLM_MODEL_NAME
 | 
			
		||||
ARG VITE_HIDE_BUCKLE_DOT_DEV
 | 
			
		||||
ARG VITE_HIDE_CHARTDB_CLOUD
 | 
			
		||||
ARG VITE_DISABLE_ANALYTICS
 | 
			
		||||
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
 | 
			
		||||
@@ -16,7 +17,8 @@ COPY . .
 | 
			
		||||
RUN echo "VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}" > .env && \
 | 
			
		||||
    echo "VITE_OPENAI_API_ENDPOINT=${VITE_OPENAI_API_ENDPOINT}" >> .env && \
 | 
			
		||||
    echo "VITE_LLM_MODEL_NAME=${VITE_LLM_MODEL_NAME}" >> .env && \
 | 
			
		||||
    echo "VITE_HIDE_BUCKLE_DOT_DEV=${VITE_HIDE_BUCKLE_DOT_DEV}" >> .env 
 | 
			
		||||
    echo "VITE_HIDE_CHARTDB_CLOUD=${VITE_HIDE_CHARTDB_CLOUD}" >> .env && \
 | 
			
		||||
    echo "VITE_DISABLE_ANALYTICS=${VITE_DISABLE_ANALYTICS}" >> .env
 | 
			
		||||
 | 
			
		||||
RUN npm run build
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -30,8 +30,8 @@
 | 
			
		||||
  <a href="https://discord.gg/QeFwyWSKwC">
 | 
			
		||||
    <img src="https://img.shields.io/discord/1277047413705670678?color=5865F2&label=Discord&logo=discord&logoColor=white" alt="Discord community channel" />
 | 
			
		||||
  </a>
 | 
			
		||||
  <a href="https://x.com/chartdb_io">
 | 
			
		||||
    <img src="https://img.shields.io/twitter/follow/ChartDB?style=social"/>
 | 
			
		||||
  <a href="https://x.com/intent/follow?screen_name=jonathanfishner">
 | 
			
		||||
    <img src="https://img.shields.io/twitter/follow/jonathanfishner?style=social"/>
 | 
			
		||||
  </a>
 | 
			
		||||
 | 
			
		||||
</h4>
 | 
			
		||||
@@ -125,6 +125,8 @@ docker run \
 | 
			
		||||
  -p 8080:80 chartdb
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
> **Privacy Note:** ChartDB includes privacy-focused analytics via Fathom Analytics. You can disable this by adding `-e DISABLE_ANALYTICS=true` to the run command or `--build-arg VITE_DISABLE_ANALYTICS=true` when building.
 | 
			
		||||
 | 
			
		||||
> **Note:** You must configure either Option 1 (OpenAI API key) OR Option 2 (Custom endpoint and model name) for AI capabilities to work. Do not mix the two options.
 | 
			
		||||
 | 
			
		||||
Open your browser and navigate to `http://localhost:8080`.
 | 
			
		||||
@@ -149,7 +151,7 @@ VITE_LLM_MODEL_NAME=Qwen/Qwen2.5-32B-Instruct-AWQ
 | 
			
		||||
 | 
			
		||||
- [Discord](https://discord.gg/QeFwyWSKwC) (For live discussion with the community and the ChartDB team)
 | 
			
		||||
- [GitHub Issues](https://github.com/chartdb/chartdb/issues) (For any bugs and errors you encounter using ChartDB)
 | 
			
		||||
- [Twitter](https://x.com/chartdb_io) (Get news fast)
 | 
			
		||||
- [Twitter](https://x.com/intent/follow?screen_name=jonathanfishner) (Get news fast)
 | 
			
		||||
 | 
			
		||||
## Contributing
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,11 +10,12 @@ server {
 | 
			
		||||
 | 
			
		||||
    location /config.js {
 | 
			
		||||
        default_type application/javascript;
 | 
			
		||||
        return 200 "window.env = { 
 | 
			
		||||
        return 200 "window.env = {
 | 
			
		||||
            OPENAI_API_KEY: \"$OPENAI_API_KEY\",
 | 
			
		||||
            OPENAI_API_ENDPOINT: \"$OPENAI_API_ENDPOINT\",
 | 
			
		||||
            LLM_MODEL_NAME: \"$LLM_MODEL_NAME\",
 | 
			
		||||
            HIDE_BUCKLE_DOT_DEV: \"$HIDE_BUCKLE_DOT_DEV\"
 | 
			
		||||
            HIDE_CHARTDB_CLOUD: \"$HIDE_CHARTDB_CLOUD\",
 | 
			
		||||
            DISABLE_ANALYTICS: \"$DISABLE_ANALYTICS\"
 | 
			
		||||
        };";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
 | 
			
		||||
# Replace placeholders in nginx.conf
 | 
			
		||||
envsubst '${OPENAI_API_KEY} ${OPENAI_API_ENDPOINT} ${LLM_MODEL_NAME} ${HIDE_BUCKLE_DOT_DEV}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
 | 
			
		||||
envsubst '${OPENAI_API_KEY} ${OPENAI_API_ENDPOINT} ${LLM_MODEL_NAME} ${HIDE_CHARTDB_CLOUD} ${DISABLE_ANALYTICS}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
 | 
			
		||||
 | 
			
		||||
# Start Nginx
 | 
			
		||||
nginx -g "daemon off;"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								index.html
									
									
									
									
									
								
							
							
						
						@@ -4,8 +4,9 @@
 | 
			
		||||
        <meta charset="UTF-8" />
 | 
			
		||||
        <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
 | 
			
		||||
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
			
		||||
        <meta name="robots" content="max-image-preview:large" />
 | 
			
		||||
        <meta name="robots" content="noindex, max-image-preview:large" />
 | 
			
		||||
        <title>ChartDB - Create & Visualize Database Schema Diagrams</title>
 | 
			
		||||
        <link rel="canonical" href="https://chartdb.io" />
 | 
			
		||||
        <link rel="preconnect" href="https://fonts.googleapis.com" />
 | 
			
		||||
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
 | 
			
		||||
        <link
 | 
			
		||||
@@ -13,11 +14,26 @@
 | 
			
		||||
            rel="stylesheet"
 | 
			
		||||
        />
 | 
			
		||||
        <script src="/config.js"></script>
 | 
			
		||||
        <script
 | 
			
		||||
            src="https://cdn.usefathom.com/script.js"
 | 
			
		||||
            data-site="PRHIVBNN"
 | 
			
		||||
            defer
 | 
			
		||||
        ></script>
 | 
			
		||||
        <script>
 | 
			
		||||
            // Load analytics only if not disabled
 | 
			
		||||
            (function () {
 | 
			
		||||
                const disableAnalytics =
 | 
			
		||||
                    (window.env && window.env.DISABLE_ANALYTICS === 'true') ||
 | 
			
		||||
                    (typeof process !== 'undefined' &&
 | 
			
		||||
                        process.env &&
 | 
			
		||||
                        process.env.VITE_DISABLE_ANALYTICS === 'true');
 | 
			
		||||
 | 
			
		||||
                if (!disableAnalytics) {
 | 
			
		||||
                    const script = document.createElement('script');
 | 
			
		||||
                    script.src = 'https://cdn.usefathom.com/script.js';
 | 
			
		||||
                    script.setAttribute('data-site', 'PRHIVBNN');
 | 
			
		||||
                    script.setAttribute('data-canonical', 'false');
 | 
			
		||||
                    script.setAttribute('data-spa', 'auto');
 | 
			
		||||
                    script.defer = true;
 | 
			
		||||
                    document.head.appendChild(script);
 | 
			
		||||
                }
 | 
			
		||||
            })();
 | 
			
		||||
        </script>
 | 
			
		||||
    </head>
 | 
			
		||||
    <body>
 | 
			
		||||
        <div id="root"></div>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1885
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										33
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "chartdb",
 | 
			
		||||
    "private": true,
 | 
			
		||||
    "version": "1.10.0",
 | 
			
		||||
    "version": "1.16.0",
 | 
			
		||||
    "type": "module",
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "dev": "vite",
 | 
			
		||||
@@ -9,11 +9,15 @@
 | 
			
		||||
        "lint": "eslint . --report-unused-disable-directives --max-warnings 0",
 | 
			
		||||
        "lint:fix": "npm run lint -- --fix",
 | 
			
		||||
        "preview": "vite preview",
 | 
			
		||||
        "prepare": "husky"
 | 
			
		||||
        "prepare": "husky",
 | 
			
		||||
        "test": "vitest",
 | 
			
		||||
        "test:ci": "vitest run --reporter=verbose --bail=1",
 | 
			
		||||
        "test:ui": "vitest --ui",
 | 
			
		||||
        "test:coverage": "vitest --coverage"
 | 
			
		||||
    },
 | 
			
		||||
    "dependencies": {
 | 
			
		||||
        "@ai-sdk/openai": "^0.0.51",
 | 
			
		||||
        "@dbml/core": "^3.9.5",
 | 
			
		||||
        "@dbml/core": "^3.13.9",
 | 
			
		||||
        "@dnd-kit/sortable": "^8.0.0",
 | 
			
		||||
        "@monaco-editor/react": "^4.6.0",
 | 
			
		||||
        "@radix-ui/react-accordion": "^1.2.0",
 | 
			
		||||
@@ -22,24 +26,24 @@
 | 
			
		||||
        "@radix-ui/react-checkbox": "^1.1.1",
 | 
			
		||||
        "@radix-ui/react-collapsible": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-context-menu": "^2.2.1",
 | 
			
		||||
        "@radix-ui/react-dialog": "^1.1.6",
 | 
			
		||||
        "@radix-ui/react-dialog": "^1.1.14",
 | 
			
		||||
        "@radix-ui/react-dropdown-menu": "^2.1.1",
 | 
			
		||||
        "@radix-ui/react-hover-card": "^1.1.1",
 | 
			
		||||
        "@radix-ui/react-icons": "^1.3.0",
 | 
			
		||||
        "@radix-ui/react-icons": "^1.3.2",
 | 
			
		||||
        "@radix-ui/react-label": "^2.1.0",
 | 
			
		||||
        "@radix-ui/react-menubar": "^1.1.1",
 | 
			
		||||
        "@radix-ui/react-popover": "^1.1.1",
 | 
			
		||||
        "@radix-ui/react-scroll-area": "1.2.0",
 | 
			
		||||
        "@radix-ui/react-select": "^2.1.1",
 | 
			
		||||
        "@radix-ui/react-separator": "^1.1.2",
 | 
			
		||||
        "@radix-ui/react-slot": "^1.1.2",
 | 
			
		||||
        "@radix-ui/react-separator": "^1.1.7",
 | 
			
		||||
        "@radix-ui/react-slot": "^1.2.3",
 | 
			
		||||
        "@radix-ui/react-tabs": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-toast": "^1.2.1",
 | 
			
		||||
        "@radix-ui/react-toggle": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-toggle-group": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-tooltip": "^1.1.8",
 | 
			
		||||
        "@radix-ui/react-tooltip": "^1.2.7",
 | 
			
		||||
        "@uidotdev/usehooks": "^2.4.1",
 | 
			
		||||
        "@xyflow/react": "^12.3.1",
 | 
			
		||||
        "@xyflow/react": "^12.8.2",
 | 
			
		||||
        "ahooks": "^3.8.1",
 | 
			
		||||
        "ai": "^3.3.14",
 | 
			
		||||
        "class-variance-authority": "^0.7.1",
 | 
			
		||||
@@ -50,8 +54,9 @@
 | 
			
		||||
        "html-to-image": "^1.11.11",
 | 
			
		||||
        "i18next": "^23.14.0",
 | 
			
		||||
        "i18next-browser-languagedetector": "^8.0.0",
 | 
			
		||||
        "lucide-react": "^0.441.0",
 | 
			
		||||
        "lucide-react": "^0.525.0",
 | 
			
		||||
        "monaco-editor": "^0.52.0",
 | 
			
		||||
        "motion": "^12.23.6",
 | 
			
		||||
        "nanoid": "^5.0.7",
 | 
			
		||||
        "node-sql-parser": "^5.3.2",
 | 
			
		||||
        "react": "^18.3.1",
 | 
			
		||||
@@ -73,12 +78,16 @@
 | 
			
		||||
        "@eslint/compat": "^1.2.4",
 | 
			
		||||
        "@eslint/eslintrc": "^3.2.0",
 | 
			
		||||
        "@eslint/js": "^9.16.0",
 | 
			
		||||
        "@testing-library/jest-dom": "^6.6.3",
 | 
			
		||||
        "@testing-library/react": "^16.3.0",
 | 
			
		||||
        "@testing-library/user-event": "^14.6.1",
 | 
			
		||||
        "@types/node": "^22.1.0",
 | 
			
		||||
        "@types/react": "^18.3.3",
 | 
			
		||||
        "@types/react-dom": "^18.3.0",
 | 
			
		||||
        "@typescript-eslint/eslint-plugin": "^8.18.0",
 | 
			
		||||
        "@typescript-eslint/parser": "^8.18.0",
 | 
			
		||||
        "@vitejs/plugin-react": "^4.3.1",
 | 
			
		||||
        "@vitest/ui": "^3.2.4",
 | 
			
		||||
        "autoprefixer": "^10.4.20",
 | 
			
		||||
        "eslint": "^9.16.0",
 | 
			
		||||
        "eslint-config-prettier": "^9.1.0",
 | 
			
		||||
@@ -90,6 +99,7 @@
 | 
			
		||||
        "eslint-plugin-react-refresh": "^0.4.7",
 | 
			
		||||
        "eslint-plugin-tailwindcss": "^3.17.4",
 | 
			
		||||
        "globals": "^15.13.0",
 | 
			
		||||
        "happy-dom": "^18.0.1",
 | 
			
		||||
        "husky": "^9.1.5",
 | 
			
		||||
        "postcss": "^8.4.40",
 | 
			
		||||
        "prettier": "^3.3.3",
 | 
			
		||||
@@ -97,6 +107,7 @@
 | 
			
		||||
        "tailwindcss": "^3.4.7",
 | 
			
		||||
        "typescript": "^5.2.2",
 | 
			
		||||
        "unplugin-inject-preload": "^3.0.0",
 | 
			
		||||
        "vite": "^5.3.4"
 | 
			
		||||
        "vite": "^5.3.4",
 | 
			
		||||
        "vitest": "^3.2.4"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
User-agent: *
 | 
			
		||||
Allow: /
 | 
			
		||||
Disallow: /
 | 
			
		||||
 | 
			
		||||
Sitemap: https://app.chartdb.io/sitemap.xml
 | 
			
		||||
 
 | 
			
		||||
| 
		 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,7 +1,7 @@
 | 
			
		||||
import { cva } from 'class-variance-authority';
 | 
			
		||||
 | 
			
		||||
export const buttonVariants = cva(
 | 
			
		||||
    'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
 | 
			
		||||
    'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
 | 
			
		||||
    {
 | 
			
		||||
        variants: {
 | 
			
		||||
            variant: {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										137
									
								
								src/components/button/button-with-alternatives.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,137 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { ChevronDownIcon } from '@radix-ui/react-icons';
 | 
			
		||||
import { Slot } from '@radix-ui/react-slot';
 | 
			
		||||
import { type VariantProps } from 'class-variance-authority';
 | 
			
		||||
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import { buttonVariants } from './button-variants';
 | 
			
		||||
import {
 | 
			
		||||
    DropdownMenu,
 | 
			
		||||
    DropdownMenuContent,
 | 
			
		||||
    DropdownMenuItem,
 | 
			
		||||
    DropdownMenuTrigger,
 | 
			
		||||
} from '@/components/dropdown-menu/dropdown-menu';
 | 
			
		||||
import {
 | 
			
		||||
    Tooltip,
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
    TooltipTrigger,
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
 | 
			
		||||
export interface ButtonAlternative {
 | 
			
		||||
    label: string;
 | 
			
		||||
    onClick: () => void;
 | 
			
		||||
    disabled?: boolean;
 | 
			
		||||
    icon?: React.ReactNode;
 | 
			
		||||
    className?: string;
 | 
			
		||||
    tooltip?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ButtonWithAlternativesProps
 | 
			
		||||
    extends React.ButtonHTMLAttributes<HTMLButtonElement>,
 | 
			
		||||
        VariantProps<typeof buttonVariants> {
 | 
			
		||||
    asChild?: boolean;
 | 
			
		||||
    alternatives: Array<ButtonAlternative>;
 | 
			
		||||
    dropdownTriggerClassName?: string;
 | 
			
		||||
    chevronDownIconClassName?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ButtonWithAlternatives = React.forwardRef<
 | 
			
		||||
    HTMLButtonElement,
 | 
			
		||||
    ButtonWithAlternativesProps
 | 
			
		||||
>(
 | 
			
		||||
    (
 | 
			
		||||
        {
 | 
			
		||||
            className,
 | 
			
		||||
            variant,
 | 
			
		||||
            size,
 | 
			
		||||
            asChild = false,
 | 
			
		||||
            alternatives,
 | 
			
		||||
            children,
 | 
			
		||||
            onClick,
 | 
			
		||||
            dropdownTriggerClassName,
 | 
			
		||||
            chevronDownIconClassName,
 | 
			
		||||
            ...props
 | 
			
		||||
        },
 | 
			
		||||
        ref
 | 
			
		||||
    ) => {
 | 
			
		||||
        const Comp = asChild ? Slot : 'button';
 | 
			
		||||
        const hasAlternatives = (alternatives?.length ?? 0) > 0;
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div className="inline-flex items-stretch">
 | 
			
		||||
                <Comp
 | 
			
		||||
                    className={cn(
 | 
			
		||||
                        buttonVariants({ variant, size }),
 | 
			
		||||
                        { 'rounded-r-none': hasAlternatives },
 | 
			
		||||
                        className
 | 
			
		||||
                    )}
 | 
			
		||||
                    ref={ref}
 | 
			
		||||
                    onClick={onClick}
 | 
			
		||||
                    {...props}
 | 
			
		||||
                >
 | 
			
		||||
                    {children}
 | 
			
		||||
                </Comp>
 | 
			
		||||
                {hasAlternatives ? (
 | 
			
		||||
                    <DropdownMenu>
 | 
			
		||||
                        <DropdownMenuTrigger asChild>
 | 
			
		||||
                            <button
 | 
			
		||||
                                className={cn(
 | 
			
		||||
                                    buttonVariants({ variant, size }),
 | 
			
		||||
                                    'rounded-l-none border-l border-l-primary/5 px-2 min-w-0',
 | 
			
		||||
                                    className?.includes('h-') &&
 | 
			
		||||
                                        className.match(/h-\d+/)?.[0],
 | 
			
		||||
                                    className?.includes('text-') &&
 | 
			
		||||
                                        className.match(/text-\w+/)?.[0],
 | 
			
		||||
                                    dropdownTriggerClassName
 | 
			
		||||
                                )}
 | 
			
		||||
                                type="button"
 | 
			
		||||
                            >
 | 
			
		||||
                                <ChevronDownIcon
 | 
			
		||||
                                    className={cn(
 | 
			
		||||
                                        'size-4 shrink-0',
 | 
			
		||||
                                        chevronDownIconClassName
 | 
			
		||||
                                    )}
 | 
			
		||||
                                />
 | 
			
		||||
                            </button>
 | 
			
		||||
                        </DropdownMenuTrigger>
 | 
			
		||||
                        <DropdownMenuContent align="end">
 | 
			
		||||
                            {alternatives.map((alternative, index) => {
 | 
			
		||||
                                const menuItem = (
 | 
			
		||||
                                    <DropdownMenuItem
 | 
			
		||||
                                        key={index}
 | 
			
		||||
                                        onClick={alternative.onClick}
 | 
			
		||||
                                        disabled={alternative.disabled}
 | 
			
		||||
                                        className={cn(alternative.className)}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <span className="flex w-full items-center justify-between gap-2">
 | 
			
		||||
                                            {alternative.label}
 | 
			
		||||
                                            {alternative.icon}
 | 
			
		||||
                                        </span>
 | 
			
		||||
                                    </DropdownMenuItem>
 | 
			
		||||
                                );
 | 
			
		||||
 | 
			
		||||
                                if (alternative.tooltip) {
 | 
			
		||||
                                    return (
 | 
			
		||||
                                        <Tooltip key={index}>
 | 
			
		||||
                                            <TooltipTrigger asChild>
 | 
			
		||||
                                                {menuItem}
 | 
			
		||||
                                            </TooltipTrigger>
 | 
			
		||||
                                            <TooltipContent side="left">
 | 
			
		||||
                                                {alternative.tooltip}
 | 
			
		||||
                                            </TooltipContent>
 | 
			
		||||
                                        </Tooltip>
 | 
			
		||||
                                    );
 | 
			
		||||
                                }
 | 
			
		||||
 | 
			
		||||
                                return menuItem;
 | 
			
		||||
                            })}
 | 
			
		||||
                        </DropdownMenuContent>
 | 
			
		||||
                    </DropdownMenu>
 | 
			
		||||
                ) : null}
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
ButtonWithAlternatives.displayName = 'ButtonWithAlternatives';
 | 
			
		||||
 | 
			
		||||
export { ButtonWithAlternatives };
 | 
			
		||||
@@ -1,2 +1,3 @@
 | 
			
		||||
import './config.ts';
 | 
			
		||||
export { Editor } from '@monaco-editor/react';
 | 
			
		||||
export { DiffEditor } from '@monaco-editor/react';
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import { useTheme } from '@/hooks/use-theme';
 | 
			
		||||
import { useMonaco } from '@monaco-editor/react';
 | 
			
		||||
import { useToast } from '@/components/toast/use-toast';
 | 
			
		||||
import { Button } from '../button/button';
 | 
			
		||||
import type { LucideIcon } from 'lucide-react';
 | 
			
		||||
import { Copy, CopyCheck } from 'lucide-react';
 | 
			
		||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
@@ -18,27 +19,48 @@ export const Editor = lazy(() =>
 | 
			
		||||
    }))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const DiffEditor = lazy(() =>
 | 
			
		||||
    import('./code-editor').then((module) => ({
 | 
			
		||||
        default: module.DiffEditor,
 | 
			
		||||
    }))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
type EditorType = typeof Editor;
 | 
			
		||||
 | 
			
		||||
export interface CodeSnippetAction {
 | 
			
		||||
    label: string;
 | 
			
		||||
    icon: LucideIcon;
 | 
			
		||||
    onClick: () => void;
 | 
			
		||||
    className?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CodeSnippetProps {
 | 
			
		||||
    className?: string;
 | 
			
		||||
    code: string;
 | 
			
		||||
    language?: 'sql' | 'shell';
 | 
			
		||||
    codeToCopy?: string;
 | 
			
		||||
    language?: 'sql' | 'shell' | 'dbml';
 | 
			
		||||
    loading?: boolean;
 | 
			
		||||
    autoScroll?: boolean;
 | 
			
		||||
    isComplete?: boolean;
 | 
			
		||||
    editorProps?: React.ComponentProps<EditorType>;
 | 
			
		||||
    actions?: CodeSnippetAction[];
 | 
			
		||||
    actionsTooltipSide?: 'top' | 'right' | 'bottom' | 'left';
 | 
			
		||||
    allowCopy?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
    ({
 | 
			
		||||
        className,
 | 
			
		||||
        code,
 | 
			
		||||
        codeToCopy,
 | 
			
		||||
        loading,
 | 
			
		||||
        language = 'sql',
 | 
			
		||||
        autoScroll = false,
 | 
			
		||||
        isComplete = true,
 | 
			
		||||
        editorProps,
 | 
			
		||||
        actions,
 | 
			
		||||
        actionsTooltipSide,
 | 
			
		||||
        allowCopy = true,
 | 
			
		||||
    }) => {
 | 
			
		||||
        const { t } = useTranslation();
 | 
			
		||||
        const monaco = useMonaco();
 | 
			
		||||
@@ -85,7 +107,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                await navigator.clipboard.writeText(code);
 | 
			
		||||
                await navigator.clipboard.writeText(codeToCopy ?? code);
 | 
			
		||||
                setIsCopied(true);
 | 
			
		||||
            } catch {
 | 
			
		||||
                setIsCopied(false);
 | 
			
		||||
@@ -97,7 +119,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
                    ),
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }, [code, t, toast]);
 | 
			
		||||
        }, [code, codeToCopy, t, toast]);
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div
 | 
			
		||||
@@ -111,36 +133,67 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
                ) : (
 | 
			
		||||
                    <Suspense fallback={<Spinner />}>
 | 
			
		||||
                        {isComplete ? (
 | 
			
		||||
                            <Tooltip
 | 
			
		||||
                                onOpenChange={setTooltipOpen}
 | 
			
		||||
                                open={isCopied || tooltipOpen}
 | 
			
		||||
                            >
 | 
			
		||||
                                <TooltipTrigger
 | 
			
		||||
                                    asChild
 | 
			
		||||
                                    className="absolute right-1 top-1 z-10"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <span>
 | 
			
		||||
                                        <Button
 | 
			
		||||
                                            className=" h-fit p-1.5"
 | 
			
		||||
                                            variant="outline"
 | 
			
		||||
                                            onClick={copyToClipboard}
 | 
			
		||||
                            <div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
 | 
			
		||||
                                {allowCopy ? (
 | 
			
		||||
                                    <Tooltip
 | 
			
		||||
                                        onOpenChange={setTooltipOpen}
 | 
			
		||||
                                        open={isCopied || tooltipOpen}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <TooltipTrigger asChild>
 | 
			
		||||
                                            <span>
 | 
			
		||||
                                                <Button
 | 
			
		||||
                                                    className="h-fit p-1.5"
 | 
			
		||||
                                                    variant="outline"
 | 
			
		||||
                                                    onClick={copyToClipboard}
 | 
			
		||||
                                                >
 | 
			
		||||
                                                    {isCopied ? (
 | 
			
		||||
                                                        <CopyCheck size={16} />
 | 
			
		||||
                                                    ) : (
 | 
			
		||||
                                                        <Copy size={16} />
 | 
			
		||||
                                                    )}
 | 
			
		||||
                                                </Button>
 | 
			
		||||
                                            </span>
 | 
			
		||||
                                        </TooltipTrigger>
 | 
			
		||||
                                        <TooltipContent
 | 
			
		||||
                                            side={actionsTooltipSide}
 | 
			
		||||
                                        >
 | 
			
		||||
                                            {isCopied ? (
 | 
			
		||||
                                                <CopyCheck size={16} />
 | 
			
		||||
                                            ) : (
 | 
			
		||||
                                                <Copy size={16} />
 | 
			
		||||
                                            {t(
 | 
			
		||||
                                                isCopied
 | 
			
		||||
                                                    ? 'copied'
 | 
			
		||||
                                                    : 'copy_to_clipboard'
 | 
			
		||||
                                            )}
 | 
			
		||||
                                        </Button>
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </TooltipTrigger>
 | 
			
		||||
                                <TooltipContent>
 | 
			
		||||
                                    {t(
 | 
			
		||||
                                        isCopied
 | 
			
		||||
                                            ? 'copied'
 | 
			
		||||
                                            : 'copy_to_clipboard'
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </TooltipContent>
 | 
			
		||||
                            </Tooltip>
 | 
			
		||||
                                        </TooltipContent>
 | 
			
		||||
                                    </Tooltip>
 | 
			
		||||
                                ) : null}
 | 
			
		||||
 | 
			
		||||
                                {actions &&
 | 
			
		||||
                                    actions.length > 0 &&
 | 
			
		||||
                                    actions.map((action, index) => (
 | 
			
		||||
                                        <Tooltip key={index}>
 | 
			
		||||
                                            <TooltipTrigger asChild>
 | 
			
		||||
                                                <span>
 | 
			
		||||
                                                    <Button
 | 
			
		||||
                                                        className={cn(
 | 
			
		||||
                                                            'h-fit p-1.5',
 | 
			
		||||
                                                            action.className
 | 
			
		||||
                                                        )}
 | 
			
		||||
                                                        variant="outline"
 | 
			
		||||
                                                        onClick={action.onClick}
 | 
			
		||||
                                                    >
 | 
			
		||||
                                                        <action.icon
 | 
			
		||||
                                                            size={16}
 | 
			
		||||
                                                        />
 | 
			
		||||
                                                    </Button>
 | 
			
		||||
                                                </span>
 | 
			
		||||
                                            </TooltipTrigger>
 | 
			
		||||
                                            <TooltipContent
 | 
			
		||||
                                                side={actionsTooltipSide}
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {action.label}
 | 
			
		||||
                                            </TooltipContent>
 | 
			
		||||
                                        </Tooltip>
 | 
			
		||||
                                    ))}
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ) : null}
 | 
			
		||||
 | 
			
		||||
                        <Editor
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										51
									
								
								src/components/code-snippet/dbml/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,51 @@
 | 
			
		||||
import type { DBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
 | 
			
		||||
import * as monaco from 'monaco-editor';
 | 
			
		||||
 | 
			
		||||
export const highlightErrorLine = ({
 | 
			
		||||
    error,
 | 
			
		||||
    model,
 | 
			
		||||
    editorDecorationsCollection,
 | 
			
		||||
}: {
 | 
			
		||||
    error: DBMLError;
 | 
			
		||||
    model?: monaco.editor.ITextModel | null;
 | 
			
		||||
    editorDecorationsCollection:
 | 
			
		||||
        | monaco.editor.IEditorDecorationsCollection
 | 
			
		||||
        | undefined;
 | 
			
		||||
}) => {
 | 
			
		||||
    if (!model) return;
 | 
			
		||||
    if (!editorDecorationsCollection) return;
 | 
			
		||||
 | 
			
		||||
    const decorations = [
 | 
			
		||||
        {
 | 
			
		||||
            range: new monaco.Range(
 | 
			
		||||
                error.line,
 | 
			
		||||
                1,
 | 
			
		||||
                error.line,
 | 
			
		||||
                model.getLineMaxColumn(error.line)
 | 
			
		||||
            ),
 | 
			
		||||
            options: {
 | 
			
		||||
                isWholeLine: true,
 | 
			
		||||
                className: 'dbml-error-line',
 | 
			
		||||
                glyphMarginClassName: 'dbml-error-glyph',
 | 
			
		||||
                hoverMessage: { value: error.message },
 | 
			
		||||
                overviewRuler: {
 | 
			
		||||
                    color: '#ff0000',
 | 
			
		||||
                    position: monaco.editor.OverviewRulerLane.Right,
 | 
			
		||||
                    darkColor: '#ff0000',
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    editorDecorationsCollection?.set(decorations);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const clearErrorHighlight = (
 | 
			
		||||
    editorDecorationsCollection:
 | 
			
		||||
        | monaco.editor.IEditorDecorationsCollection
 | 
			
		||||
        | undefined
 | 
			
		||||
) => {
 | 
			
		||||
    if (editorDecorationsCollection) {
 | 
			
		||||
        editorDecorationsCollection.clear();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
@@ -9,12 +9,14 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
 | 
			
		||||
        base: 'vs-dark',
 | 
			
		||||
        inherit: true,
 | 
			
		||||
        rules: [
 | 
			
		||||
            { token: 'comment', foreground: '6A9955' }, // Comments
 | 
			
		||||
            { 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
 | 
			
		||||
            { token: 'type', foreground: '4EC9B0' }, // Data types
 | 
			
		||||
            { token: 'identifier', foreground: '9CDCFE' }, // Field names
 | 
			
		||||
        ],
 | 
			
		||||
        colors: {},
 | 
			
		||||
    });
 | 
			
		||||
@@ -23,12 +25,14 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
 | 
			
		||||
        base: 'vs',
 | 
			
		||||
        inherit: true,
 | 
			
		||||
        rules: [
 | 
			
		||||
            { token: 'comment', foreground: '008000' }, // Comments
 | 
			
		||||
            { 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
 | 
			
		||||
            { token: 'identifier', foreground: '001080' }, // Field names
 | 
			
		||||
        ],
 | 
			
		||||
        colors: {},
 | 
			
		||||
    });
 | 
			
		||||
@@ -37,17 +41,63 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
 | 
			
		||||
    const datatypePattern = dataTypesNames.join('|');
 | 
			
		||||
 | 
			
		||||
    monaco.languages.setMonarchTokensProvider('dbml', {
 | 
			
		||||
        keywords: ['Table', 'Ref', 'Indexes'],
 | 
			
		||||
        keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum', 'enum'],
 | 
			
		||||
        datatypes: dataTypesNames,
 | 
			
		||||
        operators: ['>', '<', '-'],
 | 
			
		||||
 | 
			
		||||
        tokenizer: {
 | 
			
		||||
            root: [
 | 
			
		||||
                [/\b(Table|Ref|Indexes)\b/, 'keyword'],
 | 
			
		||||
                // Comments
 | 
			
		||||
                [/\/\/.*$/, 'comment'],
 | 
			
		||||
 | 
			
		||||
                // Keywords - case insensitive
 | 
			
		||||
                [
 | 
			
		||||
                    /\b([Tt][Aa][Bb][Ll][Ee]|[Ee][Nn][Uu][Mm]|[Rr][Ee][Ff]|[Ii][Nn][Dd][Ee][Xx][Ee][Ss]|[Nn][Oo][Tt][Ee])\b/,
 | 
			
		||||
                    'keyword',
 | 
			
		||||
                ],
 | 
			
		||||
 | 
			
		||||
                // Annotations in brackets
 | 
			
		||||
                [/\[.*?\]/, 'annotation'],
 | 
			
		||||
                [/".*?"/, 'string'],
 | 
			
		||||
                [/'.*?'/, 'string'],
 | 
			
		||||
                [/[{}]/, 'delimiter'],
 | 
			
		||||
                [/[<>]/, 'operator'],
 | 
			
		||||
                [new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
 | 
			
		||||
 | 
			
		||||
                // Strings
 | 
			
		||||
                [/'''/, 'string', '@tripleQuoteString'],
 | 
			
		||||
                [/"([^"\\]|\\.)*$/, 'string.invalid'], // non-terminated string
 | 
			
		||||
                [/'([^'\\]|\\.)*$/, 'string.invalid'], // non-terminated string
 | 
			
		||||
                [/"/, 'string', '@string_double'],
 | 
			
		||||
                [/'/, 'string', '@string_single'],
 | 
			
		||||
                [/`.*?`/, 'string'],
 | 
			
		||||
 | 
			
		||||
                // Delimiters and operators
 | 
			
		||||
                [/[{}()]/, 'delimiter'],
 | 
			
		||||
                [/[<>-]/, 'operator'],
 | 
			
		||||
                [/:/, 'delimiter'],
 | 
			
		||||
 | 
			
		||||
                // Data types
 | 
			
		||||
                [new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'],
 | 
			
		||||
 | 
			
		||||
                // Numbers
 | 
			
		||||
                [/\d+/, 'number'],
 | 
			
		||||
 | 
			
		||||
                // Identifiers
 | 
			
		||||
                [/[a-zA-Z_]\w*/, 'identifier'],
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
            string_double: [
 | 
			
		||||
                [/[^\\"]+/, 'string'],
 | 
			
		||||
                [/\\./, 'string.escape'],
 | 
			
		||||
                [/"/, 'string', '@pop'],
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
            string_single: [
 | 
			
		||||
                [/[^\\']+/, 'string'],
 | 
			
		||||
                [/\\./, 'string.escape'],
 | 
			
		||||
                [/'/, 'string', '@pop'],
 | 
			
		||||
            ],
 | 
			
		||||
 | 
			
		||||
            tripleQuoteString: [
 | 
			
		||||
                [/[^']+/, 'string'],
 | 
			
		||||
                [/'''/, 'string', '@pop'],
 | 
			
		||||
                [/'/, 'string'],
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
 
 | 
			
		||||
@@ -5,27 +5,45 @@ import {
 | 
			
		||||
    PopoverTrigger,
 | 
			
		||||
} from '@/components/popover/popover';
 | 
			
		||||
import { colorOptions } from '@/lib/colors';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
export interface ColorPickerProps {
 | 
			
		||||
    color: string;
 | 
			
		||||
    onChange: (color: string) => void;
 | 
			
		||||
    disabled?: boolean;
 | 
			
		||||
    popoverOnMouseDown?: (e: React.MouseEvent) => void;
 | 
			
		||||
    popoverOnClick?: (e: React.MouseEvent) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ColorPicker = React.forwardRef<
 | 
			
		||||
    React.ElementRef<typeof PopoverTrigger>,
 | 
			
		||||
    ColorPickerProps
 | 
			
		||||
>(({ color, onChange }, ref) => {
 | 
			
		||||
>(({ color, onChange, disabled, popoverOnMouseDown, popoverOnClick }, ref) => {
 | 
			
		||||
    return (
 | 
			
		||||
        <Popover>
 | 
			
		||||
            <PopoverTrigger asChild ref={ref}>
 | 
			
		||||
            <PopoverTrigger
 | 
			
		||||
                asChild
 | 
			
		||||
                ref={ref}
 | 
			
		||||
                disabled={disabled}
 | 
			
		||||
                {...(disabled ? { onClick: (e) => e.preventDefault() } : {})}
 | 
			
		||||
            >
 | 
			
		||||
                <div
 | 
			
		||||
                    className="h-6 w-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md"
 | 
			
		||||
                    className={cn(
 | 
			
		||||
                        'h-6 w-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md',
 | 
			
		||||
                        {
 | 
			
		||||
                            'hover:shadow-none cursor-default': disabled,
 | 
			
		||||
                        }
 | 
			
		||||
                    )}
 | 
			
		||||
                    style={{
 | 
			
		||||
                        backgroundColor: color,
 | 
			
		||||
                    }}
 | 
			
		||||
                />
 | 
			
		||||
            </PopoverTrigger>
 | 
			
		||||
            <PopoverContent className="w-fit">
 | 
			
		||||
            <PopoverContent
 | 
			
		||||
                className="w-fit"
 | 
			
		||||
                onMouseDown={popoverOnMouseDown}
 | 
			
		||||
                onClick={popoverOnClick}
 | 
			
		||||
            >
 | 
			
		||||
                <div className="grid grid-cols-4 gap-2">
 | 
			
		||||
                    {colorOptions.map((option) => (
 | 
			
		||||
                        <div
 | 
			
		||||
@@ -22,14 +22,15 @@ export interface DiagramIconProps
 | 
			
		||||
export const DiagramIcon = React.forwardRef<
 | 
			
		||||
    React.ElementRef<typeof TooltipTrigger>,
 | 
			
		||||
    DiagramIconProps
 | 
			
		||||
>(({ databaseType, databaseEdition, className, imgClassName }, ref) =>
 | 
			
		||||
>(({ databaseType, databaseEdition, className, imgClassName, onClick }, ref) =>
 | 
			
		||||
    databaseEdition ? (
 | 
			
		||||
        <Tooltip>
 | 
			
		||||
            <TooltipTrigger className={cn('mr-1', className)} ref={ref} asChild>
 | 
			
		||||
                <img
 | 
			
		||||
                    src={databaseEditionToImageMap[databaseEdition]}
 | 
			
		||||
                    className={cn('h-5 max-w-fit rounded-full', imgClassName)}
 | 
			
		||||
                    className={cn('max-h-5 max-w-5 rounded-full', imgClassName)}
 | 
			
		||||
                    alt="database"
 | 
			
		||||
                    onClick={onClick}
 | 
			
		||||
                />
 | 
			
		||||
            </TooltipTrigger>
 | 
			
		||||
            <TooltipContent>
 | 
			
		||||
@@ -41,8 +42,9 @@ export const DiagramIcon = React.forwardRef<
 | 
			
		||||
            <TooltipTrigger className={cn('mr-2', className)} ref={ref} asChild>
 | 
			
		||||
                <img
 | 
			
		||||
                    src={databaseSecondaryLogoMap[databaseType]}
 | 
			
		||||
                    className={cn('h-5 max-w-fit', imgClassName)}
 | 
			
		||||
                    className={cn('max-h-5 max-w-5', imgClassName)}
 | 
			
		||||
                    alt="database"
 | 
			
		||||
                    onClick={onClick}
 | 
			
		||||
                />
 | 
			
		||||
            </TooltipTrigger>
 | 
			
		||||
            <TooltipContent>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import { Cross2Icon } from '@radix-ui/react-icons';
 | 
			
		||||
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import { ScrollArea } from '../scroll-area/scroll-area';
 | 
			
		||||
import { ChevronLeft } from 'lucide-react';
 | 
			
		||||
 | 
			
		||||
const Dialog = DialogPrimitive.Root;
 | 
			
		||||
 | 
			
		||||
@@ -32,28 +33,75 @@ const DialogContent = React.forwardRef<
 | 
			
		||||
    React.ElementRef<typeof DialogPrimitive.Content>,
 | 
			
		||||
    React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
 | 
			
		||||
        showClose?: boolean;
 | 
			
		||||
        showBack?: boolean;
 | 
			
		||||
        backButtonClassName?: string;
 | 
			
		||||
        blurBackground?: boolean;
 | 
			
		||||
        forceOverlay?: boolean;
 | 
			
		||||
        onBackClick?: () => void;
 | 
			
		||||
    }
 | 
			
		||||
>(({ className, children, showClose, ...props }, ref) => (
 | 
			
		||||
    <DialogPortal>
 | 
			
		||||
        <DialogOverlay />
 | 
			
		||||
        <DialogPrimitive.Content
 | 
			
		||||
            ref={ref}
 | 
			
		||||
            className={cn(
 | 
			
		||||
                'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
 | 
			
		||||
                className
 | 
			
		||||
            )}
 | 
			
		||||
            {...props}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
            {showClose && (
 | 
			
		||||
                <DialogPrimitive.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-accent data-[state=open]:text-muted-foreground">
 | 
			
		||||
                    <Cross2Icon className="size-4" />
 | 
			
		||||
                    <span className="sr-only">Close</span>
 | 
			
		||||
                </DialogPrimitive.Close>
 | 
			
		||||
            )}
 | 
			
		||||
        </DialogPrimitive.Content>
 | 
			
		||||
    </DialogPortal>
 | 
			
		||||
));
 | 
			
		||||
>(
 | 
			
		||||
    (
 | 
			
		||||
        {
 | 
			
		||||
            className,
 | 
			
		||||
            children,
 | 
			
		||||
            showClose,
 | 
			
		||||
            showBack,
 | 
			
		||||
            onBackClick,
 | 
			
		||||
            backButtonClassName,
 | 
			
		||||
            blurBackground,
 | 
			
		||||
            forceOverlay,
 | 
			
		||||
            ...props
 | 
			
		||||
        },
 | 
			
		||||
        ref
 | 
			
		||||
    ) => (
 | 
			
		||||
        <DialogPortal>
 | 
			
		||||
            {forceOverlay ? (
 | 
			
		||||
                <div
 | 
			
		||||
                    className={cn(
 | 
			
		||||
                        'fixed inset-0 z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
 | 
			
		||||
                        {
 | 
			
		||||
                            'bg-black/80': !blurBackground,
 | 
			
		||||
                            'bg-black/30 backdrop-blur-sm': blurBackground,
 | 
			
		||||
                        }
 | 
			
		||||
                    )}
 | 
			
		||||
                    data-state="open"
 | 
			
		||||
                />
 | 
			
		||||
            ) : null}
 | 
			
		||||
            <DialogOverlay
 | 
			
		||||
                className={cn({
 | 
			
		||||
                    'bg-black/30 backdrop-blur-sm': blurBackground,
 | 
			
		||||
                })}
 | 
			
		||||
            />
 | 
			
		||||
            <DialogPrimitive.Content
 | 
			
		||||
                ref={ref}
 | 
			
		||||
                className={cn(
 | 
			
		||||
                    'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
 | 
			
		||||
                    className
 | 
			
		||||
                )}
 | 
			
		||||
                {...props}
 | 
			
		||||
            >
 | 
			
		||||
                {children}
 | 
			
		||||
                {showBack && (
 | 
			
		||||
                    <button
 | 
			
		||||
                        onClick={() => onBackClick?.()}
 | 
			
		||||
                        className={cn(
 | 
			
		||||
                            'absolute left-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-accent data-[state=open]:text-muted-foreground',
 | 
			
		||||
                            backButtonClassName
 | 
			
		||||
                        )}
 | 
			
		||||
                    >
 | 
			
		||||
                        <ChevronLeft className="size-4" />
 | 
			
		||||
                    </button>
 | 
			
		||||
                )}
 | 
			
		||||
                {showClose && (
 | 
			
		||||
                    <DialogPrimitive.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-accent data-[state=open]:text-muted-foreground">
 | 
			
		||||
                        <Cross2Icon className="size-4" />
 | 
			
		||||
                        <span className="sr-only">Close</span>
 | 
			
		||||
                    </DialogPrimitive.Close>
 | 
			
		||||
                )}
 | 
			
		||||
            </DialogPrimitive.Content>
 | 
			
		||||
        </DialogPortal>
 | 
			
		||||
    )
 | 
			
		||||
);
 | 
			
		||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
 | 
			
		||||
 | 
			
		||||
const DialogHeader = ({
 | 
			
		||||
 
 | 
			
		||||
@@ -52,7 +52,7 @@ export const EmptyState = forwardRef<
 | 
			
		||||
                </Label>
 | 
			
		||||
                <Label
 | 
			
		||||
                    className={cn(
 | 
			
		||||
                        'text-sm font-normal text-muted-foreground',
 | 
			
		||||
                        'text-sm text-center font-normal text-muted-foreground',
 | 
			
		||||
                        descriptionClassName
 | 
			
		||||
                    )}
 | 
			
		||||
                >
 | 
			
		||||
 
 | 
			
		||||
@@ -2,16 +2,13 @@ import React from 'react';
 | 
			
		||||
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
export interface InputProps
 | 
			
		||||
    extends React.InputHTMLAttributes<HTMLInputElement> {}
 | 
			
		||||
 | 
			
		||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
 | 
			
		||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
 | 
			
		||||
    ({ className, type, ...props }, ref) => {
 | 
			
		||||
        return (
 | 
			
		||||
            <input
 | 
			
		||||
                type={type}
 | 
			
		||||
                className={cn(
 | 
			
		||||
                    'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
 | 
			
		||||
                    'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
 | 
			
		||||
                    className
 | 
			
		||||
                )}
 | 
			
		||||
                ref={ref}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										121
									
								
								src/components/pagination/pagination.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,121 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import type { ButtonProps } from '../button/button';
 | 
			
		||||
import { buttonVariants } from '../button/button-variants';
 | 
			
		||||
import {
 | 
			
		||||
    ChevronLeftIcon,
 | 
			
		||||
    ChevronRightIcon,
 | 
			
		||||
    DotsHorizontalIcon,
 | 
			
		||||
} from '@radix-ui/react-icons';
 | 
			
		||||
 | 
			
		||||
const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => (
 | 
			
		||||
    <nav
 | 
			
		||||
        role="navigation"
 | 
			
		||||
        aria-label="pagination"
 | 
			
		||||
        className={cn('mx-auto flex w-full justify-center', className)}
 | 
			
		||||
        {...props}
 | 
			
		||||
    />
 | 
			
		||||
);
 | 
			
		||||
Pagination.displayName = 'Pagination';
 | 
			
		||||
 | 
			
		||||
const PaginationContent = React.forwardRef<
 | 
			
		||||
    HTMLUListElement,
 | 
			
		||||
    React.ComponentProps<'ul'>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
    <ul
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        className={cn('flex flex-row items-center gap-1', className)}
 | 
			
		||||
        {...props}
 | 
			
		||||
    />
 | 
			
		||||
));
 | 
			
		||||
PaginationContent.displayName = 'PaginationContent';
 | 
			
		||||
 | 
			
		||||
const PaginationItem = React.forwardRef<
 | 
			
		||||
    HTMLLIElement,
 | 
			
		||||
    React.ComponentProps<'li'>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
    <li ref={ref} className={cn('', className)} {...props} />
 | 
			
		||||
));
 | 
			
		||||
PaginationItem.displayName = 'PaginationItem';
 | 
			
		||||
 | 
			
		||||
type PaginationLinkProps = {
 | 
			
		||||
    isActive?: boolean;
 | 
			
		||||
} & Pick<ButtonProps, 'size'> &
 | 
			
		||||
    React.ComponentProps<'a'>;
 | 
			
		||||
 | 
			
		||||
const PaginationLink = ({
 | 
			
		||||
    className,
 | 
			
		||||
    isActive,
 | 
			
		||||
    size = 'icon',
 | 
			
		||||
    ...props
 | 
			
		||||
}: PaginationLinkProps) => (
 | 
			
		||||
    <a
 | 
			
		||||
        aria-current={isActive ? 'page' : undefined}
 | 
			
		||||
        className={cn(
 | 
			
		||||
            buttonVariants({
 | 
			
		||||
                variant: isActive ? 'outline' : 'ghost',
 | 
			
		||||
                size,
 | 
			
		||||
            }),
 | 
			
		||||
            className
 | 
			
		||||
        )}
 | 
			
		||||
        {...props}
 | 
			
		||||
    />
 | 
			
		||||
);
 | 
			
		||||
PaginationLink.displayName = 'PaginationLink';
 | 
			
		||||
 | 
			
		||||
const PaginationPrevious = ({
 | 
			
		||||
    className,
 | 
			
		||||
    ...props
 | 
			
		||||
}: React.ComponentProps<typeof PaginationLink>) => (
 | 
			
		||||
    <PaginationLink
 | 
			
		||||
        aria-label="Go to previous page"
 | 
			
		||||
        size="default"
 | 
			
		||||
        className={cn('gap-1 pl-2.5', className)}
 | 
			
		||||
        {...props}
 | 
			
		||||
    >
 | 
			
		||||
        <ChevronLeftIcon className="size-4" />
 | 
			
		||||
        <span>Previous</span>
 | 
			
		||||
    </PaginationLink>
 | 
			
		||||
);
 | 
			
		||||
PaginationPrevious.displayName = 'PaginationPrevious';
 | 
			
		||||
 | 
			
		||||
const PaginationNext = ({
 | 
			
		||||
    className,
 | 
			
		||||
    ...props
 | 
			
		||||
}: React.ComponentProps<typeof PaginationLink>) => (
 | 
			
		||||
    <PaginationLink
 | 
			
		||||
        aria-label="Go to next page"
 | 
			
		||||
        size="default"
 | 
			
		||||
        className={cn('gap-1 pr-2.5', className)}
 | 
			
		||||
        {...props}
 | 
			
		||||
    >
 | 
			
		||||
        <span>Next</span>
 | 
			
		||||
        <ChevronRightIcon className="size-4" />
 | 
			
		||||
    </PaginationLink>
 | 
			
		||||
);
 | 
			
		||||
PaginationNext.displayName = 'PaginationNext';
 | 
			
		||||
 | 
			
		||||
const PaginationEllipsis = ({
 | 
			
		||||
    className,
 | 
			
		||||
    ...props
 | 
			
		||||
}: React.ComponentProps<'span'>) => (
 | 
			
		||||
    <span
 | 
			
		||||
        aria-hidden
 | 
			
		||||
        className={cn('flex h-9 w-9 items-center justify-center', className)}
 | 
			
		||||
        {...props}
 | 
			
		||||
    >
 | 
			
		||||
        <DotsHorizontalIcon className="size-4" />
 | 
			
		||||
        <span className="sr-only">More pages</span>
 | 
			
		||||
    </span>
 | 
			
		||||
);
 | 
			
		||||
PaginationEllipsis.displayName = 'PaginationEllipsis';
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
    Pagination,
 | 
			
		||||
    PaginationContent,
 | 
			
		||||
    PaginationLink,
 | 
			
		||||
    PaginationItem,
 | 
			
		||||
    PaginationPrevious,
 | 
			
		||||
    PaginationNext,
 | 
			
		||||
    PaginationEllipsis,
 | 
			
		||||
};
 | 
			
		||||
@@ -26,6 +26,8 @@ export interface SelectBoxOption {
 | 
			
		||||
    description?: string;
 | 
			
		||||
    regex?: string;
 | 
			
		||||
    extractRegex?: RegExp;
 | 
			
		||||
    group?: string;
 | 
			
		||||
    icon?: React.ReactNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SelectBoxProps {
 | 
			
		||||
@@ -51,6 +53,11 @@ export interface SelectBoxProps {
 | 
			
		||||
    disabled?: boolean;
 | 
			
		||||
    open?: boolean;
 | 
			
		||||
    onOpenChange?: (open: boolean) => void;
 | 
			
		||||
    popoverClassName?: string;
 | 
			
		||||
    readonly?: boolean;
 | 
			
		||||
    footerButtons?: React.ReactNode;
 | 
			
		||||
    commandOnMouseDown?: (e: React.MouseEvent) => void;
 | 
			
		||||
    commandOnClick?: (e: React.MouseEvent) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
@@ -75,6 +82,11 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
            disabled,
 | 
			
		||||
            open,
 | 
			
		||||
            onOpenChange: setOpen,
 | 
			
		||||
            popoverClassName,
 | 
			
		||||
            readonly,
 | 
			
		||||
            footerButtons,
 | 
			
		||||
            commandOnMouseDown,
 | 
			
		||||
            commandOnClick,
 | 
			
		||||
        },
 | 
			
		||||
        ref
 | 
			
		||||
    ) => {
 | 
			
		||||
@@ -90,6 +102,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
            (isOpen: boolean) => {
 | 
			
		||||
                setOpen?.(isOpen);
 | 
			
		||||
                setIsOpen(isOpen);
 | 
			
		||||
 | 
			
		||||
                if (isOpen) {
 | 
			
		||||
                    setSearchTerm('');
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                setTimeout(() => (document.body.style.pointerEvents = ''), 500);
 | 
			
		||||
            },
 | 
			
		||||
            [setOpen]
 | 
			
		||||
        );
 | 
			
		||||
@@ -143,18 +161,20 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                            className={`inline-flex min-w-0 shrink-0 items-center gap-1 rounded-md border py-0.5 pl-2 pr-1 text-xs font-medium text-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 ${oneLine ? 'mx-0.5' : ''}`}
 | 
			
		||||
                        >
 | 
			
		||||
                            <span>{option.label}</span>
 | 
			
		||||
                            <span
 | 
			
		||||
                                onClick={(e) => {
 | 
			
		||||
                                    e.preventDefault();
 | 
			
		||||
                                    handleSelect(option.value);
 | 
			
		||||
                                }}
 | 
			
		||||
                                className="flex items-center rounded-sm px-px text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground"
 | 
			
		||||
                            >
 | 
			
		||||
                                <Cross2Icon />
 | 
			
		||||
                            </span>
 | 
			
		||||
                            {!readonly ? (
 | 
			
		||||
                                <span
 | 
			
		||||
                                    onClick={(e) => {
 | 
			
		||||
                                        e.preventDefault();
 | 
			
		||||
                                        handleSelect(option.value);
 | 
			
		||||
                                    }}
 | 
			
		||||
                                    className="flex items-center rounded-sm px-px text-muted-foreground/60 hover:bg-accent hover:text-muted-foreground"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <Cross2Icon />
 | 
			
		||||
                                </span>
 | 
			
		||||
                            ) : null}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    )),
 | 
			
		||||
            [options, value, handleSelect, oneLine, keepOrder]
 | 
			
		||||
            [options, value, handleSelect, oneLine, keepOrder, readonly]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const isAllSelected = React.useMemo(
 | 
			
		||||
@@ -175,12 +195,122 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
            [isOpen, onOpenChange]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const groups = React.useMemo(
 | 
			
		||||
            () =>
 | 
			
		||||
                options.reduce(
 | 
			
		||||
                    (acc, option) => {
 | 
			
		||||
                        if (option.group) {
 | 
			
		||||
                            if (!acc[option.group]) {
 | 
			
		||||
                                acc[option.group] = [];
 | 
			
		||||
                            }
 | 
			
		||||
                            acc[option.group].push(option);
 | 
			
		||||
                        } else {
 | 
			
		||||
                            if (!acc['default']) {
 | 
			
		||||
                                acc['default'] = [];
 | 
			
		||||
                            }
 | 
			
		||||
                            acc['default'].push(option);
 | 
			
		||||
                        }
 | 
			
		||||
                        return acc;
 | 
			
		||||
                    },
 | 
			
		||||
                    {} as Record<string, SelectBoxOption[]>
 | 
			
		||||
                ),
 | 
			
		||||
            [options]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const hasGroups = React.useMemo(
 | 
			
		||||
            () =>
 | 
			
		||||
                Object.keys(groups).filter((group) => group !== 'default')
 | 
			
		||||
                    .length > 0,
 | 
			
		||||
            [groups]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const renderOption = React.useCallback(
 | 
			
		||||
            (option: SelectBoxOption) => {
 | 
			
		||||
                const isSelected =
 | 
			
		||||
                    Array.isArray(value) && value.includes(option.value);
 | 
			
		||||
 | 
			
		||||
                const isRegexMatch =
 | 
			
		||||
                    option.regex && new RegExp(option.regex)?.test(searchTerm);
 | 
			
		||||
 | 
			
		||||
                const matches = option.extractRegex
 | 
			
		||||
                    ? searchTerm.match(option.extractRegex)
 | 
			
		||||
                    : undefined;
 | 
			
		||||
 | 
			
		||||
                return (
 | 
			
		||||
                    <CommandItem
 | 
			
		||||
                        className="flex items-center"
 | 
			
		||||
                        key={option.value}
 | 
			
		||||
                        keywords={option.regex ? [option.regex] : undefined}
 | 
			
		||||
                        onSelect={() =>
 | 
			
		||||
                            handleSelect(
 | 
			
		||||
                                option.value,
 | 
			
		||||
                                matches?.map((match) => match?.toString())
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                        onMouseDown={commandOnMouseDown}
 | 
			
		||||
                        onClick={commandOnClick}
 | 
			
		||||
                    >
 | 
			
		||||
                        {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">
 | 
			
		||||
                            {option.icon ? (
 | 
			
		||||
                                <span className="mr-2 shrink-0">
 | 
			
		||||
                                    {option.icon}
 | 
			
		||||
                                </span>
 | 
			
		||||
                            ) : null}
 | 
			
		||||
                            <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,
 | 
			
		||||
                commandOnClick,
 | 
			
		||||
                commandOnMouseDown,
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
 | 
			
		||||
                <PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
 | 
			
		||||
                    <div
 | 
			
		||||
                        className={cn(
 | 
			
		||||
                            `flex min-h-[36px] cursor-pointer items-center justify-between rounded-md border px-3 py-1 data-[state=open]:border-ring ${disabled ? 'bg-muted pointer-events-none' : ''}`,
 | 
			
		||||
                            `flex min-h-[36px] cursor-pointer items-center justify-between rounded-md border px-3 py-1 data-[state=open]:border-ring ${disabled ? 'bg-muted pointer-events-none' : ''} ${readonly ? 'pointer-events-none' : ''}`,
 | 
			
		||||
                            className
 | 
			
		||||
                        )}
 | 
			
		||||
                    >
 | 
			
		||||
@@ -245,8 +375,13 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                    </div>
 | 
			
		||||
                </PopoverTrigger>
 | 
			
		||||
                <PopoverContent
 | 
			
		||||
                    className="w-fit min-w-[var(--radix-popover-trigger-width)] p-0"
 | 
			
		||||
                    className={cn(
 | 
			
		||||
                        'w-fit min-w-[var(--radix-popover-trigger-width)] p-0',
 | 
			
		||||
                        popoverClassName
 | 
			
		||||
                    )}
 | 
			
		||||
                    align="center"
 | 
			
		||||
                    onMouseDown={(e) => e.stopPropagation()}
 | 
			
		||||
                    onClick={(e) => e.stopPropagation()}
 | 
			
		||||
                >
 | 
			
		||||
                    <Command
 | 
			
		||||
                        filter={(value, search, keywords) => {
 | 
			
		||||
@@ -317,99 +452,28 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
 | 
			
		||||
                        <ScrollArea>
 | 
			
		||||
                            <div className="max-h-64 w-full">
 | 
			
		||||
                                <CommandGroup>
 | 
			
		||||
                                    <CommandList className="max-h-fit w-full">
 | 
			
		||||
                                        {options.map((option) => {
 | 
			
		||||
                                            const isSelected =
 | 
			
		||||
                                                Array.isArray(value) &&
 | 
			
		||||
                                                value.includes(option.value);
 | 
			
		||||
 | 
			
		||||
                                            const isRegexMatch =
 | 
			
		||||
                                                option.regex &&
 | 
			
		||||
                                                new RegExp(option.regex)?.test(
 | 
			
		||||
                                                    searchTerm
 | 
			
		||||
                                                );
 | 
			
		||||
 | 
			
		||||
                                            const matches = option.extractRegex
 | 
			
		||||
                                                ? searchTerm.match(
 | 
			
		||||
                                                      option.extractRegex
 | 
			
		||||
                                                  )
 | 
			
		||||
                                                : undefined;
 | 
			
		||||
 | 
			
		||||
                                            return (
 | 
			
		||||
                                                <CommandItem
 | 
			
		||||
                                                    className="flex items-center"
 | 
			
		||||
                                                    key={option.value}
 | 
			
		||||
                                                    keywords={
 | 
			
		||||
                                                        option.regex
 | 
			
		||||
                                                            ? [option.regex]
 | 
			
		||||
                                                            : undefined
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                    // value={option.value}
 | 
			
		||||
                                                    onSelect={() =>
 | 
			
		||||
                                                        handleSelect(
 | 
			
		||||
                                                            option.value,
 | 
			
		||||
                                                            matches?.map(
 | 
			
		||||
                                                                (match) =>
 | 
			
		||||
                                                                    match.toString()
 | 
			
		||||
                                                            )
 | 
			
		||||
                                                        )
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                >
 | 
			
		||||
                                                    {multiple && (
 | 
			
		||||
                                                        <div
 | 
			
		||||
                                                            className={cn(
 | 
			
		||||
                                                                'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
 | 
			
		||||
                                                                isSelected
 | 
			
		||||
                                                                    ? 'bg-primary text-primary-foreground'
 | 
			
		||||
                                                                    : 'opacity-50 [&_svg]:invisible'
 | 
			
		||||
                                                            )}
 | 
			
		||||
                                                        >
 | 
			
		||||
                                                            <CheckIcon />
 | 
			
		||||
                                                        </div>
 | 
			
		||||
                                                    )}
 | 
			
		||||
                                                    <div className="flex items-center truncate">
 | 
			
		||||
                                                        <span>
 | 
			
		||||
                                                            {isRegexMatch
 | 
			
		||||
                                                                ? searchTerm
 | 
			
		||||
                                                                : option.label}
 | 
			
		||||
                                                            {!isRegexMatch &&
 | 
			
		||||
                                                            optionSuffix
 | 
			
		||||
                                                                ? optionSuffix(
 | 
			
		||||
                                                                      option
 | 
			
		||||
                                                                  )
 | 
			
		||||
                                                                : ''}
 | 
			
		||||
                                                        </span>
 | 
			
		||||
                                                        {option.description && (
 | 
			
		||||
                                                            <span className="ml-1 text-xs text-muted-foreground">
 | 
			
		||||
                                                                {
 | 
			
		||||
                                                                    option.description
 | 
			
		||||
                                                                }
 | 
			
		||||
                                                            </span>
 | 
			
		||||
                                                        )}
 | 
			
		||||
                                                    </div>
 | 
			
		||||
                                                    {((!multiple &&
 | 
			
		||||
                                                        option.value ===
 | 
			
		||||
                                                            value) ||
 | 
			
		||||
                                                        isRegexMatch) && (
 | 
			
		||||
                                                        <CheckIcon
 | 
			
		||||
                                                            className={cn(
 | 
			
		||||
                                                                'ml-auto',
 | 
			
		||||
                                                                option.value ===
 | 
			
		||||
                                                                    value
 | 
			
		||||
                                                                    ? 'opacity-100'
 | 
			
		||||
                                                                    : 'opacity-0'
 | 
			
		||||
                                                            )}
 | 
			
		||||
                                                        />
 | 
			
		||||
                                                    )}
 | 
			
		||||
                                                </CommandItem>
 | 
			
		||||
                                            );
 | 
			
		||||
                                        })}
 | 
			
		||||
                                    </CommandList>
 | 
			
		||||
                                </CommandGroup>
 | 
			
		||||
                                <CommandList className="max-h-fit w-full">
 | 
			
		||||
                                    {hasGroups
 | 
			
		||||
                                        ? Object.entries(groups).map(
 | 
			
		||||
                                              ([groupName, groupOptions]) => (
 | 
			
		||||
                                                  <CommandGroup
 | 
			
		||||
                                                      key={groupName}
 | 
			
		||||
                                                      heading={groupName}
 | 
			
		||||
                                                  >
 | 
			
		||||
                                                      {groupOptions.map(
 | 
			
		||||
                                                          renderOption
 | 
			
		||||
                                                      )}
 | 
			
		||||
                                                  </CommandGroup>
 | 
			
		||||
                                              )
 | 
			
		||||
                                          )
 | 
			
		||||
                                        : options.map(renderOption)}
 | 
			
		||||
                                </CommandList>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </ScrollArea>
 | 
			
		||||
                    </Command>
 | 
			
		||||
                    {footerButtons ? (
 | 
			
		||||
                        <div className="border-t">{footerButtons}</div>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                </PopoverContent>
 | 
			
		||||
            </Popover>
 | 
			
		||||
        );
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,7 @@ 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_WIDTH_ICON_EXTENDED = '4rem';
 | 
			
		||||
const SIDEBAR_KEYBOARD_SHORTCUT = 'b';
 | 
			
		||||
 | 
			
		||||
type SidebarContext = {
 | 
			
		||||
@@ -142,6 +143,8 @@ const SidebarProvider = React.forwardRef<
 | 
			
		||||
                            {
 | 
			
		||||
                                '--sidebar-width': SIDEBAR_WIDTH,
 | 
			
		||||
                                '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,
 | 
			
		||||
                                '--sidebar-width-icon-extended':
 | 
			
		||||
                                    SIDEBAR_WIDTH_ICON_EXTENDED,
 | 
			
		||||
                                ...style,
 | 
			
		||||
                            } as React.CSSProperties
 | 
			
		||||
                        }
 | 
			
		||||
@@ -166,7 +169,7 @@ const Sidebar = React.forwardRef<
 | 
			
		||||
    React.ComponentProps<'div'> & {
 | 
			
		||||
        side?: 'left' | 'right';
 | 
			
		||||
        variant?: 'sidebar' | 'floating' | 'inset';
 | 
			
		||||
        collapsible?: 'offcanvas' | 'icon' | 'none';
 | 
			
		||||
        collapsible?: 'offcanvas' | 'icon' | 'icon-extended' | 'none';
 | 
			
		||||
    }
 | 
			
		||||
>(
 | 
			
		||||
    (
 | 
			
		||||
@@ -245,8 +248,8 @@ const Sidebar = React.forwardRef<
 | 
			
		||||
                        '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]'
 | 
			
		||||
                            ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))] group-data-[collapsible=icon-extended]:w-[calc(var(--sidebar-width-icon-extended)_+_theme(spacing.4))]'
 | 
			
		||||
                            : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[collapsible=icon-extended]:w-[--sidebar-width-icon-extended]'
 | 
			
		||||
                    )}
 | 
			
		||||
                />
 | 
			
		||||
                <div
 | 
			
		||||
@@ -257,8 +260,8 @@ const Sidebar = React.forwardRef<
 | 
			
		||||
                            : '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',
 | 
			
		||||
                            ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)] group-data-[collapsible=icon-extended]:w-[calc(var(--sidebar-width-icon-extended)_+_theme(spacing.4)_+2px)]'
 | 
			
		||||
                            : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[collapsible=icon-extended]:w-[--sidebar-width-icon-extended] group-data-[side=left]:border-r group-data-[side=right]:border-l',
 | 
			
		||||
                        className
 | 
			
		||||
                    )}
 | 
			
		||||
                    {...props}
 | 
			
		||||
@@ -421,7 +424,7 @@ const SidebarContent = React.forwardRef<
 | 
			
		||||
            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',
 | 
			
		||||
                'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden group-data-[collapsible=icon-extended]:overflow-hidden',
 | 
			
		||||
                className
 | 
			
		||||
            )}
 | 
			
		||||
            {...props}
 | 
			
		||||
@@ -461,6 +464,7 @@ const SidebarGroupLabel = React.forwardRef<
 | 
			
		||||
            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',
 | 
			
		||||
                'group-data-[collapsible=icon-extended]:-mt-8 group-data-[collapsible=icon-extended]:opacity-0',
 | 
			
		||||
                className
 | 
			
		||||
            )}
 | 
			
		||||
            {...props}
 | 
			
		||||
@@ -483,7 +487,7 @@ const SidebarGroupAction = React.forwardRef<
 | 
			
		||||
                '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',
 | 
			
		||||
                'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden',
 | 
			
		||||
                className
 | 
			
		||||
            )}
 | 
			
		||||
            {...props}
 | 
			
		||||
@@ -532,7 +536,7 @@ const SidebarMenuItem = React.forwardRef<
 | 
			
		||||
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',
 | 
			
		||||
    '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-extended]:h-auto group-data-[collapsible=icon-extended]:flex-col group-data-[collapsible=icon-extended]:gap-1 group-data-[collapsible=icon-extended]:p-2 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate group-data-[collapsible=icon-extended]:[&>span]:w-full group-data-[collapsible=icon-extended]:[&>span]:text-center group-data-[collapsible=icon-extended]:[&>span]:text-[10px] group-data-[collapsible=icon-extended]:[&>span]:leading-tight [&>svg]:size-4 [&>svg]:shrink-0',
 | 
			
		||||
    {
 | 
			
		||||
        variants: {
 | 
			
		||||
            variant: {
 | 
			
		||||
@@ -636,7 +640,7 @@ const SidebarMenuAction = React.forwardRef<
 | 
			
		||||
                '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',
 | 
			
		||||
                'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]: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
 | 
			
		||||
@@ -753,7 +757,7 @@ const SidebarMenuSubButton = React.forwardRef<
 | 
			
		||||
                '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',
 | 
			
		||||
                'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden',
 | 
			
		||||
                className
 | 
			
		||||
            )}
 | 
			
		||||
            {...props}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ export function Toaster() {
 | 
			
		||||
                description,
 | 
			
		||||
                action,
 | 
			
		||||
                layout = 'row',
 | 
			
		||||
                hideCloseButton = false,
 | 
			
		||||
                ...props
 | 
			
		||||
            }) {
 | 
			
		||||
                return (
 | 
			
		||||
@@ -38,7 +39,7 @@ export function Toaster() {
 | 
			
		||||
                            ) : null}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {layout === 'row' ? action : null}
 | 
			
		||||
                        <ToastClose />
 | 
			
		||||
                        {!hideCloseButton ? <ToastClose /> : null}
 | 
			
		||||
                    </Toast>
 | 
			
		||||
                );
 | 
			
		||||
            })}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ type ToasterToast = ToastProps & {
 | 
			
		||||
    description?: React.ReactNode;
 | 
			
		||||
    action?: ToastActionElement;
 | 
			
		||||
    layout?: 'row' | 'column';
 | 
			
		||||
    hideCloseButton?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
 
 | 
			
		||||
@@ -13,15 +13,17 @@ const TooltipContent = React.forwardRef<
 | 
			
		||||
    React.ElementRef<typeof TooltipPrimitive.Content>,
 | 
			
		||||
    React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
 | 
			
		||||
>(({ className, sideOffset = 4, ...props }, ref) => (
 | 
			
		||||
    // <TooltipPrimitive.Portal>
 | 
			
		||||
    <TooltipPrimitive.Content
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        sideOffset={sideOffset}
 | 
			
		||||
        className={cn(
 | 
			
		||||
            'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
 | 
			
		||||
            'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]',
 | 
			
		||||
            className
 | 
			
		||||
        )}
 | 
			
		||||
        {...props}
 | 
			
		||||
    />
 | 
			
		||||
    // </TooltipPrimitive.Portal>
 | 
			
		||||
));
 | 
			
		||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										17
									
								
								src/components/tree-view/tree-item-skeleton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Skeleton } from '../skeleton/skeleton';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
export interface TreeItemSkeletonProps
 | 
			
		||||
    extends React.HTMLAttributes<HTMLDivElement> {}
 | 
			
		||||
 | 
			
		||||
export const TreeItemSkeleton: React.FC<TreeItemSkeletonProps> = ({
 | 
			
		||||
    className,
 | 
			
		||||
    style,
 | 
			
		||||
}) => {
 | 
			
		||||
    return (
 | 
			
		||||
        <div className={cn('px-2 py-1', className)} style={style}>
 | 
			
		||||
            <Skeleton className="h-3.5 w-full rounded-sm" />
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										461
									
								
								src/components/tree-view/tree-view.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,461 @@
 | 
			
		||||
import {
 | 
			
		||||
    ChevronRight,
 | 
			
		||||
    File,
 | 
			
		||||
    Folder,
 | 
			
		||||
    Loader2,
 | 
			
		||||
    type LucideIcon,
 | 
			
		||||
} from 'lucide-react';
 | 
			
		||||
import { motion, AnimatePresence } from 'framer-motion';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import type {
 | 
			
		||||
    TreeNode,
 | 
			
		||||
    FetchChildrenFunction,
 | 
			
		||||
    SelectableTreeProps,
 | 
			
		||||
} from './tree';
 | 
			
		||||
import type { ExpandedState } from './use-tree';
 | 
			
		||||
import { useTree } from './use-tree';
 | 
			
		||||
import type { Dispatch, ReactNode, SetStateAction } from 'react';
 | 
			
		||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import { TreeItemSkeleton } from './tree-item-skeleton';
 | 
			
		||||
import {
 | 
			
		||||
    Tooltip,
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
    TooltipTrigger,
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
 | 
			
		||||
interface TreeViewProps<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
> {
 | 
			
		||||
    data: TreeNode<Type, Context>[];
 | 
			
		||||
    fetchChildren?: FetchChildrenFunction<Type, Context>;
 | 
			
		||||
    onNodeClick?: (node: TreeNode<Type, Context>) => void;
 | 
			
		||||
    className?: string;
 | 
			
		||||
    defaultIcon?: LucideIcon;
 | 
			
		||||
    defaultFolderIcon?: LucideIcon;
 | 
			
		||||
    defaultIconProps?: React.ComponentProps<LucideIcon>;
 | 
			
		||||
    defaultFolderIconProps?: React.ComponentProps<LucideIcon>;
 | 
			
		||||
    selectable?: SelectableTreeProps<Type, Context>;
 | 
			
		||||
    expanded?: ExpandedState;
 | 
			
		||||
    setExpanded?: Dispatch<SetStateAction<ExpandedState>>;
 | 
			
		||||
    renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode;
 | 
			
		||||
    renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode;
 | 
			
		||||
    loadingNodeIds?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function TreeView<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
>({
 | 
			
		||||
    data,
 | 
			
		||||
    fetchChildren,
 | 
			
		||||
    onNodeClick,
 | 
			
		||||
    className,
 | 
			
		||||
    defaultIcon = File,
 | 
			
		||||
    defaultFolderIcon = Folder,
 | 
			
		||||
    defaultIconProps,
 | 
			
		||||
    defaultFolderIconProps,
 | 
			
		||||
    selectable,
 | 
			
		||||
    expanded: expandedProp,
 | 
			
		||||
    setExpanded: setExpandedProp,
 | 
			
		||||
    renderHoverComponent,
 | 
			
		||||
    renderActionsComponent,
 | 
			
		||||
    loadingNodeIds,
 | 
			
		||||
}: TreeViewProps<Type, Context>) {
 | 
			
		||||
    const { expanded, loading, loadedChildren, hasMoreChildren, toggleNode } =
 | 
			
		||||
        useTree({
 | 
			
		||||
            fetchChildren,
 | 
			
		||||
            expanded: expandedProp,
 | 
			
		||||
            setExpanded: setExpandedProp,
 | 
			
		||||
        });
 | 
			
		||||
    const [selectedIdInternal, setSelectedIdInternal] = React.useState<
 | 
			
		||||
        string | undefined
 | 
			
		||||
    >(selectable?.defaultSelectedId);
 | 
			
		||||
 | 
			
		||||
    const selectedId = useMemo(() => {
 | 
			
		||||
        return selectable?.selectedId ?? selectedIdInternal;
 | 
			
		||||
    }, [selectable?.selectedId, selectedIdInternal]);
 | 
			
		||||
 | 
			
		||||
    const setSelectedId = useCallback(
 | 
			
		||||
        (value: SetStateAction<string | undefined>) => {
 | 
			
		||||
            if (selectable?.setSelectedId) {
 | 
			
		||||
                selectable.setSelectedId(value);
 | 
			
		||||
            } else {
 | 
			
		||||
                setSelectedIdInternal(value);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [selectable, setSelectedIdInternal]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (selectable?.enabled && selectable.defaultSelectedId) {
 | 
			
		||||
            if (selectable.defaultSelectedId === selectedId) return;
 | 
			
		||||
            setSelectedId(selectable.defaultSelectedId);
 | 
			
		||||
            const { node, path } = findNodeById(
 | 
			
		||||
                data,
 | 
			
		||||
                selectable.defaultSelectedId
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (node) {
 | 
			
		||||
                selectable.onSelectedChange?.(node);
 | 
			
		||||
 | 
			
		||||
                // Expand all parent nodes
 | 
			
		||||
                for (const parent of path) {
 | 
			
		||||
                    if (expanded[parent.id]) continue;
 | 
			
		||||
                    toggleNode(
 | 
			
		||||
                        parent.id,
 | 
			
		||||
                        parent.type,
 | 
			
		||||
                        parent.context,
 | 
			
		||||
                        parent.children
 | 
			
		||||
                    );
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }, [selectable, toggleNode, selectedId, data, expanded, setSelectedId]);
 | 
			
		||||
 | 
			
		||||
    const handleNodeSelect = (node: TreeNode<Type, Context>) => {
 | 
			
		||||
        if (selectable?.enabled) {
 | 
			
		||||
            setSelectedId(node.id);
 | 
			
		||||
            selectable.onSelectedChange?.(node);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className={cn('w-full', className)}>
 | 
			
		||||
            {data.map((node, index) => (
 | 
			
		||||
                <TreeNode
 | 
			
		||||
                    key={node.id}
 | 
			
		||||
                    node={node}
 | 
			
		||||
                    level={0}
 | 
			
		||||
                    expanded={expanded}
 | 
			
		||||
                    loading={loading}
 | 
			
		||||
                    loadedChildren={loadedChildren}
 | 
			
		||||
                    hasMoreChildren={hasMoreChildren}
 | 
			
		||||
                    onToggle={toggleNode}
 | 
			
		||||
                    onNodeClick={onNodeClick}
 | 
			
		||||
                    defaultIcon={defaultIcon}
 | 
			
		||||
                    defaultFolderIcon={defaultFolderIcon}
 | 
			
		||||
                    defaultIconProps={defaultIconProps}
 | 
			
		||||
                    defaultFolderIconProps={defaultFolderIconProps}
 | 
			
		||||
                    selectable={selectable?.enabled}
 | 
			
		||||
                    selectedId={selectedId}
 | 
			
		||||
                    onSelect={handleNodeSelect}
 | 
			
		||||
                    className={index > 0 ? 'mt-0.5' : ''}
 | 
			
		||||
                    renderHoverComponent={renderHoverComponent}
 | 
			
		||||
                    renderActionsComponent={renderActionsComponent}
 | 
			
		||||
                    loadingNodeIds={loadingNodeIds}
 | 
			
		||||
                />
 | 
			
		||||
            ))}
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface TreeNodeProps<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
> {
 | 
			
		||||
    node: TreeNode<Type, Context>;
 | 
			
		||||
    level: number;
 | 
			
		||||
    expanded: Record<string, boolean>;
 | 
			
		||||
    loading: Record<string, boolean>;
 | 
			
		||||
    loadedChildren: Record<string, TreeNode<Type, Context>[]>;
 | 
			
		||||
    hasMoreChildren: Record<string, boolean>;
 | 
			
		||||
    onToggle: (
 | 
			
		||||
        nodeId: string,
 | 
			
		||||
        nodeType: Type,
 | 
			
		||||
        nodeContext: Context[Type],
 | 
			
		||||
        staticChildren?: TreeNode<Type, Context>[]
 | 
			
		||||
    ) => void;
 | 
			
		||||
    onNodeClick?: (node: TreeNode<Type, Context>) => void;
 | 
			
		||||
    defaultIcon: LucideIcon;
 | 
			
		||||
    defaultFolderIcon: LucideIcon;
 | 
			
		||||
    defaultIconProps?: React.ComponentProps<LucideIcon>;
 | 
			
		||||
    defaultFolderIconProps?: React.ComponentProps<LucideIcon>;
 | 
			
		||||
    selectable?: boolean;
 | 
			
		||||
    selectedId?: string;
 | 
			
		||||
    onSelect: (node: TreeNode<Type, Context>) => void;
 | 
			
		||||
    className?: string;
 | 
			
		||||
    renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode;
 | 
			
		||||
    renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode;
 | 
			
		||||
    loadingNodeIds?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TreeNode<Type extends string, Context extends Record<Type, unknown>>({
 | 
			
		||||
    node,
 | 
			
		||||
    level,
 | 
			
		||||
    expanded,
 | 
			
		||||
    loading,
 | 
			
		||||
    loadedChildren,
 | 
			
		||||
    hasMoreChildren,
 | 
			
		||||
    onToggle,
 | 
			
		||||
    onNodeClick,
 | 
			
		||||
    defaultIcon: DefaultIcon,
 | 
			
		||||
    defaultFolderIcon: DefaultFolderIcon,
 | 
			
		||||
    defaultIconProps,
 | 
			
		||||
    defaultFolderIconProps,
 | 
			
		||||
    selectable,
 | 
			
		||||
    selectedId,
 | 
			
		||||
    onSelect,
 | 
			
		||||
    className,
 | 
			
		||||
    renderHoverComponent,
 | 
			
		||||
    renderActionsComponent,
 | 
			
		||||
    loadingNodeIds,
 | 
			
		||||
}: TreeNodeProps<Type, Context>) {
 | 
			
		||||
    const [isHovered, setIsHovered] = useState(false);
 | 
			
		||||
    const isExpanded = expanded[node.id];
 | 
			
		||||
    const isLoading = loading[node.id];
 | 
			
		||||
    const children = loadedChildren[node.id] || node.children;
 | 
			
		||||
    const isSelected = selectedId === node.id;
 | 
			
		||||
 | 
			
		||||
    const IconComponent =
 | 
			
		||||
        node.icon || (node.isFolder ? DefaultFolderIcon : DefaultIcon);
 | 
			
		||||
    const iconProps: React.ComponentProps<LucideIcon> = {
 | 
			
		||||
        strokeWidth: isSelected ? 2.5 : 2,
 | 
			
		||||
        ...(node.isFolder ? defaultFolderIconProps : defaultIconProps),
 | 
			
		||||
        ...node.iconProps,
 | 
			
		||||
        className: cn(
 | 
			
		||||
            'h-3.5 w-3.5 text-muted-foreground flex-none',
 | 
			
		||||
            isSelected && 'text-primary text-white',
 | 
			
		||||
            node.iconProps?.className
 | 
			
		||||
        ),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className={cn(className)}>
 | 
			
		||||
            <div
 | 
			
		||||
                className={cn(
 | 
			
		||||
                    'flex items-center gap-1.5 px-2 py-1 rounded-lg cursor-pointer group h-6',
 | 
			
		||||
                    'transition-colors duration-200',
 | 
			
		||||
                    isSelected
 | 
			
		||||
                        ? 'bg-sky-500 border border-sky-600 border dark:bg-sky-600 dark:border-sky-700'
 | 
			
		||||
                        : 'hover:bg-gray-200/50 border border-transparent dark:hover:bg-gray-700/50',
 | 
			
		||||
                    node.className
 | 
			
		||||
                )}
 | 
			
		||||
                {...(isSelected ? { 'data-selected': true } : {})}
 | 
			
		||||
                style={{ paddingLeft: `${level * 16 + 8}px` }}
 | 
			
		||||
                onMouseEnter={() => setIsHovered(true)}
 | 
			
		||||
                onMouseLeave={() => setIsHovered(false)}
 | 
			
		||||
                onClick={(e) => {
 | 
			
		||||
                    e.stopPropagation();
 | 
			
		||||
                    if (selectable && !node.unselectable) {
 | 
			
		||||
                        onSelect(node);
 | 
			
		||||
                    }
 | 
			
		||||
                    // if (node.isFolder) {
 | 
			
		||||
                    //     onToggle(node.id, node.children);
 | 
			
		||||
                    // }
 | 
			
		||||
 | 
			
		||||
                    // called only once in case of double click
 | 
			
		||||
                    if (e.detail !== 2) {
 | 
			
		||||
                        onNodeClick?.(node);
 | 
			
		||||
                    }
 | 
			
		||||
                }}
 | 
			
		||||
                onDoubleClick={(e) => {
 | 
			
		||||
                    e.stopPropagation();
 | 
			
		||||
                    if (node.isFolder) {
 | 
			
		||||
                        onToggle(
 | 
			
		||||
                            node.id,
 | 
			
		||||
                            node.type,
 | 
			
		||||
                            node.context,
 | 
			
		||||
                            node.children
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                }}
 | 
			
		||||
            >
 | 
			
		||||
                <div className="flex flex-none items-center gap-1.5">
 | 
			
		||||
                    <Button
 | 
			
		||||
                        variant="ghost"
 | 
			
		||||
                        size="icon"
 | 
			
		||||
                        className={cn(
 | 
			
		||||
                            'h-3.5 w-3.5 p-0 hover:bg-transparent flex-none',
 | 
			
		||||
                            isExpanded && 'rotate-90',
 | 
			
		||||
                            'transition-transform duration-200'
 | 
			
		||||
                        )}
 | 
			
		||||
                        onClick={(e) => {
 | 
			
		||||
                            e.stopPropagation();
 | 
			
		||||
                            if (node.isFolder) {
 | 
			
		||||
                                onToggle(
 | 
			
		||||
                                    node.id,
 | 
			
		||||
                                    node.type,
 | 
			
		||||
                                    node.context,
 | 
			
		||||
                                    node.children
 | 
			
		||||
                                );
 | 
			
		||||
                            }
 | 
			
		||||
                        }}
 | 
			
		||||
                    >
 | 
			
		||||
                        {node.isFolder &&
 | 
			
		||||
                            (isLoading ? (
 | 
			
		||||
                                <Loader2
 | 
			
		||||
                                    className={cn('size-3.5 animate-spin', {
 | 
			
		||||
                                        'text-white': isSelected,
 | 
			
		||||
                                    })}
 | 
			
		||||
                                />
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                <ChevronRight
 | 
			
		||||
                                    className={cn('size-3.5', {
 | 
			
		||||
                                        'text-white': isSelected,
 | 
			
		||||
                                    })}
 | 
			
		||||
                                    strokeWidth={2}
 | 
			
		||||
                                />
 | 
			
		||||
                            ))}
 | 
			
		||||
                    </Button>
 | 
			
		||||
 | 
			
		||||
                    {node.tooltip ? (
 | 
			
		||||
                        <Tooltip>
 | 
			
		||||
                            <TooltipTrigger asChild>
 | 
			
		||||
                                {loadingNodeIds?.includes(node.id) ? (
 | 
			
		||||
                                    <Loader2
 | 
			
		||||
                                        className={cn('size-3.5 animate-spin', {
 | 
			
		||||
                                            'text-white': isSelected,
 | 
			
		||||
                                        })}
 | 
			
		||||
                                    />
 | 
			
		||||
                                ) : (
 | 
			
		||||
                                    <IconComponent
 | 
			
		||||
                                        {...(isSelected
 | 
			
		||||
                                            ? { 'data-selected': true }
 | 
			
		||||
                                            : {})}
 | 
			
		||||
                                        {...iconProps}
 | 
			
		||||
                                    />
 | 
			
		||||
                                )}
 | 
			
		||||
                            </TooltipTrigger>
 | 
			
		||||
                            <TooltipContent
 | 
			
		||||
                                align="center"
 | 
			
		||||
                                className="max-w-[400px]"
 | 
			
		||||
                            >
 | 
			
		||||
                                {node.tooltip}
 | 
			
		||||
                            </TooltipContent>
 | 
			
		||||
                        </Tooltip>
 | 
			
		||||
                    ) : node.empty ? null : loadingNodeIds?.includes(
 | 
			
		||||
                          node.id
 | 
			
		||||
                      ) ? (
 | 
			
		||||
                        <Loader2
 | 
			
		||||
                            className={cn('size-3.5 animate-spin', {
 | 
			
		||||
                                // 'text-white': isSelected,
 | 
			
		||||
                            })}
 | 
			
		||||
                        />
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        <IconComponent
 | 
			
		||||
                            {...(isSelected ? { 'data-selected': true } : {})}
 | 
			
		||||
                            {...iconProps}
 | 
			
		||||
                        />
 | 
			
		||||
                    )}
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <span
 | 
			
		||||
                    {...node.labelProps}
 | 
			
		||||
                    className={cn(
 | 
			
		||||
                        'text-xs truncate min-w-0 flex-1 w-0',
 | 
			
		||||
                        isSelected && 'font-medium text-primary text-white',
 | 
			
		||||
                        node.labelProps?.className
 | 
			
		||||
                    )}
 | 
			
		||||
                    {...(isSelected ? { 'data-selected': true } : {})}
 | 
			
		||||
                >
 | 
			
		||||
                    {node.empty ? '' : node.name}
 | 
			
		||||
                </span>
 | 
			
		||||
                {renderActionsComponent && renderActionsComponent(node)}
 | 
			
		||||
                {isHovered && renderHoverComponent
 | 
			
		||||
                    ? renderHoverComponent(node)
 | 
			
		||||
                    : null}
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <AnimatePresence initial={false}>
 | 
			
		||||
                {isExpanded && children && (
 | 
			
		||||
                    <motion.div
 | 
			
		||||
                        initial={{ height: 0, opacity: 0 }}
 | 
			
		||||
                        animate={{
 | 
			
		||||
                            height: 'auto',
 | 
			
		||||
                            opacity: 1,
 | 
			
		||||
                            transition: {
 | 
			
		||||
                                height: {
 | 
			
		||||
                                    duration: Math.min(
 | 
			
		||||
                                        0.3 + children.length * 0.018,
 | 
			
		||||
                                        0.7
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    ease: 'easeInOut',
 | 
			
		||||
                                },
 | 
			
		||||
                                opacity: {
 | 
			
		||||
                                    duration: Math.min(
 | 
			
		||||
                                        0.2 + children.length * 0.012,
 | 
			
		||||
                                        0.4
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    ease: 'easeInOut',
 | 
			
		||||
                                },
 | 
			
		||||
                            },
 | 
			
		||||
                        }}
 | 
			
		||||
                        exit={{
 | 
			
		||||
                            height: 0,
 | 
			
		||||
                            opacity: 0,
 | 
			
		||||
                            transition: {
 | 
			
		||||
                                height: {
 | 
			
		||||
                                    duration: Math.min(
 | 
			
		||||
                                        0.2 + children.length * 0.01,
 | 
			
		||||
                                        0.45
 | 
			
		||||
                                    ),
 | 
			
		||||
                                    ease: 'easeInOut',
 | 
			
		||||
                                },
 | 
			
		||||
                                opacity: {
 | 
			
		||||
                                    duration: 0.1,
 | 
			
		||||
                                    ease: 'easeOut',
 | 
			
		||||
                                },
 | 
			
		||||
                            },
 | 
			
		||||
                        }}
 | 
			
		||||
                        style={{ overflow: 'hidden' }}
 | 
			
		||||
                    >
 | 
			
		||||
                        {children.map((child) => (
 | 
			
		||||
                            <TreeNode
 | 
			
		||||
                                key={child.id}
 | 
			
		||||
                                node={child}
 | 
			
		||||
                                level={level + 1}
 | 
			
		||||
                                expanded={expanded}
 | 
			
		||||
                                loading={loading}
 | 
			
		||||
                                loadedChildren={loadedChildren}
 | 
			
		||||
                                hasMoreChildren={hasMoreChildren}
 | 
			
		||||
                                onToggle={onToggle}
 | 
			
		||||
                                onNodeClick={onNodeClick}
 | 
			
		||||
                                defaultIcon={DefaultIcon}
 | 
			
		||||
                                defaultFolderIcon={DefaultFolderIcon}
 | 
			
		||||
                                defaultIconProps={defaultIconProps}
 | 
			
		||||
                                defaultFolderIconProps={defaultFolderIconProps}
 | 
			
		||||
                                selectable={selectable}
 | 
			
		||||
                                selectedId={selectedId}
 | 
			
		||||
                                onSelect={onSelect}
 | 
			
		||||
                                className="mt-0.5"
 | 
			
		||||
                                renderHoverComponent={renderHoverComponent}
 | 
			
		||||
                                renderActionsComponent={renderActionsComponent}
 | 
			
		||||
                                loadingNodeIds={loadingNodeIds}
 | 
			
		||||
                            />
 | 
			
		||||
                        ))}
 | 
			
		||||
                        {isLoading ? (
 | 
			
		||||
                            <TreeItemSkeleton
 | 
			
		||||
                                style={{
 | 
			
		||||
                                    paddingLeft: `${level + 2 * 16 + 8}px`,
 | 
			
		||||
                                }}
 | 
			
		||||
                            />
 | 
			
		||||
                        ) : null}
 | 
			
		||||
                    </motion.div>
 | 
			
		||||
                )}
 | 
			
		||||
            </AnimatePresence>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function findNodeById<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
>(
 | 
			
		||||
    nodes: TreeNode<Type, Context>[],
 | 
			
		||||
    id: string,
 | 
			
		||||
    initialPath: TreeNode<Type, Context>[] = []
 | 
			
		||||
): { node: TreeNode<Type, Context> | null; path: TreeNode<Type, Context>[] } {
 | 
			
		||||
    const path: TreeNode<Type, Context>[] = [...initialPath];
 | 
			
		||||
    for (const node of nodes) {
 | 
			
		||||
        if (node.id === id) return { node, path };
 | 
			
		||||
        if (node.children) {
 | 
			
		||||
            const found = findNodeById(node.children, id, [...path, node]);
 | 
			
		||||
            if (found.node) {
 | 
			
		||||
                return found;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return { node: null, path };
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								src/components/tree-view/tree.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,41 @@
 | 
			
		||||
import type { LucideIcon } from 'lucide-react';
 | 
			
		||||
import type React from 'react';
 | 
			
		||||
 | 
			
		||||
export interface TreeNode<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
> {
 | 
			
		||||
    id: string;
 | 
			
		||||
    name: string;
 | 
			
		||||
    isFolder?: boolean;
 | 
			
		||||
    children?: TreeNode<Type, Context>[];
 | 
			
		||||
    icon?: LucideIcon;
 | 
			
		||||
    iconProps?: React.ComponentProps<LucideIcon>;
 | 
			
		||||
    labelProps?: React.ComponentProps<'span'>;
 | 
			
		||||
    type: Type;
 | 
			
		||||
    unselectable?: boolean;
 | 
			
		||||
    tooltip?: string;
 | 
			
		||||
    context: Context[Type];
 | 
			
		||||
    empty?: boolean;
 | 
			
		||||
    className?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type FetchChildrenFunction<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
> = (
 | 
			
		||||
    nodeId: string,
 | 
			
		||||
    nodeType: Type,
 | 
			
		||||
    nodeContext: Context[Type]
 | 
			
		||||
) => Promise<TreeNode<Type, Context>[]>;
 | 
			
		||||
 | 
			
		||||
export interface SelectableTreeProps<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
> {
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
    defaultSelectedId?: string;
 | 
			
		||||
    onSelectedChange?: (node: TreeNode<Type, Context>) => void;
 | 
			
		||||
    selectedId?: string;
 | 
			
		||||
    setSelectedId?: React.Dispatch<React.SetStateAction<string | undefined>>;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										153
									
								
								src/components/tree-view/use-tree.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,153 @@
 | 
			
		||||
import type { Dispatch, SetStateAction } from 'react';
 | 
			
		||||
import { useState, useCallback, useMemo } from 'react';
 | 
			
		||||
import type { TreeNode, FetchChildrenFunction } from './tree';
 | 
			
		||||
 | 
			
		||||
export interface ExpandedState {
 | 
			
		||||
    [key: string]: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface LoadingState {
 | 
			
		||||
    [key: string]: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface LoadedChildren<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
> {
 | 
			
		||||
    [key: string]: TreeNode<Type, Context>[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface HasMoreChildrenState {
 | 
			
		||||
    [key: string]: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function useTree<
 | 
			
		||||
    Type extends string,
 | 
			
		||||
    Context extends Record<Type, unknown>,
 | 
			
		||||
>({
 | 
			
		||||
    fetchChildren,
 | 
			
		||||
    expanded: expandedProp,
 | 
			
		||||
    setExpanded: setExpandedProp,
 | 
			
		||||
}: {
 | 
			
		||||
    fetchChildren?: FetchChildrenFunction<Type, Context>;
 | 
			
		||||
    expanded?: ExpandedState;
 | 
			
		||||
    setExpanded?: Dispatch<SetStateAction<ExpandedState>>;
 | 
			
		||||
}) {
 | 
			
		||||
    const [expandedInternal, setExpandedInternal] = useState<ExpandedState>({});
 | 
			
		||||
 | 
			
		||||
    const expanded = useMemo(
 | 
			
		||||
        () => expandedProp ?? expandedInternal,
 | 
			
		||||
        [expandedProp, expandedInternal]
 | 
			
		||||
    );
 | 
			
		||||
    const setExpanded = useCallback(
 | 
			
		||||
        (value: SetStateAction<ExpandedState>) => {
 | 
			
		||||
            if (setExpandedProp) {
 | 
			
		||||
                setExpandedProp(value);
 | 
			
		||||
            } else {
 | 
			
		||||
                setExpandedInternal(value);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [setExpandedProp, setExpandedInternal]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const [loading, setLoading] = useState<LoadingState>({});
 | 
			
		||||
    const [loadedChildren, setLoadedChildren] = useState<
 | 
			
		||||
        LoadedChildren<Type, Context>
 | 
			
		||||
    >({});
 | 
			
		||||
    const [hasMoreChildren, setHasMoreChildren] =
 | 
			
		||||
        useState<HasMoreChildrenState>({});
 | 
			
		||||
 | 
			
		||||
    const mergeChildren = useCallback(
 | 
			
		||||
        (
 | 
			
		||||
            staticChildren: TreeNode<Type, Context>[] = [],
 | 
			
		||||
            fetchedChildren: TreeNode<Type, Context>[] = []
 | 
			
		||||
        ) => {
 | 
			
		||||
            const fetchedChildrenIds = new Set(
 | 
			
		||||
                fetchedChildren.map((child) => child.id)
 | 
			
		||||
            );
 | 
			
		||||
            const uniqueStaticChildren = staticChildren.filter(
 | 
			
		||||
                (child) => !fetchedChildrenIds.has(child.id)
 | 
			
		||||
            );
 | 
			
		||||
            return [...uniqueStaticChildren, ...fetchedChildren];
 | 
			
		||||
        },
 | 
			
		||||
        []
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const toggleNode = useCallback(
 | 
			
		||||
        async (
 | 
			
		||||
            nodeId: string,
 | 
			
		||||
            nodeType: Type,
 | 
			
		||||
            nodeContext: Context[Type],
 | 
			
		||||
            staticChildren?: TreeNode<Type, Context>[]
 | 
			
		||||
        ) => {
 | 
			
		||||
            if (expanded[nodeId]) {
 | 
			
		||||
                // If we're collapsing, just update expanded state
 | 
			
		||||
                setExpanded((prev) => ({ ...prev, [nodeId]: false }));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Get any previously fetched children
 | 
			
		||||
            const previouslyFetchedChildren = loadedChildren[nodeId] || [];
 | 
			
		||||
 | 
			
		||||
            // If we have static children, merge them with any previously fetched children
 | 
			
		||||
            if (staticChildren?.length) {
 | 
			
		||||
                const mergedChildren = mergeChildren(
 | 
			
		||||
                    staticChildren,
 | 
			
		||||
                    previouslyFetchedChildren
 | 
			
		||||
                );
 | 
			
		||||
                setLoadedChildren((prev) => ({
 | 
			
		||||
                    ...prev,
 | 
			
		||||
                    [nodeId]: mergedChildren,
 | 
			
		||||
                }));
 | 
			
		||||
 | 
			
		||||
                // Only show "more loading" if we haven't fetched children before
 | 
			
		||||
                setHasMoreChildren((prev) => ({
 | 
			
		||||
                    ...prev,
 | 
			
		||||
                    [nodeId]: !previouslyFetchedChildren.length,
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Set expanded state immediately to show static/previously fetched children
 | 
			
		||||
            setExpanded((prev) => ({ ...prev, [nodeId]: true }));
 | 
			
		||||
 | 
			
		||||
            // If we haven't loaded dynamic children yet
 | 
			
		||||
            if (!previouslyFetchedChildren.length) {
 | 
			
		||||
                setLoading((prev) => ({ ...prev, [nodeId]: true }));
 | 
			
		||||
                try {
 | 
			
		||||
                    const fetchedChildren = await fetchChildren?.(
 | 
			
		||||
                        nodeId,
 | 
			
		||||
                        nodeType,
 | 
			
		||||
                        nodeContext
 | 
			
		||||
                    );
 | 
			
		||||
                    // Merge static and newly fetched children
 | 
			
		||||
                    const allChildren = mergeChildren(
 | 
			
		||||
                        staticChildren || [],
 | 
			
		||||
                        fetchedChildren
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    setLoadedChildren((prev) => ({
 | 
			
		||||
                        ...prev,
 | 
			
		||||
                        [nodeId]: allChildren,
 | 
			
		||||
                    }));
 | 
			
		||||
                    setHasMoreChildren((prev) => ({
 | 
			
		||||
                        ...prev,
 | 
			
		||||
                        [nodeId]: false,
 | 
			
		||||
                    }));
 | 
			
		||||
                } catch (error) {
 | 
			
		||||
                    console.error('Error loading children:', error);
 | 
			
		||||
                } finally {
 | 
			
		||||
                    setLoading((prev) => ({ ...prev, [nodeId]: false }));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [expanded, loadedChildren, fetchChildren, mergeChildren, setExpanded]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        expanded,
 | 
			
		||||
        loading,
 | 
			
		||||
        loadedChildren,
 | 
			
		||||
        hasMoreChildren,
 | 
			
		||||
        toggleNode,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
@@ -12,6 +12,18 @@ export interface CanvasContext {
 | 
			
		||||
    }) => void;
 | 
			
		||||
    setOverlapGraph: (graph: Graph<string>) => void;
 | 
			
		||||
    overlapGraph: Graph<string>;
 | 
			
		||||
    setShowFilter: React.Dispatch<React.SetStateAction<boolean>>;
 | 
			
		||||
    showFilter: boolean;
 | 
			
		||||
    editTableModeTable: {
 | 
			
		||||
        tableId: string;
 | 
			
		||||
        fieldId?: string;
 | 
			
		||||
    } | null;
 | 
			
		||||
    setEditTableModeTable: React.Dispatch<
 | 
			
		||||
        React.SetStateAction<{
 | 
			
		||||
            tableId: string;
 | 
			
		||||
            fieldId?: string;
 | 
			
		||||
        } | null>
 | 
			
		||||
    >;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const canvasContext = createContext<CanvasContext>({
 | 
			
		||||
@@ -19,4 +31,8 @@ export const canvasContext = createContext<CanvasContext>({
 | 
			
		||||
    fitView: emptyFn,
 | 
			
		||||
    setOverlapGraph: emptyFn,
 | 
			
		||||
    overlapGraph: createGraph(),
 | 
			
		||||
    setShowFilter: emptyFn,
 | 
			
		||||
    showFilter: false,
 | 
			
		||||
    editTableModeTable: null,
 | 
			
		||||
    setEditTableModeTable: emptyFn,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,25 +1,59 @@
 | 
			
		||||
import React, { type ReactNode, useCallback, useState } from 'react';
 | 
			
		||||
import React, {
 | 
			
		||||
    type ReactNode,
 | 
			
		||||
    useCallback,
 | 
			
		||||
    useState,
 | 
			
		||||
    useEffect,
 | 
			
		||||
    useRef,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import { canvasContext } from './canvas-context';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import {
 | 
			
		||||
    adjustTablePositions,
 | 
			
		||||
    shouldShowTablesBySchemaFilter,
 | 
			
		||||
} from '@/lib/domain/db-table';
 | 
			
		||||
import { adjustTablePositions } 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';
 | 
			
		||||
import { useDiagramFilter } from '../diagram-filter-context/use-diagram-filter';
 | 
			
		||||
import { filterTable } from '@/lib/domain/diagram-filter/filter';
 | 
			
		||||
import { defaultSchemas } from '@/lib/data/default-schemas';
 | 
			
		||||
 | 
			
		||||
interface CanvasProviderProps {
 | 
			
		||||
    children: ReactNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CanvasProvider = ({ children }: CanvasProviderProps) => {
 | 
			
		||||
    const { tables, relationships, updateTablesState, filteredSchemas } =
 | 
			
		||||
        useChartDB();
 | 
			
		||||
    const {
 | 
			
		||||
        tables,
 | 
			
		||||
        relationships,
 | 
			
		||||
        updateTablesState,
 | 
			
		||||
        databaseType,
 | 
			
		||||
        areas,
 | 
			
		||||
        diagramId,
 | 
			
		||||
    } = useChartDB();
 | 
			
		||||
    const { filter, loading: filterLoading } = useDiagramFilter();
 | 
			
		||||
    const { fitView } = useReactFlow();
 | 
			
		||||
    const [overlapGraph, setOverlapGraph] =
 | 
			
		||||
        useState<Graph<string>>(createGraph());
 | 
			
		||||
    const [editTableModeTable, setEditTableModeTable] = useState<{
 | 
			
		||||
        tableId: string;
 | 
			
		||||
        fieldId?: string;
 | 
			
		||||
    } | null>(null);
 | 
			
		||||
 | 
			
		||||
    const [showFilter, setShowFilter] = useState(false);
 | 
			
		||||
    const diagramIdActiveFilterRef = useRef<string>();
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (filterLoading) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (diagramIdActiveFilterRef.current === diagramId) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        diagramIdActiveFilterRef.current = diagramId;
 | 
			
		||||
 | 
			
		||||
        setShowFilter(true);
 | 
			
		||||
    }, [filterLoading, diagramId]);
 | 
			
		||||
 | 
			
		||||
    const reorderTables = useCallback(
 | 
			
		||||
        (
 | 
			
		||||
@@ -30,9 +64,19 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
 | 
			
		||||
            const newTables = adjustTablePositions({
 | 
			
		||||
                relationships,
 | 
			
		||||
                tables: tables.filter((table) =>
 | 
			
		||||
                    shouldShowTablesBySchemaFilter(table, filteredSchemas)
 | 
			
		||||
                    filterTable({
 | 
			
		||||
                        table: {
 | 
			
		||||
                            id: table.id,
 | 
			
		||||
                            schema: table.schema,
 | 
			
		||||
                        },
 | 
			
		||||
                        filter,
 | 
			
		||||
                        options: {
 | 
			
		||||
                            defaultSchema: defaultSchemas[databaseType],
 | 
			
		||||
                        },
 | 
			
		||||
                    })
 | 
			
		||||
                ),
 | 
			
		||||
                mode: 'all', // Use 'all' mode for manual reordering
 | 
			
		||||
                areas,
 | 
			
		||||
                mode: 'all',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const updatedOverlapGraph = findOverlappingTables({
 | 
			
		||||
@@ -67,7 +111,15 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
 | 
			
		||||
                });
 | 
			
		||||
            }, 500);
 | 
			
		||||
        },
 | 
			
		||||
        [filteredSchemas, relationships, tables, updateTablesState, fitView]
 | 
			
		||||
        [
 | 
			
		||||
            filter,
 | 
			
		||||
            relationships,
 | 
			
		||||
            tables,
 | 
			
		||||
            updateTablesState,
 | 
			
		||||
            fitView,
 | 
			
		||||
            databaseType,
 | 
			
		||||
            areas,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
@@ -77,6 +129,10 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
 | 
			
		||||
                fitView,
 | 
			
		||||
                setOverlapGraph,
 | 
			
		||||
                overlapGraph,
 | 
			
		||||
                setShowFilter,
 | 
			
		||||
                showFilter,
 | 
			
		||||
                editTableModeTable,
 | 
			
		||||
                setEditTableModeTable,
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,8 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
			
		||||
import type { DBSchema } from '@/lib/domain/db-schema';
 | 
			
		||||
import type { DBDependency } from '@/lib/domain/db-dependency';
 | 
			
		||||
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
 | 
			
		||||
 | 
			
		||||
export type ChartDBEventType =
 | 
			
		||||
    | 'add_tables'
 | 
			
		||||
@@ -70,12 +72,14 @@ export interface ChartDBContext {
 | 
			
		||||
    schemas: DBSchema[];
 | 
			
		||||
    relationships: DBRelationship[];
 | 
			
		||||
    dependencies: DBDependency[];
 | 
			
		||||
    areas: Area[];
 | 
			
		||||
    customTypes: DBCustomType[];
 | 
			
		||||
    currentDiagram: Diagram;
 | 
			
		||||
    events: EventEmitter<ChartDBEvent>;
 | 
			
		||||
    readonly?: boolean;
 | 
			
		||||
 | 
			
		||||
    filteredSchemas?: string[];
 | 
			
		||||
    filterSchemas: (schemaIds: string[]) => void;
 | 
			
		||||
    highlightedCustomType?: DBCustomType;
 | 
			
		||||
    highlightCustomTypeId: (id?: string) => void;
 | 
			
		||||
 | 
			
		||||
    // General operations
 | 
			
		||||
    updateDiagramId: (id: string) => Promise<void>;
 | 
			
		||||
@@ -88,6 +92,10 @@ export interface ChartDBContext {
 | 
			
		||||
    updateDiagramUpdatedAt: () => Promise<void>;
 | 
			
		||||
    clearDiagramData: () => Promise<void>;
 | 
			
		||||
    deleteDiagram: () => Promise<void>;
 | 
			
		||||
    updateDiagramData: (
 | 
			
		||||
        diagram: Diagram,
 | 
			
		||||
        options?: { forceUpdateStorage?: boolean }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
 | 
			
		||||
    // Database type operations
 | 
			
		||||
    updateDatabaseType: (databaseType: DatabaseType) => Promise<void>;
 | 
			
		||||
@@ -221,6 +229,58 @@ export interface ChartDBContext {
 | 
			
		||||
        dependency: Partial<DBDependency>,
 | 
			
		||||
        options?: { updateHistory: boolean }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
 | 
			
		||||
    // Area operations
 | 
			
		||||
    createArea: (attributes?: Partial<Omit<Area, 'id'>>) => Promise<Area>;
 | 
			
		||||
    addArea: (
 | 
			
		||||
        area: Area,
 | 
			
		||||
        options?: { updateHistory: boolean }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
    addAreas: (
 | 
			
		||||
        areas: Area[],
 | 
			
		||||
        options?: { updateHistory: boolean }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
    getArea: (id: string) => Area | null;
 | 
			
		||||
    removeArea: (
 | 
			
		||||
        id: string,
 | 
			
		||||
        options?: { updateHistory: boolean }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
    removeAreas: (
 | 
			
		||||
        ids: string[],
 | 
			
		||||
        options?: { updateHistory: boolean }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
    updateArea: (
 | 
			
		||||
        id: string,
 | 
			
		||||
        area: Partial<Area>,
 | 
			
		||||
        options?: { updateHistory: boolean }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
 | 
			
		||||
    // Custom type operations
 | 
			
		||||
    createCustomType: (
 | 
			
		||||
        attributes?: Partial<Omit<DBCustomType, 'id'>>
 | 
			
		||||
    ) => Promise<DBCustomType>;
 | 
			
		||||
    addCustomType: (
 | 
			
		||||
        customType: DBCustomType,
 | 
			
		||||
        options?: { updateHistory: boolean }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
    addCustomTypes: (
 | 
			
		||||
        customTypes: DBCustomType[],
 | 
			
		||||
        options?: { updateHistory: boolean }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
    getCustomType: (id: string) => DBCustomType | null;
 | 
			
		||||
    removeCustomType: (
 | 
			
		||||
        id: string,
 | 
			
		||||
        options?: { updateHistory: boolean }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
    removeCustomTypes: (
 | 
			
		||||
        ids: string[],
 | 
			
		||||
        options?: { updateHistory: boolean }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
    updateCustomType: (
 | 
			
		||||
        id: string,
 | 
			
		||||
        customType: Partial<DBCustomType>,
 | 
			
		||||
        options?: { updateHistory: boolean }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const chartDBContext = createContext<ChartDBContext>({
 | 
			
		||||
@@ -230,9 +290,10 @@ export const chartDBContext = createContext<ChartDBContext>({
 | 
			
		||||
    tables: [],
 | 
			
		||||
    relationships: [],
 | 
			
		||||
    dependencies: [],
 | 
			
		||||
    areas: [],
 | 
			
		||||
    customTypes: [],
 | 
			
		||||
    schemas: [],
 | 
			
		||||
    filteredSchemas: [],
 | 
			
		||||
    filterSchemas: emptyFn,
 | 
			
		||||
    highlightCustomTypeId: emptyFn,
 | 
			
		||||
    currentDiagram: {
 | 
			
		||||
        id: '',
 | 
			
		||||
        name: '',
 | 
			
		||||
@@ -250,6 +311,7 @@ export const chartDBContext = createContext<ChartDBContext>({
 | 
			
		||||
    loadDiagramFromData: emptyFn,
 | 
			
		||||
    clearDiagramData: emptyFn,
 | 
			
		||||
    deleteDiagram: emptyFn,
 | 
			
		||||
    updateDiagramData: emptyFn,
 | 
			
		||||
 | 
			
		||||
    // Database type operations
 | 
			
		||||
    updateDatabaseType: emptyFn,
 | 
			
		||||
@@ -296,4 +358,22 @@ export const chartDBContext = createContext<ChartDBContext>({
 | 
			
		||||
    removeDependencies: emptyFn,
 | 
			
		||||
    addDependencies: emptyFn,
 | 
			
		||||
    updateDependency: emptyFn,
 | 
			
		||||
 | 
			
		||||
    // Area operations
 | 
			
		||||
    createArea: emptyFn,
 | 
			
		||||
    addArea: emptyFn,
 | 
			
		||||
    addAreas: emptyFn,
 | 
			
		||||
    getArea: emptyFn,
 | 
			
		||||
    removeArea: emptyFn,
 | 
			
		||||
    removeAreas: emptyFn,
 | 
			
		||||
    updateArea: emptyFn,
 | 
			
		||||
 | 
			
		||||
    // Custom type operations
 | 
			
		||||
    createCustomType: emptyFn,
 | 
			
		||||
    addCustomType: emptyFn,
 | 
			
		||||
    addCustomTypes: emptyFn,
 | 
			
		||||
    getCustomType: emptyFn,
 | 
			
		||||
    removeCustomType: emptyFn,
 | 
			
		||||
    removeCustomTypes: emptyFn,
 | 
			
		||||
    updateCustomType: emptyFn,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,15 @@
 | 
			
		||||
import React, { useCallback, useMemo, useState } from 'react';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import { deepCopy, generateId } from '@/lib/utils';
 | 
			
		||||
import { randomColor } from '@/lib/colors';
 | 
			
		||||
import { defaultTableColor, defaultAreaColor, viewColor } from '@/lib/colors';
 | 
			
		||||
import type { ChartDBContext, ChartDBEvent } from './chartdb-context';
 | 
			
		||||
import { chartDBContext } from './chartdb-context';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DBIndex } from '@/lib/domain/db-index';
 | 
			
		||||
import {
 | 
			
		||||
    getTableIndexesWithPrimaryKey,
 | 
			
		||||
    type DBIndex,
 | 
			
		||||
} from '@/lib/domain/db-index';
 | 
			
		||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
 | 
			
		||||
import { useStorage } from '@/hooks/use-storage';
 | 
			
		||||
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
 | 
			
		||||
@@ -17,13 +20,17 @@ import {
 | 
			
		||||
    databasesWithSchemas,
 | 
			
		||||
    schemaNameToSchemaId,
 | 
			
		||||
} from '@/lib/domain/db-schema';
 | 
			
		||||
import { useLocalConfig } from '@/hooks/use-local-config';
 | 
			
		||||
import { defaultSchemas } from '@/lib/data/default-schemas';
 | 
			
		||||
import { useEventEmitter } from 'ahooks';
 | 
			
		||||
import type { DBDependency } from '@/lib/domain/db-dependency';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import { storageInitialValue } from '../storage-context/storage-context';
 | 
			
		||||
import { useDiff } from '../diff-context/use-diff';
 | 
			
		||||
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
 | 
			
		||||
import {
 | 
			
		||||
    DBCustomTypeKind,
 | 
			
		||||
    type DBCustomType,
 | 
			
		||||
} from '@/lib/domain/db-custom-type';
 | 
			
		||||
 | 
			
		||||
export interface ChartDBProviderProps {
 | 
			
		||||
    diagram?: Diagram;
 | 
			
		||||
@@ -34,11 +41,11 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
    React.PropsWithChildren<ChartDBProviderProps>
 | 
			
		||||
> = ({ children, diagram, readonly: readonlyProp }) => {
 | 
			
		||||
    const { hasDiff } = useDiff();
 | 
			
		||||
    let db = useStorage();
 | 
			
		||||
    const storageDB = useStorage();
 | 
			
		||||
    const events = useEventEmitter<ChartDBEvent>();
 | 
			
		||||
    const { setSchemasFilter, schemasFilter } = useLocalConfig();
 | 
			
		||||
    const { addUndoAction, resetRedoStack, resetUndoStack } =
 | 
			
		||||
        useRedoUndoStack();
 | 
			
		||||
 | 
			
		||||
    const [diagramId, setDiagramId] = useState('');
 | 
			
		||||
    const [diagramName, setDiagramName] = useState('');
 | 
			
		||||
    const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
 | 
			
		||||
@@ -56,8 +63,16 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
    const [dependencies, setDependencies] = useState<DBDependency[]>(
 | 
			
		||||
        diagram?.dependencies ?? []
 | 
			
		||||
    );
 | 
			
		||||
    const [areas, setAreas] = useState<Area[]>(diagram?.areas ?? []);
 | 
			
		||||
    const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
 | 
			
		||||
        diagram?.customTypes ?? []
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const { events: diffEvents } = useDiff();
 | 
			
		||||
 | 
			
		||||
    const [highlightedCustomTypeId, setHighlightedCustomTypeId] =
 | 
			
		||||
        useState<string>();
 | 
			
		||||
 | 
			
		||||
    const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
 | 
			
		||||
        const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
 | 
			
		||||
        setTables((tables) =>
 | 
			
		||||
@@ -76,17 +91,16 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
 | 
			
		||||
    diffEvents.useSubscription(diffCalculatedHandler);
 | 
			
		||||
 | 
			
		||||
    const defaultSchemaName = defaultSchemas[databaseType];
 | 
			
		||||
    const defaultSchemaName = useMemo(
 | 
			
		||||
        () => defaultSchemas[databaseType],
 | 
			
		||||
        [databaseType]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const readonly = useMemo(
 | 
			
		||||
        () => readonlyProp ?? hasDiff ?? false,
 | 
			
		||||
        [readonlyProp, hasDiff]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (readonly) {
 | 
			
		||||
        db = storageInitialValue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const schemas = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
            databasesWithSchemas.includes(databaseType)
 | 
			
		||||
@@ -97,9 +111,11 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                              .filter((schema) => !!schema) as string[]
 | 
			
		||||
                      ),
 | 
			
		||||
                  ]
 | 
			
		||||
                      .sort((a, b) =>
 | 
			
		||||
                          a === defaultSchemaName ? -1 : a.localeCompare(b)
 | 
			
		||||
                      )
 | 
			
		||||
                      .sort((a, b) => {
 | 
			
		||||
                          if (a === defaultSchemaName) return -1;
 | 
			
		||||
                          if (b === defaultSchemaName) return 1;
 | 
			
		||||
                          return a.localeCompare(b);
 | 
			
		||||
                      })
 | 
			
		||||
                      .map(
 | 
			
		||||
                          (schema): DBSchema => ({
 | 
			
		||||
                              id: schemaNameToSchemaId(schema),
 | 
			
		||||
@@ -113,34 +129,11 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
        [tables, defaultSchemaName, databaseType]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const filterSchemas: ChartDBContext['filterSchemas'] = useCallback(
 | 
			
		||||
        (schemaIds) => {
 | 
			
		||||
            setSchemasFilter((prev) => ({
 | 
			
		||||
                ...prev,
 | 
			
		||||
                [diagramId]: schemaIds,
 | 
			
		||||
            }));
 | 
			
		||||
        },
 | 
			
		||||
        [diagramId, setSchemasFilter]
 | 
			
		||||
    const db = useMemo(
 | 
			
		||||
        () => (readonly ? storageInitialValue : storageDB),
 | 
			
		||||
        [storageDB, readonly]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const filteredSchemas: ChartDBContext['filteredSchemas'] = useMemo(() => {
 | 
			
		||||
        if (schemas.length === 0) {
 | 
			
		||||
            return undefined;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const schemasFilterFromCache =
 | 
			
		||||
            (schemasFilter[diagramId] ?? []).length === 0
 | 
			
		||||
                ? undefined // in case of empty filter, skip cache
 | 
			
		||||
                : schemasFilter[diagramId];
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            schemasFilterFromCache ?? [
 | 
			
		||||
                schemas.find((s) => s.name === defaultSchemaName)?.id ??
 | 
			
		||||
                    schemas[0]?.id,
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
    }, [schemasFilter, diagramId, schemas, defaultSchemaName]);
 | 
			
		||||
 | 
			
		||||
    const currentDiagram: Diagram = useMemo(
 | 
			
		||||
        () => ({
 | 
			
		||||
            id: diagramId,
 | 
			
		||||
@@ -152,6 +145,8 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            tables,
 | 
			
		||||
            relationships,
 | 
			
		||||
            dependencies,
 | 
			
		||||
            areas,
 | 
			
		||||
            customTypes,
 | 
			
		||||
        }),
 | 
			
		||||
        [
 | 
			
		||||
            diagramId,
 | 
			
		||||
@@ -161,6 +156,8 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            tables,
 | 
			
		||||
            relationships,
 | 
			
		||||
            dependencies,
 | 
			
		||||
            areas,
 | 
			
		||||
            customTypes,
 | 
			
		||||
            diagramCreatedAt,
 | 
			
		||||
            diagramUpdatedAt,
 | 
			
		||||
        ]
 | 
			
		||||
@@ -172,6 +169,8 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            setTables([]);
 | 
			
		||||
            setRelationships([]);
 | 
			
		||||
            setDependencies([]);
 | 
			
		||||
            setAreas([]);
 | 
			
		||||
            setCustomTypes([]);
 | 
			
		||||
            setDiagramUpdatedAt(updatedAt);
 | 
			
		||||
 | 
			
		||||
            resetRedoStack();
 | 
			
		||||
@@ -182,6 +181,8 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                db.deleteDiagramTables(diagramId),
 | 
			
		||||
                db.deleteDiagramRelationships(diagramId),
 | 
			
		||||
                db.deleteDiagramDependencies(diagramId),
 | 
			
		||||
                db.deleteDiagramAreas(diagramId),
 | 
			
		||||
                db.deleteDiagramCustomTypes(diagramId),
 | 
			
		||||
            ]);
 | 
			
		||||
        }, [db, diagramId, resetRedoStack, resetUndoStack]);
 | 
			
		||||
 | 
			
		||||
@@ -194,6 +195,8 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            setTables([]);
 | 
			
		||||
            setRelationships([]);
 | 
			
		||||
            setDependencies([]);
 | 
			
		||||
            setAreas([]);
 | 
			
		||||
            setCustomTypes([]);
 | 
			
		||||
            resetRedoStack();
 | 
			
		||||
            resetUndoStack();
 | 
			
		||||
 | 
			
		||||
@@ -202,6 +205,8 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                db.deleteDiagramRelationships(diagramId),
 | 
			
		||||
                db.deleteDiagram(diagramId),
 | 
			
		||||
                db.deleteDiagramDependencies(diagramId),
 | 
			
		||||
                db.deleteDiagramAreas(diagramId),
 | 
			
		||||
                db.deleteDiagramCustomTypes(diagramId),
 | 
			
		||||
            ]);
 | 
			
		||||
        }, [db, diagramId, resetRedoStack, resetUndoStack]);
 | 
			
		||||
 | 
			
		||||
@@ -283,22 +288,27 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const addTables: ChartDBContext['addTables'] = useCallback(
 | 
			
		||||
        async (tables: DBTable[], options = { updateHistory: true }) => {
 | 
			
		||||
            setTables((currentTables) => [...currentTables, ...tables]);
 | 
			
		||||
        async (tablesToAdd: DBTable[], options = { updateHistory: true }) => {
 | 
			
		||||
            setTables((currentTables) => [...currentTables, ...tablesToAdd]);
 | 
			
		||||
            const updatedAt = new Date();
 | 
			
		||||
            setDiagramUpdatedAt(updatedAt);
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
 | 
			
		||||
                ...tables.map((table) => db.addTable({ diagramId, table })),
 | 
			
		||||
                ...tablesToAdd.map((table) =>
 | 
			
		||||
                    db.addTable({ diagramId, table })
 | 
			
		||||
                ),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            events.emit({ action: 'add_tables', data: { tables } });
 | 
			
		||||
            events.emit({
 | 
			
		||||
                action: 'add_tables',
 | 
			
		||||
                data: { tables: tablesToAdd },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (options.updateHistory) {
 | 
			
		||||
                addUndoAction({
 | 
			
		||||
                    action: 'addTables',
 | 
			
		||||
                    redoData: { tables },
 | 
			
		||||
                    undoData: { tableIds: tables.map((t) => t.id) },
 | 
			
		||||
                    redoData: { tables: tablesToAdd },
 | 
			
		||||
                    undoData: { tableIds: tablesToAdd.map((t) => t.id) },
 | 
			
		||||
                });
 | 
			
		||||
                resetRedoStack();
 | 
			
		||||
            }
 | 
			
		||||
@@ -335,12 +345,17 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
                indexes: [],
 | 
			
		||||
                color: randomColor(),
 | 
			
		||||
                color: attributes?.isView ? viewColor : defaultTableColor,
 | 
			
		||||
                createdAt: Date.now(),
 | 
			
		||||
                isView: false,
 | 
			
		||||
                order: tables.length,
 | 
			
		||||
                ...attributes,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            table.indexes = getTableIndexesWithPrimaryKey({
 | 
			
		||||
                table,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            await addTable(table);
 | 
			
		||||
 | 
			
		||||
            return table;
 | 
			
		||||
@@ -632,17 +647,30 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            options = { updateHistory: true }
 | 
			
		||||
        ) => {
 | 
			
		||||
            const prevField = getField(tableId, fieldId);
 | 
			
		||||
 | 
			
		||||
            const updateTableFn = (table: DBTable) => {
 | 
			
		||||
                const updatedTable: DBTable = {
 | 
			
		||||
                    ...table,
 | 
			
		||||
                    fields: table.fields.map((f) =>
 | 
			
		||||
                        f.id === fieldId ? { ...f, ...field } : f
 | 
			
		||||
                    ),
 | 
			
		||||
                } satisfies DBTable;
 | 
			
		||||
 | 
			
		||||
                updatedTable.indexes = getTableIndexesWithPrimaryKey({
 | 
			
		||||
                    table: updatedTable,
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return updatedTable;
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            setTables((tables) =>
 | 
			
		||||
                tables.map((table) =>
 | 
			
		||||
                    table.id === tableId
 | 
			
		||||
                        ? {
 | 
			
		||||
                              ...table,
 | 
			
		||||
                              fields: table.fields.map((f) =>
 | 
			
		||||
                                  f.id === fieldId ? { ...f, ...field } : f
 | 
			
		||||
                              ),
 | 
			
		||||
                          }
 | 
			
		||||
                        : table
 | 
			
		||||
                )
 | 
			
		||||
                tables.map((table) => {
 | 
			
		||||
                    if (table.id === tableId) {
 | 
			
		||||
                        return updateTableFn(table);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return table;
 | 
			
		||||
                })
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            const table = await db.getTable({ diagramId, id: tableId });
 | 
			
		||||
@@ -657,10 +685,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                db.updateTable({
 | 
			
		||||
                    id: tableId,
 | 
			
		||||
                    attributes: {
 | 
			
		||||
                        ...table,
 | 
			
		||||
                        fields: table.fields.map((f) =>
 | 
			
		||||
                            f.id === fieldId ? { ...f, ...field } : f
 | 
			
		||||
                        ),
 | 
			
		||||
                        ...updateTableFn(table),
 | 
			
		||||
                    },
 | 
			
		||||
                }),
 | 
			
		||||
            ]);
 | 
			
		||||
@@ -687,19 +712,29 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            fieldId: string,
 | 
			
		||||
            options = { updateHistory: true }
 | 
			
		||||
        ) => {
 | 
			
		||||
            const updateTableFn = (table: DBTable) => {
 | 
			
		||||
                const updatedTable: DBTable = {
 | 
			
		||||
                    ...table,
 | 
			
		||||
                    fields: table.fields.filter((f) => f.id !== fieldId),
 | 
			
		||||
                } satisfies DBTable;
 | 
			
		||||
 | 
			
		||||
                updatedTable.indexes = getTableIndexesWithPrimaryKey({
 | 
			
		||||
                    table: updatedTable,
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return updatedTable;
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            const fields = getTable(tableId)?.fields ?? [];
 | 
			
		||||
            const prevField = getField(tableId, fieldId);
 | 
			
		||||
            setTables((tables) =>
 | 
			
		||||
                tables.map((table) =>
 | 
			
		||||
                    table.id === tableId
 | 
			
		||||
                        ? {
 | 
			
		||||
                              ...table,
 | 
			
		||||
                              fields: table.fields.filter(
 | 
			
		||||
                                  (f) => f.id !== fieldId
 | 
			
		||||
                              ),
 | 
			
		||||
                          }
 | 
			
		||||
                        : table
 | 
			
		||||
                )
 | 
			
		||||
                tables.map((table) => {
 | 
			
		||||
                    if (table.id === tableId) {
 | 
			
		||||
                        return updateTableFn(table);
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return table;
 | 
			
		||||
                })
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            events.emit({
 | 
			
		||||
@@ -723,8 +758,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                db.updateTable({
 | 
			
		||||
                    id: tableId,
 | 
			
		||||
                    attributes: {
 | 
			
		||||
                        ...table,
 | 
			
		||||
                        fields: table.fields.filter((f) => f.id !== fieldId),
 | 
			
		||||
                        ...updateTableFn(table),
 | 
			
		||||
                    },
 | 
			
		||||
                }),
 | 
			
		||||
            ]);
 | 
			
		||||
@@ -757,13 +791,23 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            options = { updateHistory: true }
 | 
			
		||||
        ) => {
 | 
			
		||||
            const fields = getTable(tableId)?.fields ?? [];
 | 
			
		||||
            setTables((tables) =>
 | 
			
		||||
                tables.map((table) =>
 | 
			
		||||
                    table.id === tableId
 | 
			
		||||
                        ? { ...table, fields: [...table.fields, field] }
 | 
			
		||||
                        : table
 | 
			
		||||
                )
 | 
			
		||||
            );
 | 
			
		||||
            setTables((tables) => {
 | 
			
		||||
                return tables.map((table) => {
 | 
			
		||||
                    if (table.id === tableId) {
 | 
			
		||||
                        db.updateTable({
 | 
			
		||||
                            id: tableId,
 | 
			
		||||
                            attributes: {
 | 
			
		||||
                                ...table,
 | 
			
		||||
                                fields: [...table.fields, field],
 | 
			
		||||
                            },
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        return { ...table, fields: [...table.fields, field] };
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return table;
 | 
			
		||||
                });
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            events.emit({
 | 
			
		||||
                action: 'add_field',
 | 
			
		||||
@@ -784,13 +828,6 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            setDiagramUpdatedAt(updatedAt);
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
 | 
			
		||||
                db.updateTable({
 | 
			
		||||
                    id: tableId,
 | 
			
		||||
                    attributes: {
 | 
			
		||||
                        ...table,
 | 
			
		||||
                        fields: [...table.fields, field],
 | 
			
		||||
                    },
 | 
			
		||||
                }),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            if (options.updateHistory) {
 | 
			
		||||
@@ -1077,12 +1114,15 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
 | 
			
		||||
                const sourceFieldName = sourceField?.name ?? '';
 | 
			
		||||
 | 
			
		||||
                const targetTable = getTable(targetTableId);
 | 
			
		||||
                const targetTableSchema = targetTable?.schema;
 | 
			
		||||
 | 
			
		||||
                const relationship: DBRelationship = {
 | 
			
		||||
                    id: generateId(),
 | 
			
		||||
                    name: `${sourceTableName}_${sourceFieldName}_fk`,
 | 
			
		||||
                    sourceSchema: sourceTable?.schema,
 | 
			
		||||
                    sourceTableId,
 | 
			
		||||
                    targetSchema: sourceTable?.schema,
 | 
			
		||||
                    targetSchema: targetTableSchema,
 | 
			
		||||
                    targetTableId,
 | 
			
		||||
                    sourceFieldId,
 | 
			
		||||
                    targetFieldId,
 | 
			
		||||
@@ -1363,20 +1403,161 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Area operations
 | 
			
		||||
    const addAreas: ChartDBContext['addAreas'] = useCallback(
 | 
			
		||||
        async (areas: Area[], options = { updateHistory: true }) => {
 | 
			
		||||
            setAreas((currentAreas) => [...currentAreas, ...areas]);
 | 
			
		||||
 | 
			
		||||
            const updatedAt = new Date();
 | 
			
		||||
            setDiagramUpdatedAt(updatedAt);
 | 
			
		||||
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                ...areas.map((area) => db.addArea({ diagramId, area })),
 | 
			
		||||
                db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            if (options.updateHistory) {
 | 
			
		||||
                addUndoAction({
 | 
			
		||||
                    action: 'addAreas',
 | 
			
		||||
                    redoData: { areas },
 | 
			
		||||
                    undoData: { areaIds: areas.map((a) => a.id) },
 | 
			
		||||
                });
 | 
			
		||||
                resetRedoStack();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [db, diagramId, setAreas, addUndoAction, resetRedoStack]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const addArea: ChartDBContext['addArea'] = useCallback(
 | 
			
		||||
        async (area: Area, options = { updateHistory: true }) => {
 | 
			
		||||
            return addAreas([area], options);
 | 
			
		||||
        },
 | 
			
		||||
        [addAreas]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const createArea: ChartDBContext['createArea'] = useCallback(
 | 
			
		||||
        async (attributes) => {
 | 
			
		||||
            const area: Area = {
 | 
			
		||||
                id: generateId(),
 | 
			
		||||
                name: `Area ${areas.length + 1}`,
 | 
			
		||||
                x: 0,
 | 
			
		||||
                y: 0,
 | 
			
		||||
                width: 300,
 | 
			
		||||
                height: 200,
 | 
			
		||||
                color: defaultAreaColor,
 | 
			
		||||
                ...attributes,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            await addArea(area);
 | 
			
		||||
 | 
			
		||||
            return area;
 | 
			
		||||
        },
 | 
			
		||||
        [areas, addArea]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getArea: ChartDBContext['getArea'] = useCallback(
 | 
			
		||||
        (id: string) => areas.find((area) => area.id === id) ?? null,
 | 
			
		||||
        [areas]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const removeAreas: ChartDBContext['removeAreas'] = useCallback(
 | 
			
		||||
        async (ids: string[], options = { updateHistory: true }) => {
 | 
			
		||||
            const prevAreas = [
 | 
			
		||||
                ...areas.filter((area) => ids.includes(area.id)),
 | 
			
		||||
            ];
 | 
			
		||||
 | 
			
		||||
            setAreas((areas) => areas.filter((area) => !ids.includes(area.id)));
 | 
			
		||||
 | 
			
		||||
            const updatedAt = new Date();
 | 
			
		||||
            setDiagramUpdatedAt(updatedAt);
 | 
			
		||||
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                ...ids.map((id) => db.deleteArea({ diagramId, id })),
 | 
			
		||||
                db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            if (prevAreas.length > 0 && options.updateHistory) {
 | 
			
		||||
                addUndoAction({
 | 
			
		||||
                    action: 'removeAreas',
 | 
			
		||||
                    redoData: { areaIds: ids },
 | 
			
		||||
                    undoData: { areas: prevAreas },
 | 
			
		||||
                });
 | 
			
		||||
                resetRedoStack();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [db, diagramId, setAreas, areas, addUndoAction, resetRedoStack]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const removeArea: ChartDBContext['removeArea'] = useCallback(
 | 
			
		||||
        async (id: string, options = { updateHistory: true }) => {
 | 
			
		||||
            return removeAreas([id], options);
 | 
			
		||||
        },
 | 
			
		||||
        [removeAreas]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const updateArea: ChartDBContext['updateArea'] = useCallback(
 | 
			
		||||
        async (
 | 
			
		||||
            id: string,
 | 
			
		||||
            area: Partial<Area>,
 | 
			
		||||
            options = { updateHistory: true }
 | 
			
		||||
        ) => {
 | 
			
		||||
            const prevArea = getArea(id);
 | 
			
		||||
 | 
			
		||||
            setAreas((areas) =>
 | 
			
		||||
                areas.map((a) => (a.id === id ? { ...a, ...area } : a))
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            const updatedAt = new Date();
 | 
			
		||||
            setDiagramUpdatedAt(updatedAt);
 | 
			
		||||
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
 | 
			
		||||
                db.updateArea({ id, attributes: area }),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            if (!!prevArea && options.updateHistory) {
 | 
			
		||||
                addUndoAction({
 | 
			
		||||
                    action: 'updateArea',
 | 
			
		||||
                    redoData: { areaId: id, area },
 | 
			
		||||
                    undoData: { areaId: id, area: prevArea },
 | 
			
		||||
                });
 | 
			
		||||
                resetRedoStack();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const highlightCustomTypeId = useCallback(
 | 
			
		||||
        (id?: string) => setHighlightedCustomTypeId(id),
 | 
			
		||||
        [setHighlightedCustomTypeId]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const highlightedCustomType = useMemo(() => {
 | 
			
		||||
        return highlightedCustomTypeId
 | 
			
		||||
            ? customTypes.find((type) => type.id === highlightedCustomTypeId)
 | 
			
		||||
            : undefined;
 | 
			
		||||
    }, [highlightedCustomTypeId, customTypes]);
 | 
			
		||||
 | 
			
		||||
    const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            async (diagram) => {
 | 
			
		||||
            (diagram) => {
 | 
			
		||||
                setDiagramId(diagram.id);
 | 
			
		||||
                setDiagramName(diagram.name);
 | 
			
		||||
                setDatabaseType(diagram.databaseType);
 | 
			
		||||
                setDatabaseEdition(diagram.databaseEdition);
 | 
			
		||||
                setTables(diagram?.tables ?? []);
 | 
			
		||||
                setRelationships(diagram?.relationships ?? []);
 | 
			
		||||
                setDependencies(diagram?.dependencies ?? []);
 | 
			
		||||
                setTables(diagram.tables ?? []);
 | 
			
		||||
                setRelationships(diagram.relationships ?? []);
 | 
			
		||||
                setDependencies(diagram.dependencies ?? []);
 | 
			
		||||
                setAreas(diagram.areas ?? []);
 | 
			
		||||
                setCustomTypes(diagram.customTypes ?? []);
 | 
			
		||||
                setDiagramCreatedAt(diagram.createdAt);
 | 
			
		||||
                setDiagramUpdatedAt(diagram.updatedAt);
 | 
			
		||||
                setHighlightedCustomTypeId(undefined);
 | 
			
		||||
 | 
			
		||||
                events.emit({ action: 'load_diagram', data: { diagram } });
 | 
			
		||||
 | 
			
		||||
                resetRedoStack();
 | 
			
		||||
                resetUndoStack();
 | 
			
		||||
            },
 | 
			
		||||
            [
 | 
			
		||||
                setDiagramId,
 | 
			
		||||
@@ -1386,18 +1567,35 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                setTables,
 | 
			
		||||
                setRelationships,
 | 
			
		||||
                setDependencies,
 | 
			
		||||
                setAreas,
 | 
			
		||||
                setCustomTypes,
 | 
			
		||||
                setDiagramCreatedAt,
 | 
			
		||||
                setDiagramUpdatedAt,
 | 
			
		||||
                setHighlightedCustomTypeId,
 | 
			
		||||
                events,
 | 
			
		||||
                resetRedoStack,
 | 
			
		||||
                resetUndoStack,
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const updateDiagramData: ChartDBContext['updateDiagramData'] = useCallback(
 | 
			
		||||
        async (diagram, options) => {
 | 
			
		||||
            const st = options?.forceUpdateStorage ? storageDB : db;
 | 
			
		||||
            await st.deleteDiagram(diagram.id);
 | 
			
		||||
            await st.addDiagram({ diagram });
 | 
			
		||||
            loadDiagramFromData(diagram);
 | 
			
		||||
        },
 | 
			
		||||
        [db, storageDB, loadDiagramFromData]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
 | 
			
		||||
        async (diagramId: string) => {
 | 
			
		||||
            const diagram = await db.getDiagram(diagramId, {
 | 
			
		||||
            const diagram = await storageDB.getDiagram(diagramId, {
 | 
			
		||||
                includeRelationships: true,
 | 
			
		||||
                includeTables: true,
 | 
			
		||||
                includeDependencies: true,
 | 
			
		||||
                includeAreas: true,
 | 
			
		||||
                includeCustomTypes: true,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diagram) {
 | 
			
		||||
@@ -1406,7 +1604,151 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
 | 
			
		||||
            return diagram;
 | 
			
		||||
        },
 | 
			
		||||
        [db, loadDiagramFromData]
 | 
			
		||||
        [storageDB, loadDiagramFromData]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Custom type operations
 | 
			
		||||
    const getCustomType: ChartDBContext['getCustomType'] = useCallback(
 | 
			
		||||
        (id: string) => customTypes.find((type) => type.id === id) ?? null,
 | 
			
		||||
        [customTypes]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const addCustomTypes: ChartDBContext['addCustomTypes'] = useCallback(
 | 
			
		||||
        async (
 | 
			
		||||
            customTypes: DBCustomType[],
 | 
			
		||||
            options = { updateHistory: true }
 | 
			
		||||
        ) => {
 | 
			
		||||
            setCustomTypes((currentTypes) => [...currentTypes, ...customTypes]);
 | 
			
		||||
            const updatedAt = new Date();
 | 
			
		||||
            setDiagramUpdatedAt(updatedAt);
 | 
			
		||||
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
 | 
			
		||||
                ...customTypes.map((customType) =>
 | 
			
		||||
                    db.addCustomType({ diagramId, customType })
 | 
			
		||||
                ),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            if (options.updateHistory) {
 | 
			
		||||
                addUndoAction({
 | 
			
		||||
                    action: 'addCustomTypes',
 | 
			
		||||
                    redoData: { customTypes },
 | 
			
		||||
                    undoData: { customTypeIds: customTypes.map((t) => t.id) },
 | 
			
		||||
                });
 | 
			
		||||
                resetRedoStack();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [db, diagramId, setCustomTypes, addUndoAction, resetRedoStack]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const addCustomType: ChartDBContext['addCustomType'] = useCallback(
 | 
			
		||||
        async (customType: DBCustomType, options = { updateHistory: true }) => {
 | 
			
		||||
            return addCustomTypes([customType], options);
 | 
			
		||||
        },
 | 
			
		||||
        [addCustomTypes]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const createCustomType: ChartDBContext['createCustomType'] = useCallback(
 | 
			
		||||
        async (attributes) => {
 | 
			
		||||
            const customType: DBCustomType = {
 | 
			
		||||
                id: generateId(),
 | 
			
		||||
                name: `type_${customTypes.length + 1}`,
 | 
			
		||||
                kind: DBCustomTypeKind.enum,
 | 
			
		||||
                values: [],
 | 
			
		||||
                fields: [],
 | 
			
		||||
                ...attributes,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            await addCustomType(customType);
 | 
			
		||||
            return customType;
 | 
			
		||||
        },
 | 
			
		||||
        [addCustomType, customTypes]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const removeCustomTypes: ChartDBContext['removeCustomTypes'] = useCallback(
 | 
			
		||||
        async (ids, options = { updateHistory: true }) => {
 | 
			
		||||
            const typesToRemove = ids
 | 
			
		||||
                .map((id) => getCustomType(id))
 | 
			
		||||
                .filter(Boolean) as DBCustomType[];
 | 
			
		||||
 | 
			
		||||
            setCustomTypes((types) =>
 | 
			
		||||
                types.filter((type) => !ids.includes(type.id))
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            const updatedAt = new Date();
 | 
			
		||||
            setDiagramUpdatedAt(updatedAt);
 | 
			
		||||
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
 | 
			
		||||
                ...ids.map((id) => db.deleteCustomType({ diagramId, id })),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            if (typesToRemove.length > 0 && options.updateHistory) {
 | 
			
		||||
                addUndoAction({
 | 
			
		||||
                    action: 'removeCustomTypes',
 | 
			
		||||
                    redoData: {
 | 
			
		||||
                        customTypeIds: ids,
 | 
			
		||||
                    },
 | 
			
		||||
                    undoData: {
 | 
			
		||||
                        customTypes: typesToRemove,
 | 
			
		||||
                    },
 | 
			
		||||
                });
 | 
			
		||||
                resetRedoStack();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
            db,
 | 
			
		||||
            diagramId,
 | 
			
		||||
            setCustomTypes,
 | 
			
		||||
            addUndoAction,
 | 
			
		||||
            resetRedoStack,
 | 
			
		||||
            getCustomType,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const removeCustomType: ChartDBContext['removeCustomType'] = useCallback(
 | 
			
		||||
        async (id: string, options = { updateHistory: true }) => {
 | 
			
		||||
            return removeCustomTypes([id], options);
 | 
			
		||||
        },
 | 
			
		||||
        [removeCustomTypes]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const updateCustomType: ChartDBContext['updateCustomType'] = useCallback(
 | 
			
		||||
        async (
 | 
			
		||||
            id: string,
 | 
			
		||||
            customType: Partial<DBCustomType>,
 | 
			
		||||
            options = { updateHistory: true }
 | 
			
		||||
        ) => {
 | 
			
		||||
            const prevCustomType = getCustomType(id);
 | 
			
		||||
            setCustomTypes((types) =>
 | 
			
		||||
                types.map((t) => (t.id === id ? { ...t, ...customType } : t))
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            const updatedAt = new Date();
 | 
			
		||||
            setDiagramUpdatedAt(updatedAt);
 | 
			
		||||
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
 | 
			
		||||
                db.updateCustomType({ id, attributes: customType }),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            if (!!prevCustomType && options.updateHistory) {
 | 
			
		||||
                addUndoAction({
 | 
			
		||||
                    action: 'updateCustomType',
 | 
			
		||||
                    redoData: { customTypeId: id, customType },
 | 
			
		||||
                    undoData: { customTypeId: id, customType: prevCustomType },
 | 
			
		||||
                });
 | 
			
		||||
                resetRedoStack();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
            db,
 | 
			
		||||
            setCustomTypes,
 | 
			
		||||
            addUndoAction,
 | 
			
		||||
            resetRedoStack,
 | 
			
		||||
            getCustomType,
 | 
			
		||||
            diagramId,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
@@ -1418,12 +1760,12 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                tables,
 | 
			
		||||
                relationships,
 | 
			
		||||
                dependencies,
 | 
			
		||||
                areas,
 | 
			
		||||
                currentDiagram,
 | 
			
		||||
                schemas,
 | 
			
		||||
                filteredSchemas,
 | 
			
		||||
                events,
 | 
			
		||||
                readonly,
 | 
			
		||||
                filterSchemas,
 | 
			
		||||
                updateDiagramData,
 | 
			
		||||
                updateDiagramId,
 | 
			
		||||
                updateDiagramName,
 | 
			
		||||
                loadDiagram,
 | 
			
		||||
@@ -1465,6 +1807,23 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                removeDependency,
 | 
			
		||||
                removeDependencies,
 | 
			
		||||
                updateDependency,
 | 
			
		||||
                createArea,
 | 
			
		||||
                addArea,
 | 
			
		||||
                addAreas,
 | 
			
		||||
                getArea,
 | 
			
		||||
                removeArea,
 | 
			
		||||
                removeAreas,
 | 
			
		||||
                updateArea,
 | 
			
		||||
                customTypes,
 | 
			
		||||
                createCustomType,
 | 
			
		||||
                addCustomType,
 | 
			
		||||
                addCustomTypes,
 | 
			
		||||
                getCustomType,
 | 
			
		||||
                removeCustomType,
 | 
			
		||||
                removeCustomTypes,
 | 
			
		||||
                updateCustomType,
 | 
			
		||||
                highlightCustomTypeId,
 | 
			
		||||
                highlightedCustomType,
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,10 @@ import type { ChartDBConfig } from '@/lib/domain/config';
 | 
			
		||||
 | 
			
		||||
export interface ConfigContext {
 | 
			
		||||
    config?: ChartDBConfig;
 | 
			
		||||
    updateConfig: (config: Partial<ChartDBConfig>) => Promise<void>;
 | 
			
		||||
    updateConfig: (params: {
 | 
			
		||||
        config?: Partial<ChartDBConfig>;
 | 
			
		||||
        updateFn?: (config: ChartDBConfig) => ChartDBConfig;
 | 
			
		||||
    }) => Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ConfigContext = createContext<ConfigContext>({
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import React, { useEffect } from 'react';
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { ConfigContext } from './config-context';
 | 
			
		||||
 | 
			
		||||
import { useStorage } from '@/hooks/use-storage';
 | 
			
		||||
@@ -8,7 +8,7 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { getConfig, updateConfig: updateDataConfig } = useStorage();
 | 
			
		||||
    const [config, setConfig] = React.useState<ChartDBConfig | undefined>();
 | 
			
		||||
    const [config, setConfig] = useState<ChartDBConfig | undefined>();
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        const loadConfig = async () => {
 | 
			
		||||
@@ -19,19 +19,38 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        loadConfig();
 | 
			
		||||
    }, [getConfig]);
 | 
			
		||||
 | 
			
		||||
    const updateConfig: ConfigContext['updateConfig'] = async (
 | 
			
		||||
        config: Partial<ChartDBConfig>
 | 
			
		||||
    ) => {
 | 
			
		||||
        await updateDataConfig(config);
 | 
			
		||||
        setConfig((prevConfig) =>
 | 
			
		||||
            prevConfig
 | 
			
		||||
                ? { ...prevConfig, ...config }
 | 
			
		||||
                : { ...{ defaultDiagramId: '' }, ...config }
 | 
			
		||||
        );
 | 
			
		||||
    const updateConfig: ConfigContext['updateConfig'] = async ({
 | 
			
		||||
        config,
 | 
			
		||||
        updateFn,
 | 
			
		||||
    }) => {
 | 
			
		||||
        const promise = new Promise<void>((resolve) => {
 | 
			
		||||
            setConfig((prevConfig) => {
 | 
			
		||||
                let baseConfig: ChartDBConfig = { defaultDiagramId: '' };
 | 
			
		||||
                if (prevConfig) {
 | 
			
		||||
                    baseConfig = prevConfig;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const updatedConfig = updateFn
 | 
			
		||||
                    ? updateFn(baseConfig)
 | 
			
		||||
                    : { ...baseConfig, ...config };
 | 
			
		||||
 | 
			
		||||
                updateDataConfig(updatedConfig).then(() => {
 | 
			
		||||
                    resolve();
 | 
			
		||||
                });
 | 
			
		||||
                return updatedConfig;
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return promise;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <ConfigContext.Provider value={{ config, updateConfig }}>
 | 
			
		||||
        <ConfigContext.Provider
 | 
			
		||||
            value={{
 | 
			
		||||
                config,
 | 
			
		||||
                updateConfig,
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
        </ConfigContext.Provider>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,50 @@
 | 
			
		||||
import type { DBSchema } from '@/lib/domain';
 | 
			
		||||
import type {
 | 
			
		||||
    DiagramFilter,
 | 
			
		||||
    FilterTableInfo,
 | 
			
		||||
} from '@/lib/domain/diagram-filter/diagram-filter';
 | 
			
		||||
import { emptyFn } from '@/lib/utils';
 | 
			
		||||
import { createContext } from 'react';
 | 
			
		||||
 | 
			
		||||
export interface DiagramFilterContext {
 | 
			
		||||
    filter?: DiagramFilter;
 | 
			
		||||
    loading: boolean;
 | 
			
		||||
 | 
			
		||||
    hasActiveFilter: boolean;
 | 
			
		||||
    schemasDisplayed: DBSchema[];
 | 
			
		||||
 | 
			
		||||
    clearSchemaIdsFilter: () => void;
 | 
			
		||||
    clearTableIdsFilter: () => void;
 | 
			
		||||
 | 
			
		||||
    setTableIdsFilterEmpty: () => void;
 | 
			
		||||
 | 
			
		||||
    // reset
 | 
			
		||||
    resetFilter: () => void;
 | 
			
		||||
 | 
			
		||||
    toggleSchemaFilter: (schemaId: string) => void;
 | 
			
		||||
    toggleTableFilter: (tableId: string) => void;
 | 
			
		||||
    addSchemaToFilter: (schemaId: string) => void;
 | 
			
		||||
    addTablesToFilter: (attrs: {
 | 
			
		||||
        tableIds?: string[];
 | 
			
		||||
        filterCallback?: (table: FilterTableInfo) => boolean;
 | 
			
		||||
    }) => void;
 | 
			
		||||
    removeTablesFromFilter: (attrs: {
 | 
			
		||||
        tableIds?: string[];
 | 
			
		||||
        filterCallback?: (table: FilterTableInfo) => boolean;
 | 
			
		||||
    }) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const diagramFilterContext = createContext<DiagramFilterContext>({
 | 
			
		||||
    hasActiveFilter: false,
 | 
			
		||||
    clearSchemaIdsFilter: emptyFn,
 | 
			
		||||
    clearTableIdsFilter: emptyFn,
 | 
			
		||||
    setTableIdsFilterEmpty: emptyFn,
 | 
			
		||||
    resetFilter: emptyFn,
 | 
			
		||||
    toggleSchemaFilter: emptyFn,
 | 
			
		||||
    toggleTableFilter: emptyFn,
 | 
			
		||||
    addSchemaToFilter: emptyFn,
 | 
			
		||||
    schemasDisplayed: [],
 | 
			
		||||
    addTablesToFilter: emptyFn,
 | 
			
		||||
    removeTablesFromFilter: emptyFn,
 | 
			
		||||
    loading: false,
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										559
									
								
								src/context/diagram-filter-context/diagram-filter-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,559 @@
 | 
			
		||||
import React, {
 | 
			
		||||
    useCallback,
 | 
			
		||||
    useEffect,
 | 
			
		||||
    useMemo,
 | 
			
		||||
    useRef,
 | 
			
		||||
    useState,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import type { DiagramFilterContext } from './diagram-filter-context';
 | 
			
		||||
import { diagramFilterContext } from './diagram-filter-context';
 | 
			
		||||
import type {
 | 
			
		||||
    DiagramFilter,
 | 
			
		||||
    FilterTableInfo,
 | 
			
		||||
} from '@/lib/domain/diagram-filter/diagram-filter';
 | 
			
		||||
import {
 | 
			
		||||
    reduceFilter,
 | 
			
		||||
    spreadFilterTables,
 | 
			
		||||
} from '@/lib/domain/diagram-filter/diagram-filter';
 | 
			
		||||
import { useStorage } from '@/hooks/use-storage';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { filterTable } from '@/lib/domain/diagram-filter/filter';
 | 
			
		||||
import { databasesWithSchemas, schemaNameToSchemaId } from '@/lib/domain';
 | 
			
		||||
import { defaultSchemas } from '@/lib/data/default-schemas';
 | 
			
		||||
import type { ChartDBEvent } from '../chartdb-context/chartdb-context';
 | 
			
		||||
 | 
			
		||||
export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { diagramId, tables, schemas, databaseType, events } = useChartDB();
 | 
			
		||||
    const { getDiagramFilter, updateDiagramFilter } = useStorage();
 | 
			
		||||
    const [filter, setFilter] = useState<DiagramFilter>({});
 | 
			
		||||
    const [loading, setLoading] = useState<boolean>(true);
 | 
			
		||||
 | 
			
		||||
    const allSchemasIds = useMemo(() => {
 | 
			
		||||
        return schemas.map((schema) => schema.id);
 | 
			
		||||
    }, [schemas]);
 | 
			
		||||
 | 
			
		||||
    const allTables: FilterTableInfo[] = useMemo(() => {
 | 
			
		||||
        return tables.map(
 | 
			
		||||
            (table) =>
 | 
			
		||||
                ({
 | 
			
		||||
                    id: table.id,
 | 
			
		||||
                    schemaId: table.schema
 | 
			
		||||
                        ? schemaNameToSchemaId(table.schema)
 | 
			
		||||
                        : defaultSchemas[databaseType],
 | 
			
		||||
                    schema: table.schema ?? defaultSchemas[databaseType],
 | 
			
		||||
                    areaId: table.parentAreaId ?? undefined,
 | 
			
		||||
                }) satisfies FilterTableInfo
 | 
			
		||||
        );
 | 
			
		||||
    }, [tables, databaseType]);
 | 
			
		||||
 | 
			
		||||
    const diagramIdOfLoadedFilter = useRef<string | null>(null);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (diagramId && diagramId === diagramIdOfLoadedFilter.current) {
 | 
			
		||||
            updateDiagramFilter(diagramId, filter);
 | 
			
		||||
        }
 | 
			
		||||
    }, [diagramId, filter, updateDiagramFilter]);
 | 
			
		||||
 | 
			
		||||
    // Reset filter when diagram changes
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (diagramIdOfLoadedFilter.current === diagramId) {
 | 
			
		||||
            // If the diagramId hasn't changed, do not reset the filter
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setLoading(true);
 | 
			
		||||
 | 
			
		||||
        const loadFilterFromStorage = async (diagramId: string) => {
 | 
			
		||||
            if (diagramId) {
 | 
			
		||||
                const storedFilter = await getDiagramFilter(diagramId);
 | 
			
		||||
 | 
			
		||||
                let filterToSet = storedFilter;
 | 
			
		||||
 | 
			
		||||
                if (!filterToSet) {
 | 
			
		||||
                    // If no filter is stored, set default based on database type
 | 
			
		||||
                    filterToSet =
 | 
			
		||||
                        schemas.length > 1
 | 
			
		||||
                            ? { schemaIds: [schemas[0].id] }
 | 
			
		||||
                            : {};
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                setFilter(filterToSet);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            setLoading(false);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        setFilter({});
 | 
			
		||||
 | 
			
		||||
        if (diagramId) {
 | 
			
		||||
            loadFilterFromStorage(diagramId);
 | 
			
		||||
            diagramIdOfLoadedFilter.current = diagramId;
 | 
			
		||||
        }
 | 
			
		||||
    }, [diagramId, getDiagramFilter, schemas]);
 | 
			
		||||
 | 
			
		||||
    const clearSchemaIds: DiagramFilterContext['clearSchemaIdsFilter'] =
 | 
			
		||||
        useCallback(() => {
 | 
			
		||||
            setFilter(
 | 
			
		||||
                (prev) =>
 | 
			
		||||
                    ({
 | 
			
		||||
                        ...prev,
 | 
			
		||||
                        schemaIds: undefined,
 | 
			
		||||
                    }) satisfies DiagramFilter
 | 
			
		||||
            );
 | 
			
		||||
        }, []);
 | 
			
		||||
 | 
			
		||||
    const clearTableIds: DiagramFilterContext['clearTableIdsFilter'] =
 | 
			
		||||
        useCallback(() => {
 | 
			
		||||
            setFilter(
 | 
			
		||||
                (prev) =>
 | 
			
		||||
                    ({
 | 
			
		||||
                        ...prev,
 | 
			
		||||
                        tableIds: undefined,
 | 
			
		||||
                    }) satisfies DiagramFilter
 | 
			
		||||
            );
 | 
			
		||||
        }, []);
 | 
			
		||||
 | 
			
		||||
    const setTableIdsEmpty: DiagramFilterContext['setTableIdsFilterEmpty'] =
 | 
			
		||||
        useCallback(() => {
 | 
			
		||||
            setFilter(
 | 
			
		||||
                (prev) =>
 | 
			
		||||
                    ({
 | 
			
		||||
                        ...prev,
 | 
			
		||||
                        tableIds: [],
 | 
			
		||||
                    }) satisfies DiagramFilter
 | 
			
		||||
            );
 | 
			
		||||
        }, []);
 | 
			
		||||
 | 
			
		||||
    // Reset filter
 | 
			
		||||
    const resetFilter: DiagramFilterContext['resetFilter'] = useCallback(() => {
 | 
			
		||||
        setFilter({});
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const toggleSchemaFilter: DiagramFilterContext['toggleSchemaFilter'] =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (schemaId: string) => {
 | 
			
		||||
                setFilter((prev) => {
 | 
			
		||||
                    const currentSchemaIds = prev.schemaIds;
 | 
			
		||||
 | 
			
		||||
                    // Check if schema is currently visible
 | 
			
		||||
                    const isSchemaVisible = !allTables.some(
 | 
			
		||||
                        (table) =>
 | 
			
		||||
                            table.schemaId === schemaId &&
 | 
			
		||||
                            filterTable({
 | 
			
		||||
                                table: {
 | 
			
		||||
                                    id: table.id,
 | 
			
		||||
                                    schema: table.schema,
 | 
			
		||||
                                },
 | 
			
		||||
                                filter: prev,
 | 
			
		||||
                                options: {
 | 
			
		||||
                                    defaultSchema: defaultSchemas[databaseType],
 | 
			
		||||
                                },
 | 
			
		||||
                            }) === false
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    let newSchemaIds: string[] | undefined;
 | 
			
		||||
                    let newTableIds: string[] | undefined = prev.tableIds;
 | 
			
		||||
 | 
			
		||||
                    if (isSchemaVisible) {
 | 
			
		||||
                        // Schema is visible, make it not visible
 | 
			
		||||
                        if (!currentSchemaIds) {
 | 
			
		||||
                            // All schemas are visible, create filter with all except this one
 | 
			
		||||
                            newSchemaIds = allSchemasIds.filter(
 | 
			
		||||
                                (id) => id !== schemaId
 | 
			
		||||
                            );
 | 
			
		||||
                        } else {
 | 
			
		||||
                            // Remove this schema from the filter
 | 
			
		||||
                            newSchemaIds = currentSchemaIds.filter(
 | 
			
		||||
                                (id) => id !== schemaId
 | 
			
		||||
                            );
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        // Remove tables from this schema from tableIds if present
 | 
			
		||||
                        if (prev.tableIds) {
 | 
			
		||||
                            const schemaTableIds = allTables
 | 
			
		||||
                                .filter((table) => table.schemaId === schemaId)
 | 
			
		||||
                                .map((table) => table.id);
 | 
			
		||||
                            newTableIds = prev.tableIds.filter(
 | 
			
		||||
                                (id) => !schemaTableIds.includes(id)
 | 
			
		||||
                            );
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // Schema is not visible, make it visible
 | 
			
		||||
                        newSchemaIds = [
 | 
			
		||||
                            ...new Set([...(currentSchemaIds || []), schemaId]),
 | 
			
		||||
                        ];
 | 
			
		||||
 | 
			
		||||
                        // Add tables from this schema to tableIds if tableIds is defined
 | 
			
		||||
                        if (prev.tableIds) {
 | 
			
		||||
                            const schemaTableIds = allTables
 | 
			
		||||
                                .filter((table) => table.schemaId === schemaId)
 | 
			
		||||
                                .map((table) => table.id);
 | 
			
		||||
                            newTableIds = [
 | 
			
		||||
                                ...new Set([
 | 
			
		||||
                                    ...prev.tableIds,
 | 
			
		||||
                                    ...schemaTableIds,
 | 
			
		||||
                                ]),
 | 
			
		||||
                            ];
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Use reduceFilter to optimize and handle edge cases
 | 
			
		||||
                    return reduceFilter(
 | 
			
		||||
                        {
 | 
			
		||||
                            schemaIds: newSchemaIds,
 | 
			
		||||
                            tableIds: newTableIds,
 | 
			
		||||
                        },
 | 
			
		||||
                        allTables satisfies FilterTableInfo[],
 | 
			
		||||
                        {
 | 
			
		||||
                            databaseWithSchemas:
 | 
			
		||||
                                databasesWithSchemas.includes(databaseType),
 | 
			
		||||
                        }
 | 
			
		||||
                    );
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
            [allSchemasIds, allTables, databaseType]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const toggleTableFilterForNoSchema = useCallback(
 | 
			
		||||
        (tableId: string) => {
 | 
			
		||||
            setFilter((prev) => {
 | 
			
		||||
                const currentTableIds = prev.tableIds;
 | 
			
		||||
 | 
			
		||||
                // Check if table is currently visible
 | 
			
		||||
                const isTableVisible = filterTable({
 | 
			
		||||
                    table: { id: tableId, schema: undefined },
 | 
			
		||||
                    filter: prev,
 | 
			
		||||
                    options: { defaultSchema: undefined },
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                let newTableIds: string[] | undefined;
 | 
			
		||||
 | 
			
		||||
                if (isTableVisible) {
 | 
			
		||||
                    // Table is visible, make it not visible
 | 
			
		||||
                    if (!currentTableIds) {
 | 
			
		||||
                        // All tables are visible, create filter with all except this one
 | 
			
		||||
                        newTableIds = allTables
 | 
			
		||||
                            .filter((t) => t.id !== tableId)
 | 
			
		||||
                            .map((t) => t.id);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // Remove this table from the filter
 | 
			
		||||
                        newTableIds = currentTableIds.filter(
 | 
			
		||||
                            (id) => id !== tableId
 | 
			
		||||
                        );
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    // Table is not visible, make it visible
 | 
			
		||||
                    newTableIds = [
 | 
			
		||||
                        ...new Set([...(currentTableIds || []), tableId]),
 | 
			
		||||
                    ];
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // Use reduceFilter to optimize and handle edge cases
 | 
			
		||||
                return reduceFilter(
 | 
			
		||||
                    {
 | 
			
		||||
                        schemaIds: undefined,
 | 
			
		||||
                        tableIds: newTableIds,
 | 
			
		||||
                    },
 | 
			
		||||
                    allTables satisfies FilterTableInfo[],
 | 
			
		||||
                    {
 | 
			
		||||
                        databaseWithSchemas:
 | 
			
		||||
                            databasesWithSchemas.includes(databaseType),
 | 
			
		||||
                    }
 | 
			
		||||
                );
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [allTables, databaseType]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const toggleTableFilter: DiagramFilterContext['toggleTableFilter'] =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (tableId: string) => {
 | 
			
		||||
                if (!databasesWithSchemas.includes(databaseType)) {
 | 
			
		||||
                    // No schemas, toggle table filter without schema context
 | 
			
		||||
                    toggleTableFilterForNoSchema(tableId);
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                setFilter((prev) => {
 | 
			
		||||
                    // Find the table in the tables list
 | 
			
		||||
                    const tableInfo = allTables.find((t) => t.id === tableId);
 | 
			
		||||
 | 
			
		||||
                    if (!tableInfo) {
 | 
			
		||||
                        return prev;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Check if table is currently visible using filterTable
 | 
			
		||||
                    const isTableVisible = filterTable({
 | 
			
		||||
                        table: {
 | 
			
		||||
                            id: tableInfo.id,
 | 
			
		||||
                            schema: tableInfo.schema,
 | 
			
		||||
                        },
 | 
			
		||||
                        filter: prev,
 | 
			
		||||
                        options: {
 | 
			
		||||
                            defaultSchema: defaultSchemas[databaseType],
 | 
			
		||||
                        },
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    let newSchemaIds = prev.schemaIds;
 | 
			
		||||
                    let newTableIds = prev.tableIds;
 | 
			
		||||
 | 
			
		||||
                    if (isTableVisible) {
 | 
			
		||||
                        // Table is visible, make it not visible
 | 
			
		||||
 | 
			
		||||
                        // If the table is visible due to its schema being in schemaIds
 | 
			
		||||
                        if (
 | 
			
		||||
                            tableInfo?.schemaId &&
 | 
			
		||||
                            prev.schemaIds?.includes(tableInfo.schemaId)
 | 
			
		||||
                        ) {
 | 
			
		||||
                            // Remove the schema from schemaIds and add all other tables from that schema to tableIds
 | 
			
		||||
                            newSchemaIds = prev.schemaIds.filter(
 | 
			
		||||
                                (id) => id !== tableInfo.schemaId
 | 
			
		||||
                            );
 | 
			
		||||
 | 
			
		||||
                            // Get all other tables from this schema (except the one being toggled)
 | 
			
		||||
                            const otherTablesFromSchema = allTables
 | 
			
		||||
                                .filter(
 | 
			
		||||
                                    (t) =>
 | 
			
		||||
                                        t.schemaId === tableInfo.schemaId &&
 | 
			
		||||
                                        t.id !== tableId
 | 
			
		||||
                                )
 | 
			
		||||
                                .map((t) => t.id);
 | 
			
		||||
 | 
			
		||||
                            // Add these tables to tableIds
 | 
			
		||||
                            newTableIds = [
 | 
			
		||||
                                ...(prev.tableIds || []),
 | 
			
		||||
                                ...otherTablesFromSchema,
 | 
			
		||||
                            ];
 | 
			
		||||
                        } else if (prev.tableIds?.includes(tableId)) {
 | 
			
		||||
                            // Table is visible because it's in tableIds, remove it
 | 
			
		||||
                            newTableIds = prev.tableIds.filter(
 | 
			
		||||
                                (id) => id !== tableId
 | 
			
		||||
                            );
 | 
			
		||||
                        } else if (!prev.tableIds && !prev.schemaIds) {
 | 
			
		||||
                            // No filters = all visible, create filter with all tables except this one
 | 
			
		||||
                            newTableIds = allTables
 | 
			
		||||
                                .filter((t) => t.id !== tableId)
 | 
			
		||||
                                .map((t) => t.id);
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // Table is not visible, make it visible by adding to tableIds
 | 
			
		||||
                        newTableIds = [...(prev.tableIds || []), tableId];
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Use reduceFilter to optimize and handle edge cases
 | 
			
		||||
                    return reduceFilter(
 | 
			
		||||
                        {
 | 
			
		||||
                            schemaIds: newSchemaIds,
 | 
			
		||||
                            tableIds: newTableIds,
 | 
			
		||||
                        },
 | 
			
		||||
                        allTables satisfies FilterTableInfo[],
 | 
			
		||||
                        {
 | 
			
		||||
                            databaseWithSchemas:
 | 
			
		||||
                                databasesWithSchemas.includes(databaseType),
 | 
			
		||||
                        }
 | 
			
		||||
                    );
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
            [allTables, databaseType, toggleTableFilterForNoSchema]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const addSchemaToFilter: DiagramFilterContext['addSchemaToFilter'] =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (schemaId: string) => {
 | 
			
		||||
                setFilter((prev) => {
 | 
			
		||||
                    const currentSchemaIds = prev.schemaIds;
 | 
			
		||||
                    if (!currentSchemaIds) {
 | 
			
		||||
                        // No schemas are filtered
 | 
			
		||||
                        return prev;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // If schema is already filtered, do nothing
 | 
			
		||||
                    if (currentSchemaIds.includes(schemaId)) {
 | 
			
		||||
                        return prev;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Add schema to the filter
 | 
			
		||||
                    const newSchemaIds = [...currentSchemaIds, schemaId];
 | 
			
		||||
 | 
			
		||||
                    if (newSchemaIds.length === allSchemasIds.length) {
 | 
			
		||||
                        // All schemas are now filtered, set to undefined
 | 
			
		||||
                        return {
 | 
			
		||||
                            ...prev,
 | 
			
		||||
                            schemaIds: undefined,
 | 
			
		||||
                        } satisfies DiagramFilter;
 | 
			
		||||
                    }
 | 
			
		||||
                    return {
 | 
			
		||||
                        ...prev,
 | 
			
		||||
                        schemaIds: newSchemaIds,
 | 
			
		||||
                    } satisfies DiagramFilter;
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
            [allSchemasIds.length]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const hasActiveFilter: boolean = useMemo(() => {
 | 
			
		||||
        return !!filter.schemaIds || !!filter.tableIds;
 | 
			
		||||
    }, [filter]);
 | 
			
		||||
 | 
			
		||||
    const schemasDisplayed: DiagramFilterContext['schemasDisplayed'] =
 | 
			
		||||
        useMemo(() => {
 | 
			
		||||
            if (!hasActiveFilter) {
 | 
			
		||||
                return schemas;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const displayedSchemaIds = new Set<string>();
 | 
			
		||||
            for (const table of allTables) {
 | 
			
		||||
                if (
 | 
			
		||||
                    filterTable({
 | 
			
		||||
                        table: {
 | 
			
		||||
                            id: table.id,
 | 
			
		||||
                            schema: table.schema,
 | 
			
		||||
                        },
 | 
			
		||||
                        filter,
 | 
			
		||||
                        options: {
 | 
			
		||||
                            defaultSchema: defaultSchemas[databaseType],
 | 
			
		||||
                        },
 | 
			
		||||
                    })
 | 
			
		||||
                ) {
 | 
			
		||||
                    if (table.schemaId) {
 | 
			
		||||
                        displayedSchemaIds.add(table.schemaId);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return schemas.filter((schema) =>
 | 
			
		||||
                displayedSchemaIds.has(schema.id)
 | 
			
		||||
            );
 | 
			
		||||
        }, [hasActiveFilter, schemas, allTables, filter, databaseType]);
 | 
			
		||||
 | 
			
		||||
    const addTablesToFilter: DiagramFilterContext['addTablesToFilter'] =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            ({ tableIds, filterCallback }) => {
 | 
			
		||||
                setFilter((prev) => {
 | 
			
		||||
                    let tableIdsToAdd: string[];
 | 
			
		||||
 | 
			
		||||
                    if (tableIds) {
 | 
			
		||||
                        // If tableIds are provided, use them directly
 | 
			
		||||
                        tableIdsToAdd = tableIds;
 | 
			
		||||
                    } else if (filterCallback) {
 | 
			
		||||
                        // If filterCallback is provided, filter tables based on it
 | 
			
		||||
                        tableIdsToAdd = allTables
 | 
			
		||||
                            .filter(filterCallback)
 | 
			
		||||
                            .map((table) => table.id);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // If neither is provided, do nothing
 | 
			
		||||
                        return prev;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    const filterByTableIds = spreadFilterTables(
 | 
			
		||||
                        prev,
 | 
			
		||||
                        allTables satisfies FilterTableInfo[]
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    const currentTableIds = filterByTableIds.tableIds || [];
 | 
			
		||||
                    const newTableIds = [
 | 
			
		||||
                        ...new Set([...currentTableIds, ...tableIdsToAdd]),
 | 
			
		||||
                    ];
 | 
			
		||||
 | 
			
		||||
                    return reduceFilter(
 | 
			
		||||
                        {
 | 
			
		||||
                            ...filterByTableIds,
 | 
			
		||||
                            tableIds: newTableIds,
 | 
			
		||||
                        },
 | 
			
		||||
                        allTables satisfies FilterTableInfo[],
 | 
			
		||||
                        {
 | 
			
		||||
                            databaseWithSchemas:
 | 
			
		||||
                                databasesWithSchemas.includes(databaseType),
 | 
			
		||||
                        }
 | 
			
		||||
                    );
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
            [allTables, databaseType]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const removeTablesFromFilter: DiagramFilterContext['removeTablesFromFilter'] =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            ({ tableIds, filterCallback }) => {
 | 
			
		||||
                setFilter((prev) => {
 | 
			
		||||
                    let tableIdsToRemovoe: string[];
 | 
			
		||||
 | 
			
		||||
                    if (tableIds) {
 | 
			
		||||
                        // If tableIds are provided, use them directly
 | 
			
		||||
                        tableIdsToRemovoe = tableIds;
 | 
			
		||||
                    } else if (filterCallback) {
 | 
			
		||||
                        // If filterCallback is provided, filter tables based on it
 | 
			
		||||
                        tableIdsToRemovoe = allTables
 | 
			
		||||
                            .filter(filterCallback)
 | 
			
		||||
                            .map((table) => table.id);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        // If neither is provided, do nothing
 | 
			
		||||
                        return prev;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    const filterByTableIds = spreadFilterTables(
 | 
			
		||||
                        prev,
 | 
			
		||||
                        allTables satisfies FilterTableInfo[]
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    const currentTableIds = filterByTableIds.tableIds || [];
 | 
			
		||||
                    const newTableIds = currentTableIds.filter(
 | 
			
		||||
                        (id) => !tableIdsToRemovoe.includes(id)
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    return reduceFilter(
 | 
			
		||||
                        {
 | 
			
		||||
                            ...filterByTableIds,
 | 
			
		||||
                            tableIds: newTableIds,
 | 
			
		||||
                        },
 | 
			
		||||
                        allTables satisfies FilterTableInfo[],
 | 
			
		||||
                        {
 | 
			
		||||
                            databaseWithSchemas:
 | 
			
		||||
                                databasesWithSchemas.includes(databaseType),
 | 
			
		||||
                        }
 | 
			
		||||
                    );
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
            [allTables, databaseType]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const eventConsumer = useCallback(
 | 
			
		||||
        (event: ChartDBEvent) => {
 | 
			
		||||
            if (!hasActiveFilter) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (event.action === 'add_tables') {
 | 
			
		||||
                addTablesToFilter({
 | 
			
		||||
                    tableIds: event.data.tables.map((table) => table.id),
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [hasActiveFilter, addTablesToFilter]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    events.useSubscription(eventConsumer);
 | 
			
		||||
 | 
			
		||||
    const value: DiagramFilterContext = {
 | 
			
		||||
        loading,
 | 
			
		||||
        filter,
 | 
			
		||||
        clearSchemaIdsFilter: clearSchemaIds,
 | 
			
		||||
        setTableIdsFilterEmpty: setTableIdsEmpty,
 | 
			
		||||
        clearTableIdsFilter: clearTableIds,
 | 
			
		||||
        resetFilter,
 | 
			
		||||
        toggleSchemaFilter,
 | 
			
		||||
        toggleTableFilter,
 | 
			
		||||
        addSchemaToFilter,
 | 
			
		||||
        hasActiveFilter,
 | 
			
		||||
        schemasDisplayed,
 | 
			
		||||
        addTablesToFilter,
 | 
			
		||||
        removeTablesFromFilter,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <diagramFilterContext.Provider value={value}>
 | 
			
		||||
            {children}
 | 
			
		||||
        </diagramFilterContext.Provider>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										4
									
								
								src/context/diagram-filter-context/use-diagram-filter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
			
		||||
import { useContext } from 'react';
 | 
			
		||||
import { diagramFilterContext } from './diagram-filter-context';
 | 
			
		||||
 | 
			
		||||
export const useDiagramFilter = () => useContext(diagramFilterContext);
 | 
			
		||||
@@ -7,12 +7,14 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
 | 
			
		||||
import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-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 {
 | 
			
		||||
    // Create diagram dialog
 | 
			
		||||
    openCreateDiagramDialog: () => void;
 | 
			
		||||
    openCreateDiagramDialog: (
 | 
			
		||||
        params?: Omit<CreateDiagramDialogProps, 'dialog'>
 | 
			
		||||
    ) => void;
 | 
			
		||||
    closeCreateDiagramDialog: () => void;
 | 
			
		||||
 | 
			
		||||
    // Open diagram dialog
 | 
			
		||||
@@ -64,12 +66,6 @@ export interface DialogContext {
 | 
			
		||||
        params: Omit<ImportDiagramDialogProps, 'dialog'>
 | 
			
		||||
    ) => void;
 | 
			
		||||
    closeImportDiagramDialog: () => void;
 | 
			
		||||
 | 
			
		||||
    // Import DBML dialog
 | 
			
		||||
    openImportDBMLDialog: (
 | 
			
		||||
        params?: Omit<ImportDBMLDialogProps, 'dialog'>
 | 
			
		||||
    ) => void;
 | 
			
		||||
    closeImportDBMLDialog: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const dialogContext = createContext<DialogContext>({
 | 
			
		||||
@@ -93,6 +89,4 @@ export const dialogContext = createContext<DialogContext>({
 | 
			
		||||
    closeExportDiagramDialog: emptyFn,
 | 
			
		||||
    openImportDiagramDialog: emptyFn,
 | 
			
		||||
    closeImportDiagramDialog: emptyFn,
 | 
			
		||||
    openImportDBMLDialog: emptyFn,
 | 
			
		||||
    closeImportDBMLDialog: emptyFn,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import React, { useCallback, useState } from 'react';
 | 
			
		||||
import type { DialogContext } from './dialog-context';
 | 
			
		||||
import { dialogContext } from './dialog-context';
 | 
			
		||||
import type { CreateDiagramDialogProps } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
 | 
			
		||||
import { CreateDiagramDialog } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
 | 
			
		||||
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
 | 
			
		||||
import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
 | 
			
		||||
@@ -19,13 +20,22 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
 | 
			
		||||
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
 | 
			
		||||
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
 | 
			
		||||
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
 | 
			
		||||
import 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> = ({
 | 
			
		||||
    children,
 | 
			
		||||
}) => {
 | 
			
		||||
    const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false);
 | 
			
		||||
    const [newDiagramDialogParams, setNewDiagramDialogParams] =
 | 
			
		||||
        useState<Omit<CreateDiagramDialogProps, 'dialog'>>();
 | 
			
		||||
    const openNewDiagramDialogHandler: DialogContext['openCreateDiagramDialog'] =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (props) => {
 | 
			
		||||
                setNewDiagramDialogParams(props);
 | 
			
		||||
                setOpenNewDiagramDialog(true);
 | 
			
		||||
            },
 | 
			
		||||
            [setOpenNewDiagramDialog]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const [openOpenDiagramDialog, setOpenOpenDiagramDialog] = useState(false);
 | 
			
		||||
    const [openDiagramDialogParams, setOpenDiagramDialogParams] =
 | 
			
		||||
        useState<Omit<OpenDiagramDialogProps, 'dialog'>>();
 | 
			
		||||
@@ -120,15 +130,10 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    const [openImportDiagramDialog, setOpenImportDiagramDialog] =
 | 
			
		||||
        useState(false);
 | 
			
		||||
 | 
			
		||||
    // Import DBML dialog
 | 
			
		||||
    const [openImportDBMLDialog, setOpenImportDBMLDialog] = useState(false);
 | 
			
		||||
    const [importDBMLDialogParams, setImportDBMLDialogParams] =
 | 
			
		||||
        useState<Omit<ImportDBMLDialogProps, 'dialog'>>();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <dialogContext.Provider
 | 
			
		||||
            value={{
 | 
			
		||||
                openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
 | 
			
		||||
                openCreateDiagramDialog: openNewDiagramDialogHandler,
 | 
			
		||||
                closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
 | 
			
		||||
                openOpenDiagramDialog: openOpenDiagramDialogHandler,
 | 
			
		||||
                closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
 | 
			
		||||
@@ -153,15 +158,13 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
 | 
			
		||||
                closeImportDiagramDialog: () =>
 | 
			
		||||
                    setOpenImportDiagramDialog(false),
 | 
			
		||||
                openImportDBMLDialog: (params) => {
 | 
			
		||||
                    setImportDBMLDialogParams(params);
 | 
			
		||||
                    setOpenImportDBMLDialog(true);
 | 
			
		||||
                },
 | 
			
		||||
                closeImportDBMLDialog: () => setOpenImportDBMLDialog(false),
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
            <CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} />
 | 
			
		||||
            <CreateDiagramDialog
 | 
			
		||||
                dialog={{ open: openNewDiagramDialog }}
 | 
			
		||||
                {...newDiagramDialogParams}
 | 
			
		||||
            />
 | 
			
		||||
            <OpenDiagramDialog
 | 
			
		||||
                dialog={{ open: openOpenDiagramDialog }}
 | 
			
		||||
                {...openDiagramDialogParams}
 | 
			
		||||
@@ -189,10 +192,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            />
 | 
			
		||||
            <ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
 | 
			
		||||
            <ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
 | 
			
		||||
            <ImportDBMLDialog
 | 
			
		||||
                dialog={{ open: openImportDBMLDialog }}
 | 
			
		||||
                {...importDBMLDialogParams}
 | 
			
		||||
            />
 | 
			
		||||
        </dialogContext.Provider>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,433 +0,0 @@
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import type {
 | 
			
		||||
    ChartDBDiff,
 | 
			
		||||
    DiffMap,
 | 
			
		||||
    DiffObject,
 | 
			
		||||
    FieldDiffAttribute,
 | 
			
		||||
} from '../types';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DBIndex } from '@/lib/domain/db-index';
 | 
			
		||||
 | 
			
		||||
export function getDiffMapKey({
 | 
			
		||||
    diffObject,
 | 
			
		||||
    objectId,
 | 
			
		||||
    attribute,
 | 
			
		||||
}: {
 | 
			
		||||
    diffObject: DiffObject;
 | 
			
		||||
    objectId: string;
 | 
			
		||||
    attribute?: string;
 | 
			
		||||
}): string {
 | 
			
		||||
    return attribute
 | 
			
		||||
        ? `${diffObject}-${attribute}-${objectId}`
 | 
			
		||||
        : `${diffObject}-${objectId}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function generateDiff({
 | 
			
		||||
    diagram,
 | 
			
		||||
    newDiagram,
 | 
			
		||||
}: {
 | 
			
		||||
    diagram: Diagram;
 | 
			
		||||
    newDiagram: Diagram;
 | 
			
		||||
}): {
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
    changedTables: Map<string, boolean>;
 | 
			
		||||
    changedFields: Map<string, boolean>;
 | 
			
		||||
} {
 | 
			
		||||
    const newDiffs = new Map<string, ChartDBDiff>();
 | 
			
		||||
    const changedTables = new Map<string, boolean>();
 | 
			
		||||
    const changedFields = new Map<string, boolean>();
 | 
			
		||||
 | 
			
		||||
    // Compare tables
 | 
			
		||||
    compareTables({ diagram, newDiagram, diffMap: newDiffs, changedTables });
 | 
			
		||||
 | 
			
		||||
    // Compare fields and indexes for matching tables
 | 
			
		||||
    compareTableContents({
 | 
			
		||||
        diagram,
 | 
			
		||||
        newDiagram,
 | 
			
		||||
        diffMap: newDiffs,
 | 
			
		||||
        changedTables,
 | 
			
		||||
        changedFields,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Compare relationships
 | 
			
		||||
    compareRelationships({ diagram, newDiagram, diffMap: newDiffs });
 | 
			
		||||
 | 
			
		||||
    return { diffMap: newDiffs, changedTables, changedFields };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Compare tables between diagrams
 | 
			
		||||
function compareTables({
 | 
			
		||||
    diagram,
 | 
			
		||||
    newDiagram,
 | 
			
		||||
    diffMap,
 | 
			
		||||
    changedTables,
 | 
			
		||||
}: {
 | 
			
		||||
    diagram: Diagram;
 | 
			
		||||
    newDiagram: Diagram;
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
    changedTables: Map<string, boolean>;
 | 
			
		||||
}) {
 | 
			
		||||
    const oldTables = diagram.tables || [];
 | 
			
		||||
    const newTables = newDiagram.tables || [];
 | 
			
		||||
 | 
			
		||||
    // Check for added tables
 | 
			
		||||
    for (const newTable of newTables) {
 | 
			
		||||
        if (!oldTables.find((t) => t.id === newTable.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({ diffObject: 'table', objectId: newTable.id }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'table',
 | 
			
		||||
                    type: 'added',
 | 
			
		||||
                    tableId: newTable.id,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            changedTables.set(newTable.id, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check for removed tables
 | 
			
		||||
    for (const oldTable of oldTables) {
 | 
			
		||||
        if (!newTables.find((t) => t.id === oldTable.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({ diffObject: 'table', objectId: oldTable.id }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'table',
 | 
			
		||||
                    type: 'removed',
 | 
			
		||||
                    tableId: oldTable.id,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            changedTables.set(oldTable.id, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check for table name and comments changes
 | 
			
		||||
    for (const oldTable of oldTables) {
 | 
			
		||||
        const newTable = newTables.find((t) => t.id === oldTable.id);
 | 
			
		||||
 | 
			
		||||
        if (!newTable) continue;
 | 
			
		||||
 | 
			
		||||
        if (oldTable.name !== newTable.name) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'table',
 | 
			
		||||
                    objectId: oldTable.id,
 | 
			
		||||
                    attribute: 'name',
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'table',
 | 
			
		||||
                    type: 'changed',
 | 
			
		||||
                    tableId: oldTable.id,
 | 
			
		||||
                    attributes: 'name',
 | 
			
		||||
                    newValue: newTable.name,
 | 
			
		||||
                    oldValue: oldTable.name,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            changedTables.set(oldTable.id, true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (oldTable.comments !== newTable.comments) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'table',
 | 
			
		||||
                    objectId: oldTable.id,
 | 
			
		||||
                    attribute: 'comments',
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'table',
 | 
			
		||||
                    type: 'changed',
 | 
			
		||||
                    tableId: oldTable.id,
 | 
			
		||||
                    attributes: 'comments',
 | 
			
		||||
                    newValue: newTable.comments,
 | 
			
		||||
                    oldValue: oldTable.comments,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            changedTables.set(oldTable.id, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Compare fields and indexes for matching tables
 | 
			
		||||
function compareTableContents({
 | 
			
		||||
    diagram,
 | 
			
		||||
    newDiagram,
 | 
			
		||||
    diffMap,
 | 
			
		||||
    changedTables,
 | 
			
		||||
    changedFields,
 | 
			
		||||
}: {
 | 
			
		||||
    diagram: Diagram;
 | 
			
		||||
    newDiagram: Diagram;
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
    changedTables: Map<string, boolean>;
 | 
			
		||||
    changedFields: Map<string, boolean>;
 | 
			
		||||
}) {
 | 
			
		||||
    const oldTables = diagram.tables || [];
 | 
			
		||||
    const newTables = newDiagram.tables || [];
 | 
			
		||||
 | 
			
		||||
    // For each table that exists in both diagrams
 | 
			
		||||
    for (const oldTable of oldTables) {
 | 
			
		||||
        const newTable = newTables.find((t) => t.id === oldTable.id);
 | 
			
		||||
        if (!newTable) continue;
 | 
			
		||||
 | 
			
		||||
        // Compare fields
 | 
			
		||||
        compareFields({
 | 
			
		||||
            tableId: oldTable.id,
 | 
			
		||||
            oldFields: oldTable.fields,
 | 
			
		||||
            newFields: newTable.fields,
 | 
			
		||||
            diffMap,
 | 
			
		||||
            changedTables,
 | 
			
		||||
            changedFields,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Compare indexes
 | 
			
		||||
        compareIndexes({
 | 
			
		||||
            tableId: oldTable.id,
 | 
			
		||||
            oldIndexes: oldTable.indexes,
 | 
			
		||||
            newIndexes: newTable.indexes,
 | 
			
		||||
            diffMap,
 | 
			
		||||
            changedTables,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Compare fields between tables
 | 
			
		||||
function compareFields({
 | 
			
		||||
    tableId,
 | 
			
		||||
    oldFields,
 | 
			
		||||
    newFields,
 | 
			
		||||
    diffMap,
 | 
			
		||||
    changedTables,
 | 
			
		||||
    changedFields,
 | 
			
		||||
}: {
 | 
			
		||||
    tableId: string;
 | 
			
		||||
    oldFields: DBField[];
 | 
			
		||||
    newFields: DBField[];
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
    changedTables: Map<string, boolean>;
 | 
			
		||||
    changedFields: Map<string, boolean>;
 | 
			
		||||
}) {
 | 
			
		||||
    // Check for added fields
 | 
			
		||||
    for (const newField of newFields) {
 | 
			
		||||
        if (!oldFields.find((f) => f.id === newField.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'field',
 | 
			
		||||
                    objectId: newField.id,
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'field',
 | 
			
		||||
                    type: 'added',
 | 
			
		||||
                    fieldId: newField.id,
 | 
			
		||||
                    tableId,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            changedTables.set(tableId, true);
 | 
			
		||||
            changedFields.set(newField.id, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check for removed fields
 | 
			
		||||
    for (const oldField of oldFields) {
 | 
			
		||||
        if (!newFields.find((f) => f.id === oldField.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'field',
 | 
			
		||||
                    objectId: oldField.id,
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'field',
 | 
			
		||||
                    type: 'removed',
 | 
			
		||||
                    fieldId: oldField.id,
 | 
			
		||||
                    tableId,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            changedTables.set(tableId, true);
 | 
			
		||||
            changedFields.set(oldField.id, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check for field changes
 | 
			
		||||
    for (const oldField of oldFields) {
 | 
			
		||||
        const newField = newFields.find((f) => f.id === oldField.id);
 | 
			
		||||
        if (!newField) continue;
 | 
			
		||||
 | 
			
		||||
        // Compare basic field properties
 | 
			
		||||
        compareFieldProperties({
 | 
			
		||||
            tableId,
 | 
			
		||||
            oldField,
 | 
			
		||||
            newField,
 | 
			
		||||
            diffMap,
 | 
			
		||||
            changedTables,
 | 
			
		||||
            changedFields,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Compare field properties
 | 
			
		||||
function compareFieldProperties({
 | 
			
		||||
    tableId,
 | 
			
		||||
    oldField,
 | 
			
		||||
    newField,
 | 
			
		||||
    diffMap,
 | 
			
		||||
    changedTables,
 | 
			
		||||
    changedFields,
 | 
			
		||||
}: {
 | 
			
		||||
    tableId: string;
 | 
			
		||||
    oldField: DBField;
 | 
			
		||||
    newField: DBField;
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
    changedTables: Map<string, boolean>;
 | 
			
		||||
    changedFields: Map<string, boolean>;
 | 
			
		||||
}) {
 | 
			
		||||
    const changedAttributes: FieldDiffAttribute[] = [];
 | 
			
		||||
 | 
			
		||||
    if (oldField.name !== newField.name) {
 | 
			
		||||
        changedAttributes.push('name');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (oldField.type.id !== newField.type.id) {
 | 
			
		||||
        changedAttributes.push('type');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (oldField.primaryKey !== newField.primaryKey) {
 | 
			
		||||
        changedAttributes.push('primaryKey');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (oldField.unique !== newField.unique) {
 | 
			
		||||
        changedAttributes.push('unique');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (oldField.nullable !== newField.nullable) {
 | 
			
		||||
        changedAttributes.push('nullable');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (oldField.comments !== newField.comments) {
 | 
			
		||||
        changedAttributes.push('comments');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (changedAttributes.length > 0) {
 | 
			
		||||
        for (const attribute of changedAttributes) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'field',
 | 
			
		||||
                    objectId: oldField.id,
 | 
			
		||||
                    attribute: attribute,
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'field',
 | 
			
		||||
                    type: 'changed',
 | 
			
		||||
                    fieldId: oldField.id,
 | 
			
		||||
                    tableId,
 | 
			
		||||
                    attributes: attribute,
 | 
			
		||||
                    oldValue: oldField[attribute],
 | 
			
		||||
                    newValue: newField[attribute],
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        changedTables.set(tableId, true);
 | 
			
		||||
        changedFields.set(oldField.id, true);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Compare indexes between tables
 | 
			
		||||
function compareIndexes({
 | 
			
		||||
    tableId,
 | 
			
		||||
    oldIndexes,
 | 
			
		||||
    newIndexes,
 | 
			
		||||
    diffMap,
 | 
			
		||||
    changedTables,
 | 
			
		||||
}: {
 | 
			
		||||
    tableId: string;
 | 
			
		||||
    oldIndexes: DBIndex[];
 | 
			
		||||
    newIndexes: DBIndex[];
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
    changedTables: Map<string, boolean>;
 | 
			
		||||
}) {
 | 
			
		||||
    // Check for added indexes
 | 
			
		||||
    for (const newIndex of newIndexes) {
 | 
			
		||||
        if (!oldIndexes.find((i) => i.id === newIndex.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'index',
 | 
			
		||||
                    objectId: newIndex.id,
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'index',
 | 
			
		||||
                    type: 'added',
 | 
			
		||||
                    indexId: newIndex.id,
 | 
			
		||||
                    tableId,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            changedTables.set(tableId, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check for removed indexes
 | 
			
		||||
    for (const oldIndex of oldIndexes) {
 | 
			
		||||
        if (!newIndexes.find((i) => i.id === oldIndex.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'index',
 | 
			
		||||
                    objectId: oldIndex.id,
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'index',
 | 
			
		||||
                    type: 'removed',
 | 
			
		||||
                    indexId: oldIndex.id,
 | 
			
		||||
                    tableId,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            changedTables.set(tableId, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Compare relationships between diagrams
 | 
			
		||||
function compareRelationships({
 | 
			
		||||
    diagram,
 | 
			
		||||
    newDiagram,
 | 
			
		||||
    diffMap,
 | 
			
		||||
}: {
 | 
			
		||||
    diagram: Diagram;
 | 
			
		||||
    newDiagram: Diagram;
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
}) {
 | 
			
		||||
    const oldRelationships = diagram.relationships || [];
 | 
			
		||||
    const newRelationships = newDiagram.relationships || [];
 | 
			
		||||
 | 
			
		||||
    // Check for added relationships
 | 
			
		||||
    for (const newRelationship of newRelationships) {
 | 
			
		||||
        if (!oldRelationships.find((r) => r.id === newRelationship.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'relationship',
 | 
			
		||||
                    objectId: newRelationship.id,
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'relationship',
 | 
			
		||||
                    type: 'added',
 | 
			
		||||
                    relationshipId: newRelationship.id,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check for removed relationships
 | 
			
		||||
    for (const oldRelationship of oldRelationships) {
 | 
			
		||||
        if (!newRelationships.find((r) => r.id === oldRelationship.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'relationship',
 | 
			
		||||
                    objectId: oldRelationship.id,
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'relationship',
 | 
			
		||||
                    type: 'removed',
 | 
			
		||||
                    relationshipId: oldRelationship.id,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import { createContext } from 'react';
 | 
			
		||||
import type { DiffMap } from './types';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import type { EventEmitter } from 'ahooks/lib/useEventEmitter';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DataType } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
 | 
			
		||||
import type { DiffMap } from '@/lib/domain/diff/diff';
 | 
			
		||||
 | 
			
		||||
export type DiffEventType = 'diff_calculated';
 | 
			
		||||
 | 
			
		||||
@@ -14,35 +14,45 @@ export type DiffEventBase<T extends DiffEventType, D> = {
 | 
			
		||||
    data: D;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type DiffCalculatedData = {
 | 
			
		||||
    tablesAdded: DBTable[];
 | 
			
		||||
    fieldsAdded: Map<string, DBField[]>;
 | 
			
		||||
    relationshipsAdded: DBRelationship[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type DiffCalculatedEvent = DiffEventBase<
 | 
			
		||||
    'diff_calculated',
 | 
			
		||||
    {
 | 
			
		||||
        tablesAdded: DBTable[];
 | 
			
		||||
        fieldsAdded: Map<string, DBField[]>;
 | 
			
		||||
        relationshipsAdded: DBRelationship[];
 | 
			
		||||
    }
 | 
			
		||||
    DiffCalculatedData
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export type DiffEvent = DiffCalculatedEvent;
 | 
			
		||||
 | 
			
		||||
export interface DiffContext {
 | 
			
		||||
    newDiagram: Diagram | null;
 | 
			
		||||
    originalDiagram: Diagram | null;
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
    hasDiff: boolean;
 | 
			
		||||
    isSummaryOnly: boolean;
 | 
			
		||||
 | 
			
		||||
    calculateDiff: ({
 | 
			
		||||
        diagram,
 | 
			
		||||
        newDiagram,
 | 
			
		||||
        options,
 | 
			
		||||
    }: {
 | 
			
		||||
        diagram: Diagram;
 | 
			
		||||
        newDiagram: Diagram;
 | 
			
		||||
        options?: {
 | 
			
		||||
            summaryOnly?: boolean;
 | 
			
		||||
        };
 | 
			
		||||
    }) => void;
 | 
			
		||||
    resetDiff: () => void;
 | 
			
		||||
 | 
			
		||||
    // table diff
 | 
			
		||||
    checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
 | 
			
		||||
    checkIfNewTable: ({ tableId }: { tableId: string }) => boolean;
 | 
			
		||||
    checkIfTableRemoved: ({ tableId }: { tableId: string }) => boolean;
 | 
			
		||||
    getTableNewName: ({ tableId }: { tableId: string }) => string | null;
 | 
			
		||||
    getTableNewColor: ({ tableId }: { tableId: string }) => string | null;
 | 
			
		||||
 | 
			
		||||
    // field diff
 | 
			
		||||
    checkIfFieldHasChange: ({
 | 
			
		||||
@@ -56,6 +66,15 @@ export interface DiffContext {
 | 
			
		||||
    checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
 | 
			
		||||
    getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
 | 
			
		||||
    getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
 | 
			
		||||
    getFieldNewPrimaryKey: ({ fieldId }: { fieldId: string }) => boolean | null;
 | 
			
		||||
    getFieldNewNullable: ({ fieldId }: { fieldId: string }) => boolean | null;
 | 
			
		||||
    getFieldNewCharacterMaximumLength: ({
 | 
			
		||||
        fieldId,
 | 
			
		||||
    }: {
 | 
			
		||||
        fieldId: string;
 | 
			
		||||
    }) => string | null;
 | 
			
		||||
    getFieldNewScale: ({ fieldId }: { fieldId: string }) => number | null;
 | 
			
		||||
    getFieldNewPrecision: ({ fieldId }: { fieldId: string }) => number | null;
 | 
			
		||||
 | 
			
		||||
    // relationship diff
 | 
			
		||||
    checkIfNewRelationship: ({
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,28 @@
 | 
			
		||||
import React, { useCallback } from 'react';
 | 
			
		||||
import type { DiffContext, DiffEvent } from './diff-context';
 | 
			
		||||
import type {
 | 
			
		||||
    DiffCalculatedData,
 | 
			
		||||
    DiffContext,
 | 
			
		||||
    DiffEvent,
 | 
			
		||||
} from './diff-context';
 | 
			
		||||
import { diffContext } from './diff-context';
 | 
			
		||||
import type { ChartDBDiff, DiffMap } from './types';
 | 
			
		||||
import { generateDiff, getDiffMapKey } from './diff-check/diff-check';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    generateDiff,
 | 
			
		||||
    getDiffMapKey,
 | 
			
		||||
} from '@/lib/domain/diff/diff-check/diff-check';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import { useEventEmitter } from 'ahooks';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DataType } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
 | 
			
		||||
import type { ChartDBDiff, DiffMap } from '@/lib/domain/diff/diff';
 | 
			
		||||
 | 
			
		||||
export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
}) => {
 | 
			
		||||
    const [newDiagram, setNewDiagram] = React.useState<Diagram | null>(null);
 | 
			
		||||
    const [originalDiagram, setOriginalDiagram] =
 | 
			
		||||
        React.useState<Diagram | null>(null);
 | 
			
		||||
    const [diffMap, setDiffMap] = React.useState<DiffMap>(
 | 
			
		||||
        new Map<string, ChartDBDiff>()
 | 
			
		||||
    );
 | 
			
		||||
@@ -22,6 +32,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    const [fieldsChanged, setFieldsChanged] = React.useState<
 | 
			
		||||
        Map<string, boolean>
 | 
			
		||||
    >(new Map<string, boolean>());
 | 
			
		||||
    const [isSummaryOnly, setIsSummaryOnly] = React.useState<boolean>(false);
 | 
			
		||||
 | 
			
		||||
    const events = useEventEmitter<DiffEvent>();
 | 
			
		||||
 | 
			
		||||
@@ -39,7 +50,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                if (diff.object === 'field' && diff.type === 'added') {
 | 
			
		||||
                    const field = newDiagram?.tables
 | 
			
		||||
                        ?.find((table) => table.id === diff.tableId)
 | 
			
		||||
                        ?.fields.find((f) => f.id === diff.fieldId);
 | 
			
		||||
                        ?.fields.find((f) => f.id === diff.newField.id);
 | 
			
		||||
 | 
			
		||||
                    if (field) {
 | 
			
		||||
                        newFieldsMap.set(diff.tableId, [
 | 
			
		||||
@@ -67,7 +78,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            diffMap.forEach((diff) => {
 | 
			
		||||
                if (diff.object === 'relationship' && diff.type === 'added') {
 | 
			
		||||
                    const relationship = newDiagram?.relationships?.find(
 | 
			
		||||
                        (rel) => rel.id === diff.relationshipId
 | 
			
		||||
                        (rel) => rel.id === diff.newRelationship.id
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    if (relationship) {
 | 
			
		||||
@@ -81,8 +92,43 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        []
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const generateDiffCalculatedData = useCallback(
 | 
			
		||||
        ({
 | 
			
		||||
            newDiagram,
 | 
			
		||||
            diffMap,
 | 
			
		||||
        }: {
 | 
			
		||||
            newDiagram: Diagram;
 | 
			
		||||
            diffMap: DiffMap;
 | 
			
		||||
        }): DiffCalculatedData => {
 | 
			
		||||
            return {
 | 
			
		||||
                tablesAdded:
 | 
			
		||||
                    newDiagram?.tables?.filter((table) => {
 | 
			
		||||
                        const tableKey = getDiffMapKey({
 | 
			
		||||
                            diffObject: 'table',
 | 
			
		||||
                            objectId: table.id,
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                        return (
 | 
			
		||||
                            diffMap.has(tableKey) &&
 | 
			
		||||
                            diffMap.get(tableKey)?.type === 'added'
 | 
			
		||||
                        );
 | 
			
		||||
                    }) ?? [],
 | 
			
		||||
 | 
			
		||||
                fieldsAdded: generateNewFieldsMap({
 | 
			
		||||
                    diffMap: diffMap,
 | 
			
		||||
                    newDiagram: newDiagram,
 | 
			
		||||
                }),
 | 
			
		||||
                relationshipsAdded: findNewRelationships({
 | 
			
		||||
                    diffMap: diffMap,
 | 
			
		||||
                    newDiagram: newDiagram,
 | 
			
		||||
                }),
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        [findNewRelationships, generateNewFieldsMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const calculateDiff: DiffContext['calculateDiff'] = useCallback(
 | 
			
		||||
        ({ diagram, newDiagram: newDiagramArg }) => {
 | 
			
		||||
        ({ diagram, newDiagram: newDiagramArg, options }) => {
 | 
			
		||||
            const {
 | 
			
		||||
                diffMap: newDiffs,
 | 
			
		||||
                changedTables: newChangedTables,
 | 
			
		||||
@@ -93,35 +139,18 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            setTablesChanged(newChangedTables);
 | 
			
		||||
            setFieldsChanged(newChangedFields);
 | 
			
		||||
            setNewDiagram(newDiagramArg);
 | 
			
		||||
            setOriginalDiagram(diagram);
 | 
			
		||||
            setIsSummaryOnly(options?.summaryOnly ?? false);
 | 
			
		||||
 | 
			
		||||
            events.emit({
 | 
			
		||||
                action: 'diff_calculated',
 | 
			
		||||
                data: {
 | 
			
		||||
                    tablesAdded:
 | 
			
		||||
                        newDiagramArg?.tables?.filter((table) => {
 | 
			
		||||
                            const tableKey = getDiffMapKey({
 | 
			
		||||
                                diffObject: 'table',
 | 
			
		||||
                                objectId: table.id,
 | 
			
		||||
                            });
 | 
			
		||||
 | 
			
		||||
                            return (
 | 
			
		||||
                                newDiffs.has(tableKey) &&
 | 
			
		||||
                                newDiffs.get(tableKey)?.type === 'added'
 | 
			
		||||
                            );
 | 
			
		||||
                        }) ?? [],
 | 
			
		||||
 | 
			
		||||
                    fieldsAdded: generateNewFieldsMap({
 | 
			
		||||
                        diffMap: newDiffs,
 | 
			
		||||
                        newDiagram: newDiagramArg,
 | 
			
		||||
                    }),
 | 
			
		||||
                    relationshipsAdded: findNewRelationships({
 | 
			
		||||
                        diffMap: newDiffs,
 | 
			
		||||
                        newDiagram: newDiagramArg,
 | 
			
		||||
                    }),
 | 
			
		||||
                },
 | 
			
		||||
                data: generateDiffCalculatedData({
 | 
			
		||||
                    diffMap: newDiffs,
 | 
			
		||||
                    newDiagram: newDiagramArg,
 | 
			
		||||
                }),
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [setDiffMap, events, generateNewFieldsMap, findNewRelationships]
 | 
			
		||||
        [setDiffMap, events, generateDiffCalculatedData]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getTableNewName = useCallback<DiffContext['getTableNewName']>(
 | 
			
		||||
@@ -145,6 +174,26 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getTableNewColor = useCallback<DiffContext['getTableNewColor']>(
 | 
			
		||||
        ({ tableId }) => {
 | 
			
		||||
            const tableColorKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'table',
 | 
			
		||||
                objectId: tableId,
 | 
			
		||||
                attribute: 'color',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diffMap.has(tableColorKey)) {
 | 
			
		||||
                const diff = diffMap.get(tableColorKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as string;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            return null;
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const checkIfTableHasChange = useCallback<
 | 
			
		||||
        DiffContext['checkIfTableHasChange']
 | 
			
		||||
    >(({ tableId }) => tablesChanged.get(tableId) ?? false, [tablesChanged]);
 | 
			
		||||
@@ -258,6 +307,117 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getFieldNewPrimaryKey = useCallback<
 | 
			
		||||
        DiffContext['getFieldNewPrimaryKey']
 | 
			
		||||
    >(
 | 
			
		||||
        ({ fieldId }) => {
 | 
			
		||||
            const fieldKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'field',
 | 
			
		||||
                objectId: fieldId,
 | 
			
		||||
                attribute: 'primaryKey',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diffMap.has(fieldKey)) {
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as boolean;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getFieldNewNullable = useCallback<DiffContext['getFieldNewNullable']>(
 | 
			
		||||
        ({ fieldId }) => {
 | 
			
		||||
            const fieldKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'field',
 | 
			
		||||
                objectId: fieldId,
 | 
			
		||||
                attribute: 'nullable',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diffMap.has(fieldKey)) {
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as boolean;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getFieldNewCharacterMaximumLength = useCallback<
 | 
			
		||||
        DiffContext['getFieldNewCharacterMaximumLength']
 | 
			
		||||
    >(
 | 
			
		||||
        ({ fieldId }) => {
 | 
			
		||||
            const fieldKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'field',
 | 
			
		||||
                objectId: fieldId,
 | 
			
		||||
                attribute: 'characterMaximumLength',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diffMap.has(fieldKey)) {
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as string;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getFieldNewScale = useCallback<DiffContext['getFieldNewScale']>(
 | 
			
		||||
        ({ fieldId }) => {
 | 
			
		||||
            const fieldKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'field',
 | 
			
		||||
                objectId: fieldId,
 | 
			
		||||
                attribute: 'scale',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diffMap.has(fieldKey)) {
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as number;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getFieldNewPrecision = useCallback<
 | 
			
		||||
        DiffContext['getFieldNewPrecision']
 | 
			
		||||
    >(
 | 
			
		||||
        ({ fieldId }) => {
 | 
			
		||||
            const fieldKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'field',
 | 
			
		||||
                objectId: fieldId,
 | 
			
		||||
                attribute: 'precision',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diffMap.has(fieldKey)) {
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as number;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const checkIfNewRelationship = useCallback<
 | 
			
		||||
        DiffContext['checkIfNewRelationship']
 | 
			
		||||
    >(
 | 
			
		||||
@@ -292,20 +452,33 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const resetDiff = useCallback<DiffContext['resetDiff']>(() => {
 | 
			
		||||
        setDiffMap(new Map<string, ChartDBDiff>());
 | 
			
		||||
        setTablesChanged(new Map<string, boolean>());
 | 
			
		||||
        setFieldsChanged(new Map<string, boolean>());
 | 
			
		||||
        setNewDiagram(null);
 | 
			
		||||
        setOriginalDiagram(null);
 | 
			
		||||
        setIsSummaryOnly(false);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <diffContext.Provider
 | 
			
		||||
            value={{
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                originalDiagram,
 | 
			
		||||
                diffMap,
 | 
			
		||||
                hasDiff: diffMap.size > 0,
 | 
			
		||||
                isSummaryOnly,
 | 
			
		||||
 | 
			
		||||
                calculateDiff,
 | 
			
		||||
                resetDiff,
 | 
			
		||||
 | 
			
		||||
                // table diff
 | 
			
		||||
                getTableNewName,
 | 
			
		||||
                checkIfNewTable,
 | 
			
		||||
                checkIfTableRemoved,
 | 
			
		||||
                checkIfTableHasChange,
 | 
			
		||||
                getTableNewColor,
 | 
			
		||||
 | 
			
		||||
                // field diff
 | 
			
		||||
                checkIfFieldHasChange,
 | 
			
		||||
@@ -313,6 +486,11 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                checkIfNewField,
 | 
			
		||||
                getFieldNewName,
 | 
			
		||||
                getFieldNewType,
 | 
			
		||||
                getFieldNewPrimaryKey,
 | 
			
		||||
                getFieldNewNullable,
 | 
			
		||||
                getFieldNewCharacterMaximumLength,
 | 
			
		||||
                getFieldNewScale,
 | 
			
		||||
                getFieldNewPrecision,
 | 
			
		||||
 | 
			
		||||
                // relationship diff
 | 
			
		||||
                checkIfNewRelationship,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,53 +0,0 @@
 | 
			
		||||
import type { DataType } from '@/lib/data/data-types/data-types';
 | 
			
		||||
 | 
			
		||||
export type TableDiffAttribute = 'name' | 'comments';
 | 
			
		||||
 | 
			
		||||
export interface TableDiff {
 | 
			
		||||
    object: 'table';
 | 
			
		||||
    type: 'added' | 'removed' | 'changed';
 | 
			
		||||
    tableId: string;
 | 
			
		||||
    attributes?: TableDiffAttribute;
 | 
			
		||||
    oldValue?: string;
 | 
			
		||||
    newValue?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RelationshipDiff {
 | 
			
		||||
    object: 'relationship';
 | 
			
		||||
    type: 'added' | 'removed';
 | 
			
		||||
    relationshipId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type FieldDiffAttribute =
 | 
			
		||||
    | 'name'
 | 
			
		||||
    | 'type'
 | 
			
		||||
    | 'primaryKey'
 | 
			
		||||
    | 'unique'
 | 
			
		||||
    | 'nullable'
 | 
			
		||||
    | 'comments';
 | 
			
		||||
 | 
			
		||||
export interface FieldDiff {
 | 
			
		||||
    object: 'field';
 | 
			
		||||
    type: 'added' | 'removed' | 'changed';
 | 
			
		||||
    fieldId: string;
 | 
			
		||||
    tableId: string;
 | 
			
		||||
    attributes?: FieldDiffAttribute;
 | 
			
		||||
    oldValue?: string | boolean | DataType;
 | 
			
		||||
    newValue?: string | boolean | DataType;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IndexDiff {
 | 
			
		||||
    object: 'index';
 | 
			
		||||
    type: 'added' | 'removed';
 | 
			
		||||
    indexId: string;
 | 
			
		||||
    tableId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ChartDBDiff = TableDiff | FieldDiff | IndexDiff | RelationshipDiff;
 | 
			
		||||
 | 
			
		||||
export type DiffMap = Map<string, ChartDBDiff>;
 | 
			
		||||
 | 
			
		||||
export type DiffObject =
 | 
			
		||||
    | TableDiff['object']
 | 
			
		||||
    | FieldDiff['object']
 | 
			
		||||
    | IndexDiff['object']
 | 
			
		||||
    | RelationshipDiff['object'];
 | 
			
		||||
@@ -3,7 +3,14 @@ import { emptyFn } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
export type ImageType = 'png' | 'jpeg' | 'svg';
 | 
			
		||||
export interface ExportImageContext {
 | 
			
		||||
    exportImage: (type: ImageType, scale: number) => Promise<void>;
 | 
			
		||||
    exportImage: (
 | 
			
		||||
        type: ImageType,
 | 
			
		||||
        options: {
 | 
			
		||||
            includePatternBG: boolean;
 | 
			
		||||
            transparent: boolean;
 | 
			
		||||
            scale: number;
 | 
			
		||||
        }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const exportImageContext = createContext<ExportImageContext>({
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
 | 
			
		||||
import { useTheme } from '@/hooks/use-theme';
 | 
			
		||||
import logoDark from '@/assets/logo-dark.png';
 | 
			
		||||
import logoLight from '@/assets/logo-light.png';
 | 
			
		||||
import type { EffectiveTheme } from '../theme-context/theme-context';
 | 
			
		||||
 | 
			
		||||
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
@@ -57,8 +58,16 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        []
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getBackgroundColor = useCallback(
 | 
			
		||||
        (theme: EffectiveTheme, transparent: boolean): string => {
 | 
			
		||||
            if (transparent) return 'transparent';
 | 
			
		||||
            return theme === 'light' ? '#ffffff' : '#141414';
 | 
			
		||||
        },
 | 
			
		||||
        []
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const exportImage: ExportImageContext['exportImage'] = useCallback(
 | 
			
		||||
        async (type, scale = 1) => {
 | 
			
		||||
        async (type, { includePatternBG, transparent, scale }) => {
 | 
			
		||||
            showLoader({
 | 
			
		||||
                animated: false,
 | 
			
		||||
            });
 | 
			
		||||
@@ -114,34 +123,37 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                    defs.innerHTML = markerDefs.innerHTML;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const pattern = document.createElementNS(
 | 
			
		||||
                    'http://www.w3.org/2000/svg',
 | 
			
		||||
                    'pattern'
 | 
			
		||||
                );
 | 
			
		||||
                pattern.setAttribute('id', 'background-pattern');
 | 
			
		||||
                pattern.setAttribute('width', String(16 * viewport.zoom));
 | 
			
		||||
                pattern.setAttribute('height', String(16 * viewport.zoom));
 | 
			
		||||
                pattern.setAttribute('patternUnits', 'userSpaceOnUse');
 | 
			
		||||
                pattern.setAttribute(
 | 
			
		||||
                    'patternTransform',
 | 
			
		||||
                    `translate(${viewport.x % (16 * viewport.zoom)} ${viewport.y % (16 * viewport.zoom)})`
 | 
			
		||||
                );
 | 
			
		||||
                if (includePatternBG) {
 | 
			
		||||
                    const pattern = document.createElementNS(
 | 
			
		||||
                        'http://www.w3.org/2000/svg',
 | 
			
		||||
                        'pattern'
 | 
			
		||||
                    );
 | 
			
		||||
                    pattern.setAttribute('id', 'background-pattern');
 | 
			
		||||
                    pattern.setAttribute('width', String(16 * viewport.zoom));
 | 
			
		||||
                    pattern.setAttribute('height', String(16 * viewport.zoom));
 | 
			
		||||
                    pattern.setAttribute('patternUnits', 'userSpaceOnUse');
 | 
			
		||||
                    pattern.setAttribute(
 | 
			
		||||
                        'patternTransform',
 | 
			
		||||
                        `translate(${viewport.x % (16 * viewport.zoom)} ${viewport.y % (16 * viewport.zoom)})`
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                const dot = document.createElementNS(
 | 
			
		||||
                    'http://www.w3.org/2000/svg',
 | 
			
		||||
                    'circle'
 | 
			
		||||
                );
 | 
			
		||||
                    const dot = document.createElementNS(
 | 
			
		||||
                        'http://www.w3.org/2000/svg',
 | 
			
		||||
                        'circle'
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                const dotSize = viewport.zoom * 0.5;
 | 
			
		||||
                dot.setAttribute('cx', String(viewport.zoom));
 | 
			
		||||
                dot.setAttribute('cy', String(viewport.zoom));
 | 
			
		||||
                dot.setAttribute('r', String(dotSize));
 | 
			
		||||
                const dotColor =
 | 
			
		||||
                    effectiveTheme === 'light' ? '#92939C' : '#777777';
 | 
			
		||||
                dot.setAttribute('fill', dotColor);
 | 
			
		||||
                    const dotSize = viewport.zoom * 0.5;
 | 
			
		||||
                    dot.setAttribute('cx', String(viewport.zoom));
 | 
			
		||||
                    dot.setAttribute('cy', String(viewport.zoom));
 | 
			
		||||
                    dot.setAttribute('r', String(dotSize));
 | 
			
		||||
                    const dotColor =
 | 
			
		||||
                        effectiveTheme === 'light' ? '#92939C' : '#777777';
 | 
			
		||||
                    dot.setAttribute('fill', dotColor);
 | 
			
		||||
 | 
			
		||||
                    pattern.appendChild(dot);
 | 
			
		||||
                    defs.appendChild(pattern);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                pattern.appendChild(dot);
 | 
			
		||||
                defs.appendChild(pattern);
 | 
			
		||||
                tempSvg.appendChild(defs);
 | 
			
		||||
 | 
			
		||||
                const backgroundRect = document.createElementNS(
 | 
			
		||||
@@ -196,10 +208,10 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                    const initialDataUrl = await imageCreateFn(
 | 
			
		||||
                        viewportElement,
 | 
			
		||||
                        {
 | 
			
		||||
                            backgroundColor:
 | 
			
		||||
                                effectiveTheme === 'light'
 | 
			
		||||
                                    ? '#ffffff'
 | 
			
		||||
                                    : '#141414',
 | 
			
		||||
                            backgroundColor: getBackgroundColor(
 | 
			
		||||
                                effectiveTheme,
 | 
			
		||||
                                transparent
 | 
			
		||||
                            ),
 | 
			
		||||
                            width: reactFlowBounds.width,
 | 
			
		||||
                            height: reactFlowBounds.height,
 | 
			
		||||
                            style: {
 | 
			
		||||
@@ -285,6 +297,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            }, 0);
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
            getBackgroundColor,
 | 
			
		||||
            downloadImage,
 | 
			
		||||
            getViewport,
 | 
			
		||||
            hideLoader,
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,12 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        removeIndex,
 | 
			
		||||
        updateIndex,
 | 
			
		||||
        removeRelationships,
 | 
			
		||||
        addAreas,
 | 
			
		||||
        removeAreas,
 | 
			
		||||
        updateArea,
 | 
			
		||||
        addCustomTypes,
 | 
			
		||||
        removeCustomTypes,
 | 
			
		||||
        updateCustomType,
 | 
			
		||||
    } = useChartDB();
 | 
			
		||||
 | 
			
		||||
    const redoActionHandlers = useMemo(
 | 
			
		||||
@@ -107,6 +113,28 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                    updateHistory: false,
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
            addAreas: ({ redoData: { areas } }) => {
 | 
			
		||||
                return addAreas(areas, { updateHistory: false });
 | 
			
		||||
            },
 | 
			
		||||
            removeAreas: ({ redoData: { areaIds } }) => {
 | 
			
		||||
                return removeAreas(areaIds, { updateHistory: false });
 | 
			
		||||
            },
 | 
			
		||||
            updateArea: ({ redoData: { areaId, area } }) => {
 | 
			
		||||
                return updateArea(areaId, area, { updateHistory: false });
 | 
			
		||||
            },
 | 
			
		||||
            addCustomTypes: ({ redoData: { customTypes } }) => {
 | 
			
		||||
                return addCustomTypes(customTypes, { updateHistory: false });
 | 
			
		||||
            },
 | 
			
		||||
            removeCustomTypes: ({ redoData: { customTypeIds } }) => {
 | 
			
		||||
                return removeCustomTypes(customTypeIds, {
 | 
			
		||||
                    updateHistory: false,
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
            updateCustomType: ({ redoData: { customTypeId, customType } }) => {
 | 
			
		||||
                return updateCustomType(customTypeId, customType, {
 | 
			
		||||
                    updateHistory: false,
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
        }),
 | 
			
		||||
        [
 | 
			
		||||
            addTables,
 | 
			
		||||
@@ -126,6 +154,12 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            addDependencies,
 | 
			
		||||
            removeDependencies,
 | 
			
		||||
            updateDependency,
 | 
			
		||||
            addAreas,
 | 
			
		||||
            removeAreas,
 | 
			
		||||
            updateArea,
 | 
			
		||||
            addCustomTypes,
 | 
			
		||||
            removeCustomTypes,
 | 
			
		||||
            updateCustomType,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@@ -215,6 +249,28 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                    updateHistory: false,
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
            addAreas: ({ undoData: { areaIds } }) => {
 | 
			
		||||
                return removeAreas(areaIds, { updateHistory: false });
 | 
			
		||||
            },
 | 
			
		||||
            removeAreas: ({ undoData: { areas } }) => {
 | 
			
		||||
                return addAreas(areas, { updateHistory: false });
 | 
			
		||||
            },
 | 
			
		||||
            updateArea: ({ undoData: { areaId, area } }) => {
 | 
			
		||||
                return updateArea(areaId, area, { updateHistory: false });
 | 
			
		||||
            },
 | 
			
		||||
            addCustomTypes: ({ undoData: { customTypeIds } }) => {
 | 
			
		||||
                return removeCustomTypes(customTypeIds, {
 | 
			
		||||
                    updateHistory: false,
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
            removeCustomTypes: ({ undoData: { customTypes } }) => {
 | 
			
		||||
                return addCustomTypes(customTypes, { updateHistory: false });
 | 
			
		||||
            },
 | 
			
		||||
            updateCustomType: ({ undoData: { customTypeId, customType } }) => {
 | 
			
		||||
                return updateCustomType(customTypeId, customType, {
 | 
			
		||||
                    updateHistory: false,
 | 
			
		||||
                });
 | 
			
		||||
            },
 | 
			
		||||
        }),
 | 
			
		||||
        [
 | 
			
		||||
            addTables,
 | 
			
		||||
@@ -234,6 +290,12 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            addDependencies,
 | 
			
		||||
            removeDependencies,
 | 
			
		||||
            updateDependency,
 | 
			
		||||
            addAreas,
 | 
			
		||||
            removeAreas,
 | 
			
		||||
            updateArea,
 | 
			
		||||
            addCustomTypes,
 | 
			
		||||
            removeCustomTypes,
 | 
			
		||||
            updateCustomType,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,8 @@ import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DBIndex } from '@/lib/domain/db-index';
 | 
			
		||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
 | 
			
		||||
import type { DBDependency } from '@/lib/domain/db-dependency';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
 | 
			
		||||
 | 
			
		||||
type Action = keyof ChartDBContext;
 | 
			
		||||
 | 
			
		||||
@@ -123,6 +125,42 @@ type RedoUndoActionRemoveDependencies = RedoUndoActionBase<
 | 
			
		||||
    { dependencies: DBDependency[] }
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
type RedoUndoActionAddAreas = RedoUndoActionBase<
 | 
			
		||||
    'addAreas',
 | 
			
		||||
    { areas: Area[] },
 | 
			
		||||
    { areaIds: string[] }
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
type RedoUndoActionUpdateArea = RedoUndoActionBase<
 | 
			
		||||
    'updateArea',
 | 
			
		||||
    { areaId: string; area: Partial<Area> },
 | 
			
		||||
    { areaId: string; area: Partial<Area> }
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
type RedoUndoActionRemoveAreas = RedoUndoActionBase<
 | 
			
		||||
    'removeAreas',
 | 
			
		||||
    { areaIds: string[] },
 | 
			
		||||
    { areas: Area[] }
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
type RedoUndoActionAddCustomTypes = RedoUndoActionBase<
 | 
			
		||||
    'addCustomTypes',
 | 
			
		||||
    { customTypes: DBCustomType[] },
 | 
			
		||||
    { customTypeIds: string[] }
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
type RedoUndoActionUpdateCustomType = RedoUndoActionBase<
 | 
			
		||||
    'updateCustomType',
 | 
			
		||||
    { customTypeId: string; customType: Partial<DBCustomType> },
 | 
			
		||||
    { customTypeId: string; customType: Partial<DBCustomType> }
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
type RedoUndoActionRemoveCustomTypes = RedoUndoActionBase<
 | 
			
		||||
    'removeCustomTypes',
 | 
			
		||||
    { customTypeIds: string[] },
 | 
			
		||||
    { customTypes: DBCustomType[] }
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export type RedoUndoAction =
 | 
			
		||||
    | RedoUndoActionAddTables
 | 
			
		||||
    | RedoUndoActionRemoveTables
 | 
			
		||||
@@ -140,7 +178,13 @@ export type RedoUndoAction =
 | 
			
		||||
    | RedoUndoActionRemoveRelationships
 | 
			
		||||
    | RedoUndoActionAddDependencies
 | 
			
		||||
    | RedoUndoActionUpdateDependency
 | 
			
		||||
    | RedoUndoActionRemoveDependencies;
 | 
			
		||||
    | RedoUndoActionRemoveDependencies
 | 
			
		||||
    | RedoUndoActionAddAreas
 | 
			
		||||
    | RedoUndoActionUpdateArea
 | 
			
		||||
    | RedoUndoActionRemoveAreas
 | 
			
		||||
    | RedoUndoActionAddCustomTypes
 | 
			
		||||
    | RedoUndoActionUpdateCustomType
 | 
			
		||||
    | RedoUndoActionRemoveCustomTypes;
 | 
			
		||||
 | 
			
		||||
export type RedoActionData<T extends Action> = Extract<
 | 
			
		||||
    RedoUndoAction,
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ export enum KeyboardShortcutAction {
 | 
			
		||||
    TOGGLE_SIDE_PANEL = 'toggle_side_panel',
 | 
			
		||||
    SHOW_ALL = 'show_all',
 | 
			
		||||
    TOGGLE_THEME = 'toggle_theme',
 | 
			
		||||
    TOGGLE_FILTER = 'toggle_filter',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface KeyboardShortcut {
 | 
			
		||||
@@ -71,6 +72,13 @@ export const keyboardShortcuts: Record<
 | 
			
		||||
        keyCombinationMac: 'meta+m',
 | 
			
		||||
        keyCombinationWin: 'ctrl+m',
 | 
			
		||||
    },
 | 
			
		||||
    [KeyboardShortcutAction.TOGGLE_FILTER]: {
 | 
			
		||||
        action: KeyboardShortcutAction.TOGGLE_FILTER,
 | 
			
		||||
        keyCombinationLabelMac: '⌘F',
 | 
			
		||||
        keyCombinationLabelWin: 'Ctrl+F',
 | 
			
		||||
        keyCombinationMac: 'meta+f',
 | 
			
		||||
        keyCombinationWin: 'ctrl+f',
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface KeyboardShortcutForOS {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,36 @@
 | 
			
		||||
import { emptyFn } from '@/lib/utils';
 | 
			
		||||
import { createContext } from 'react';
 | 
			
		||||
 | 
			
		||||
export type SidebarSection = 'tables' | 'relationships' | 'dependencies';
 | 
			
		||||
export type SidebarSection =
 | 
			
		||||
    | 'dbml'
 | 
			
		||||
    | 'tables'
 | 
			
		||||
    | 'refs'
 | 
			
		||||
    | 'areas'
 | 
			
		||||
    | 'customTypes';
 | 
			
		||||
 | 
			
		||||
export interface LayoutContext {
 | 
			
		||||
    openedTableInSidebar: string | undefined;
 | 
			
		||||
    openTableFromSidebar: (tableId: string) => void;
 | 
			
		||||
    closeAllTablesInSidebar: () => void;
 | 
			
		||||
 | 
			
		||||
    openedRelationshipInSidebar: string | undefined;
 | 
			
		||||
    openRelationshipFromSidebar: (relationshipId: string) => void;
 | 
			
		||||
    closeAllRelationshipsInSidebar: () => void;
 | 
			
		||||
 | 
			
		||||
    openedDependencyInSidebar: string | undefined;
 | 
			
		||||
    openDependencyFromSidebar: (dependencyId: string) => void;
 | 
			
		||||
    closeAllDependenciesInSidebar: () => void;
 | 
			
		||||
 | 
			
		||||
    openedRefInSidebar: string | undefined;
 | 
			
		||||
    openRefFromSidebar: (refId: string) => void;
 | 
			
		||||
    closeAllRefsInSidebar: () => void;
 | 
			
		||||
 | 
			
		||||
    openedAreaInSidebar: string | undefined;
 | 
			
		||||
    openAreaFromSidebar: (areaId: string) => void;
 | 
			
		||||
    closeAllAreasInSidebar: () => void;
 | 
			
		||||
 | 
			
		||||
    openedCustomTypeInSidebar: string | undefined;
 | 
			
		||||
    openCustomTypeFromSidebar: (customTypeId: string) => void;
 | 
			
		||||
    closeAllCustomTypesInSidebar: () => void;
 | 
			
		||||
 | 
			
		||||
    selectedSidebarSection: SidebarSection;
 | 
			
		||||
    selectSidebarSection: (section: SidebarSection) => void;
 | 
			
		||||
 | 
			
		||||
@@ -23,24 +38,30 @@ export interface LayoutContext {
 | 
			
		||||
    hideSidePanel: () => void;
 | 
			
		||||
    showSidePanel: () => void;
 | 
			
		||||
    toggleSidePanel: () => void;
 | 
			
		||||
 | 
			
		||||
    isSelectSchemaOpen: boolean;
 | 
			
		||||
    openSelectSchema: () => void;
 | 
			
		||||
    closeSelectSchema: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const layoutContext = createContext<LayoutContext>({
 | 
			
		||||
    openedTableInSidebar: undefined,
 | 
			
		||||
    selectedSidebarSection: 'tables',
 | 
			
		||||
 | 
			
		||||
    openedRelationshipInSidebar: undefined,
 | 
			
		||||
    openRelationshipFromSidebar: emptyFn,
 | 
			
		||||
    closeAllRelationshipsInSidebar: emptyFn,
 | 
			
		||||
 | 
			
		||||
    openedDependencyInSidebar: undefined,
 | 
			
		||||
    openDependencyFromSidebar: emptyFn,
 | 
			
		||||
    closeAllDependenciesInSidebar: emptyFn,
 | 
			
		||||
 | 
			
		||||
    openedRefInSidebar: undefined,
 | 
			
		||||
    openRefFromSidebar: emptyFn,
 | 
			
		||||
    closeAllRefsInSidebar: emptyFn,
 | 
			
		||||
 | 
			
		||||
    openedAreaInSidebar: undefined,
 | 
			
		||||
    openAreaFromSidebar: emptyFn,
 | 
			
		||||
    closeAllAreasInSidebar: emptyFn,
 | 
			
		||||
 | 
			
		||||
    openedCustomTypeInSidebar: undefined,
 | 
			
		||||
    openCustomTypeFromSidebar: emptyFn,
 | 
			
		||||
    closeAllCustomTypesInSidebar: emptyFn,
 | 
			
		||||
 | 
			
		||||
    selectSidebarSection: emptyFn,
 | 
			
		||||
    openTableFromSidebar: emptyFn,
 | 
			
		||||
    closeAllTablesInSidebar: emptyFn,
 | 
			
		||||
@@ -49,8 +70,4 @@ export const layoutContext = createContext<LayoutContext>({
 | 
			
		||||
    hideSidePanel: emptyFn,
 | 
			
		||||
    showSidePanel: emptyFn,
 | 
			
		||||
    toggleSidePanel: emptyFn,
 | 
			
		||||
 | 
			
		||||
    isSelectSchemaOpen: false,
 | 
			
		||||
    openSelectSchema: emptyFn,
 | 
			
		||||
    closeSelectSchema: emptyFn,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -10,25 +10,36 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    const [openedTableInSidebar, setOpenedTableInSidebar] = React.useState<
 | 
			
		||||
        string | undefined
 | 
			
		||||
    >();
 | 
			
		||||
    const [openedRelationshipInSidebar, setOpenedRelationshipInSidebar] =
 | 
			
		||||
        React.useState<string | undefined>();
 | 
			
		||||
    const [openedDependencyInSidebar, setOpenedDependencyInSidebar] =
 | 
			
		||||
    const [openedRefInSidebar, setOpenedRefInSidebar] = React.useState<
 | 
			
		||||
        string | undefined
 | 
			
		||||
    >();
 | 
			
		||||
    const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
 | 
			
		||||
        string | undefined
 | 
			
		||||
    >();
 | 
			
		||||
    const [openedCustomTypeInSidebar, setOpenedCustomTypeInSidebar] =
 | 
			
		||||
        React.useState<string | undefined>();
 | 
			
		||||
    const [selectedSidebarSection, setSelectedSidebarSection] =
 | 
			
		||||
        React.useState<SidebarSection>('tables');
 | 
			
		||||
    const [isSidePanelShowed, setIsSidePanelShowed] =
 | 
			
		||||
        React.useState<boolean>(isDesktop);
 | 
			
		||||
    const [isSelectSchemaOpen, setIsSelectSchemaOpen] =
 | 
			
		||||
        React.useState<boolean>(false);
 | 
			
		||||
 | 
			
		||||
    const closeAllTablesInSidebar: LayoutContext['closeAllTablesInSidebar'] =
 | 
			
		||||
        () => setOpenedTableInSidebar('');
 | 
			
		||||
 | 
			
		||||
    const closeAllRelationshipsInSidebar: LayoutContext['closeAllRelationshipsInSidebar'] =
 | 
			
		||||
        () => setOpenedRelationshipInSidebar('');
 | 
			
		||||
        () => setOpenedRefInSidebar('');
 | 
			
		||||
 | 
			
		||||
    const closeAllDependenciesInSidebar: LayoutContext['closeAllDependenciesInSidebar'] =
 | 
			
		||||
        () => setOpenedDependencyInSidebar('');
 | 
			
		||||
        () => setOpenedRefInSidebar('');
 | 
			
		||||
 | 
			
		||||
    const closeAllRefsInSidebar: LayoutContext['closeAllRefsInSidebar'] = () =>
 | 
			
		||||
        setOpenedRefInSidebar('');
 | 
			
		||||
 | 
			
		||||
    const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
 | 
			
		||||
        () => setOpenedAreaInSidebar('');
 | 
			
		||||
 | 
			
		||||
    const closeAllCustomTypesInSidebar: LayoutContext['closeAllCustomTypesInSidebar'] =
 | 
			
		||||
        () => setOpenedCustomTypeInSidebar('');
 | 
			
		||||
 | 
			
		||||
    const hideSidePanel: LayoutContext['hideSidePanel'] = () =>
 | 
			
		||||
        setIsSidePanelShowed(false);
 | 
			
		||||
@@ -51,22 +62,38 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    const openRelationshipFromSidebar: LayoutContext['openRelationshipFromSidebar'] =
 | 
			
		||||
        (relationshipId) => {
 | 
			
		||||
            showSidePanel();
 | 
			
		||||
            setSelectedSidebarSection('relationships');
 | 
			
		||||
            setOpenedRelationshipInSidebar(relationshipId);
 | 
			
		||||
            setSelectedSidebarSection('refs');
 | 
			
		||||
            setOpenedRefInSidebar(relationshipId);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    const openDependencyFromSidebar: LayoutContext['openDependencyFromSidebar'] =
 | 
			
		||||
        (dependencyId) => {
 | 
			
		||||
            showSidePanel();
 | 
			
		||||
            setSelectedSidebarSection('dependencies');
 | 
			
		||||
            setOpenedDependencyInSidebar(dependencyId);
 | 
			
		||||
            setSelectedSidebarSection('refs');
 | 
			
		||||
            setOpenedRefInSidebar(dependencyId);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    const openSelectSchema: LayoutContext['openSelectSchema'] = () =>
 | 
			
		||||
        setIsSelectSchemaOpen(true);
 | 
			
		||||
    const openRefFromSidebar: LayoutContext['openRefFromSidebar'] = (refId) => {
 | 
			
		||||
        showSidePanel();
 | 
			
		||||
        setSelectedSidebarSection('refs');
 | 
			
		||||
        setOpenedRefInSidebar(refId);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const openAreaFromSidebar: LayoutContext['openAreaFromSidebar'] = (
 | 
			
		||||
        areaId
 | 
			
		||||
    ) => {
 | 
			
		||||
        showSidePanel();
 | 
			
		||||
        setSelectedSidebarSection('areas');
 | 
			
		||||
        setOpenedAreaInSidebar(areaId);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const openCustomTypeFromSidebar: LayoutContext['openCustomTypeFromSidebar'] =
 | 
			
		||||
        (customTypeId) => {
 | 
			
		||||
            showSidePanel();
 | 
			
		||||
            setSelectedSidebarSection('customTypes');
 | 
			
		||||
            setOpenedTableInSidebar(customTypeId);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    const closeSelectSchema: LayoutContext['closeSelectSchema'] = () =>
 | 
			
		||||
        setIsSelectSchemaOpen(false);
 | 
			
		||||
    return (
 | 
			
		||||
        <layoutContext.Provider
 | 
			
		||||
            value={{
 | 
			
		||||
@@ -74,7 +101,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                selectedSidebarSection,
 | 
			
		||||
                openTableFromSidebar,
 | 
			
		||||
                selectSidebarSection: setSelectedSidebarSection,
 | 
			
		||||
                openedRelationshipInSidebar,
 | 
			
		||||
                openRelationshipFromSidebar,
 | 
			
		||||
                closeAllTablesInSidebar,
 | 
			
		||||
                closeAllRelationshipsInSidebar,
 | 
			
		||||
@@ -82,12 +108,17 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                hideSidePanel,
 | 
			
		||||
                showSidePanel,
 | 
			
		||||
                toggleSidePanel,
 | 
			
		||||
                isSelectSchemaOpen,
 | 
			
		||||
                openSelectSchema,
 | 
			
		||||
                closeSelectSchema,
 | 
			
		||||
                openedDependencyInSidebar,
 | 
			
		||||
                openDependencyFromSidebar,
 | 
			
		||||
                closeAllDependenciesInSidebar,
 | 
			
		||||
                openedRefInSidebar,
 | 
			
		||||
                openRefFromSidebar,
 | 
			
		||||
                closeAllRefsInSidebar,
 | 
			
		||||
                openedAreaInSidebar,
 | 
			
		||||
                openAreaFromSidebar,
 | 
			
		||||
                closeAllAreasInSidebar,
 | 
			
		||||
                openedCustomTypeInSidebar,
 | 
			
		||||
                openCustomTypeFromSidebar,
 | 
			
		||||
                closeAllCustomTypesInSidebar,
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,6 @@ import type { Theme } from '../theme-context/theme-context';
 | 
			
		||||
 | 
			
		||||
export type ScrollAction = 'pan' | 'zoom';
 | 
			
		||||
 | 
			
		||||
export type SchemasFilter = Record<string, string[]>;
 | 
			
		||||
 | 
			
		||||
export interface LocalConfigContext {
 | 
			
		||||
    theme: Theme;
 | 
			
		||||
    setTheme: (theme: Theme) => void;
 | 
			
		||||
@@ -13,16 +11,14 @@ export interface LocalConfigContext {
 | 
			
		||||
    scrollAction: ScrollAction;
 | 
			
		||||
    setScrollAction: (action: ScrollAction) => void;
 | 
			
		||||
 | 
			
		||||
    schemasFilter: SchemasFilter;
 | 
			
		||||
    setSchemasFilter: React.Dispatch<React.SetStateAction<SchemasFilter>>;
 | 
			
		||||
    showDBViews: boolean;
 | 
			
		||||
    setShowDBViews: (showViews: boolean) => void;
 | 
			
		||||
 | 
			
		||||
    showCardinality: boolean;
 | 
			
		||||
    setShowCardinality: (showCardinality: boolean) => void;
 | 
			
		||||
 | 
			
		||||
    hideMultiSchemaNotification: boolean;
 | 
			
		||||
    setHideMultiSchemaNotification: (
 | 
			
		||||
        hideMultiSchemaNotification: boolean
 | 
			
		||||
    ) => void;
 | 
			
		||||
    showFieldAttributes: boolean;
 | 
			
		||||
    setShowFieldAttributes: (showFieldAttributes: boolean) => void;
 | 
			
		||||
 | 
			
		||||
    githubRepoOpened: boolean;
 | 
			
		||||
    setGithubRepoOpened: (githubRepoOpened: boolean) => void;
 | 
			
		||||
@@ -30,9 +26,6 @@ export interface LocalConfigContext {
 | 
			
		||||
    starUsDialogLastOpen: number;
 | 
			
		||||
    setStarUsDialogLastOpen: (lastOpen: number) => void;
 | 
			
		||||
 | 
			
		||||
    showDependenciesOnCanvas: boolean;
 | 
			
		||||
    setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
 | 
			
		||||
 | 
			
		||||
    showMiniMapOnCanvas: boolean;
 | 
			
		||||
    setShowMiniMapOnCanvas: (showMiniMapOnCanvas: boolean) => void;
 | 
			
		||||
}
 | 
			
		||||
@@ -44,14 +37,14 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
 | 
			
		||||
    scrollAction: 'pan',
 | 
			
		||||
    setScrollAction: emptyFn,
 | 
			
		||||
 | 
			
		||||
    schemasFilter: {},
 | 
			
		||||
    setSchemasFilter: emptyFn,
 | 
			
		||||
    showDBViews: false,
 | 
			
		||||
    setShowDBViews: emptyFn,
 | 
			
		||||
 | 
			
		||||
    showCardinality: true,
 | 
			
		||||
    setShowCardinality: emptyFn,
 | 
			
		||||
 | 
			
		||||
    hideMultiSchemaNotification: false,
 | 
			
		||||
    setHideMultiSchemaNotification: emptyFn,
 | 
			
		||||
    showFieldAttributes: true,
 | 
			
		||||
    setShowFieldAttributes: emptyFn,
 | 
			
		||||
 | 
			
		||||
    githubRepoOpened: false,
 | 
			
		||||
    setGithubRepoOpened: emptyFn,
 | 
			
		||||
@@ -59,9 +52,6 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
 | 
			
		||||
    starUsDialogLastOpen: 0,
 | 
			
		||||
    setStarUsDialogLastOpen: emptyFn,
 | 
			
		||||
 | 
			
		||||
    showDependenciesOnCanvas: false,
 | 
			
		||||
    setShowDependenciesOnCanvas: emptyFn,
 | 
			
		||||
 | 
			
		||||
    showMiniMapOnCanvas: false,
 | 
			
		||||
    setShowMiniMapOnCanvas: emptyFn,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,16 @@
 | 
			
		||||
import React, { useEffect } from 'react';
 | 
			
		||||
import type { SchemasFilter, ScrollAction } from './local-config-context';
 | 
			
		||||
import type { ScrollAction } from './local-config-context';
 | 
			
		||||
import { LocalConfigContext } from './local-config-context';
 | 
			
		||||
import type { Theme } from '../theme-context/theme-context';
 | 
			
		||||
 | 
			
		||||
const themeKey = 'theme';
 | 
			
		||||
const scrollActionKey = 'scroll_action';
 | 
			
		||||
const schemasFilterKey = 'schemas_filter';
 | 
			
		||||
const showCardinalityKey = 'show_cardinality';
 | 
			
		||||
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
 | 
			
		||||
const showFieldAttributesKey = 'show_field_attributes';
 | 
			
		||||
const githubRepoOpenedKey = 'github_repo_opened';
 | 
			
		||||
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
 | 
			
		||||
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
 | 
			
		||||
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
 | 
			
		||||
const showDBViewsKey = 'show_db_views';
 | 
			
		||||
 | 
			
		||||
export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
@@ -24,20 +23,17 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        (localStorage.getItem(scrollActionKey) as ScrollAction) || 'pan'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const [schemasFilter, setSchemasFilter] = React.useState<SchemasFilter>(
 | 
			
		||||
        JSON.parse(
 | 
			
		||||
            localStorage.getItem(schemasFilterKey) || '{}'
 | 
			
		||||
        ) as SchemasFilter
 | 
			
		||||
    const [showDBViews, setShowDBViews] = React.useState<boolean>(
 | 
			
		||||
        (localStorage.getItem(showDBViewsKey) || 'false') === 'true'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const [showCardinality, setShowCardinality] = React.useState<boolean>(
 | 
			
		||||
        (localStorage.getItem(showCardinalityKey) || 'true') === 'true'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
 | 
			
		||||
    const [showFieldAttributes, setShowFieldAttributes] =
 | 
			
		||||
        React.useState<boolean>(
 | 
			
		||||
            (localStorage.getItem(hideMultiSchemaNotificationKey) ||
 | 
			
		||||
                'false') === 'true'
 | 
			
		||||
            (localStorage.getItem(showFieldAttributesKey) || 'true') === 'true'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const [githubRepoOpened, setGithubRepoOpened] = React.useState<boolean>(
 | 
			
		||||
@@ -49,12 +45,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
 | 
			
		||||
        React.useState<boolean>(
 | 
			
		||||
            (localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
 | 
			
		||||
                'true'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const [showMiniMapOnCanvas, setShowMiniMapOnCanvas] =
 | 
			
		||||
        React.useState<boolean>(
 | 
			
		||||
            (localStorage.getItem(showMiniMapOnCanvasKey) || 'true') === 'true'
 | 
			
		||||
@@ -71,13 +61,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
 | 
			
		||||
    }, [githubRepoOpened]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        localStorage.setItem(
 | 
			
		||||
            hideMultiSchemaNotificationKey,
 | 
			
		||||
            hideMultiSchemaNotification.toString()
 | 
			
		||||
        );
 | 
			
		||||
    }, [hideMultiSchemaNotification]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        localStorage.setItem(themeKey, theme);
 | 
			
		||||
    }, [theme]);
 | 
			
		||||
@@ -87,20 +70,13 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    }, [scrollAction]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        localStorage.setItem(schemasFilterKey, JSON.stringify(schemasFilter));
 | 
			
		||||
    }, [schemasFilter]);
 | 
			
		||||
        localStorage.setItem(showDBViewsKey, showDBViews.toString());
 | 
			
		||||
    }, [showDBViews]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        localStorage.setItem(showCardinalityKey, showCardinality.toString());
 | 
			
		||||
    }, [showCardinality]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        localStorage.setItem(
 | 
			
		||||
            showDependenciesOnCanvasKey,
 | 
			
		||||
            showDependenciesOnCanvas.toString()
 | 
			
		||||
        );
 | 
			
		||||
    }, [showDependenciesOnCanvas]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        localStorage.setItem(
 | 
			
		||||
            showMiniMapOnCanvasKey,
 | 
			
		||||
@@ -115,18 +91,16 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                setTheme,
 | 
			
		||||
                scrollAction,
 | 
			
		||||
                setScrollAction,
 | 
			
		||||
                schemasFilter,
 | 
			
		||||
                setSchemasFilter,
 | 
			
		||||
                showDBViews,
 | 
			
		||||
                setShowDBViews,
 | 
			
		||||
                showCardinality,
 | 
			
		||||
                setShowCardinality,
 | 
			
		||||
                hideMultiSchemaNotification,
 | 
			
		||||
                setHideMultiSchemaNotification,
 | 
			
		||||
                showFieldAttributes,
 | 
			
		||||
                setShowFieldAttributes,
 | 
			
		||||
                setGithubRepoOpened,
 | 
			
		||||
                githubRepoOpened,
 | 
			
		||||
                starUsDialogLastOpen,
 | 
			
		||||
                setStarUsDialogLastOpen,
 | 
			
		||||
                showDependenciesOnCanvas,
 | 
			
		||||
                setShowDependenciesOnCanvas,
 | 
			
		||||
                showMiniMapOnCanvas,
 | 
			
		||||
                setShowMiniMapOnCanvas,
 | 
			
		||||
            }}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,18 +5,31 @@ import type { DBRelationship } from '@/lib/domain/db-relationship';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import type { ChartDBConfig } from '@/lib/domain/config';
 | 
			
		||||
import type { DBDependency } from '@/lib/domain/db-dependency';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
 | 
			
		||||
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
 | 
			
		||||
 | 
			
		||||
export interface StorageContext {
 | 
			
		||||
    // Config operations
 | 
			
		||||
    getConfig: () => Promise<ChartDBConfig | undefined>;
 | 
			
		||||
    updateConfig: (config: Partial<ChartDBConfig>) => Promise<void>;
 | 
			
		||||
 | 
			
		||||
    // Diagram filter operations
 | 
			
		||||
    getDiagramFilter: (diagramId: string) => Promise<DiagramFilter | undefined>;
 | 
			
		||||
    updateDiagramFilter: (
 | 
			
		||||
        diagramId: string,
 | 
			
		||||
        filter: DiagramFilter
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
    deleteDiagramFilter: (diagramId: string) => Promise<void>;
 | 
			
		||||
 | 
			
		||||
    // Diagram operations
 | 
			
		||||
    addDiagram: (params: { diagram: Diagram }) => Promise<void>;
 | 
			
		||||
    listDiagrams: (options?: {
 | 
			
		||||
        includeTables?: boolean;
 | 
			
		||||
        includeRelationships?: boolean;
 | 
			
		||||
        includeDependencies?: boolean;
 | 
			
		||||
        includeAreas?: boolean;
 | 
			
		||||
        includeCustomTypes?: boolean;
 | 
			
		||||
    }) => Promise<Diagram[]>;
 | 
			
		||||
    getDiagram: (
 | 
			
		||||
        id: string,
 | 
			
		||||
@@ -24,6 +37,8 @@ export interface StorageContext {
 | 
			
		||||
            includeTables?: boolean;
 | 
			
		||||
            includeRelationships?: boolean;
 | 
			
		||||
            includeDependencies?: boolean;
 | 
			
		||||
            includeAreas?: boolean;
 | 
			
		||||
            includeCustomTypes?: boolean;
 | 
			
		||||
        }
 | 
			
		||||
    ) => Promise<Diagram | undefined>;
 | 
			
		||||
    updateDiagram: (params: {
 | 
			
		||||
@@ -86,12 +101,50 @@ export interface StorageContext {
 | 
			
		||||
    }) => Promise<void>;
 | 
			
		||||
    listDependencies: (diagramId: string) => Promise<DBDependency[]>;
 | 
			
		||||
    deleteDiagramDependencies: (diagramId: string) => Promise<void>;
 | 
			
		||||
 | 
			
		||||
    // Area operations
 | 
			
		||||
    addArea: (params: { diagramId: string; area: Area }) => Promise<void>;
 | 
			
		||||
    getArea: (params: {
 | 
			
		||||
        diagramId: string;
 | 
			
		||||
        id: string;
 | 
			
		||||
    }) => Promise<Area | undefined>;
 | 
			
		||||
    updateArea: (params: {
 | 
			
		||||
        id: string;
 | 
			
		||||
        attributes: Partial<Area>;
 | 
			
		||||
    }) => Promise<void>;
 | 
			
		||||
    deleteArea: (params: { diagramId: string; id: string }) => Promise<void>;
 | 
			
		||||
    listAreas: (diagramId: string) => Promise<Area[]>;
 | 
			
		||||
    deleteDiagramAreas: (diagramId: string) => Promise<void>;
 | 
			
		||||
 | 
			
		||||
    // Custom type operations
 | 
			
		||||
    addCustomType: (params: {
 | 
			
		||||
        diagramId: string;
 | 
			
		||||
        customType: DBCustomType;
 | 
			
		||||
    }) => Promise<void>;
 | 
			
		||||
    getCustomType: (params: {
 | 
			
		||||
        diagramId: string;
 | 
			
		||||
        id: string;
 | 
			
		||||
    }) => Promise<DBCustomType | undefined>;
 | 
			
		||||
    updateCustomType: (params: {
 | 
			
		||||
        id: string;
 | 
			
		||||
        attributes: Partial<DBCustomType>;
 | 
			
		||||
    }) => Promise<void>;
 | 
			
		||||
    deleteCustomType: (params: {
 | 
			
		||||
        diagramId: string;
 | 
			
		||||
        id: string;
 | 
			
		||||
    }) => Promise<void>;
 | 
			
		||||
    listCustomTypes: (diagramId: string) => Promise<DBCustomType[]>;
 | 
			
		||||
    deleteDiagramCustomTypes: (diagramId: string) => Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const storageInitialValue: StorageContext = {
 | 
			
		||||
    getConfig: emptyFn,
 | 
			
		||||
    updateConfig: emptyFn,
 | 
			
		||||
 | 
			
		||||
    getDiagramFilter: emptyFn,
 | 
			
		||||
    updateDiagramFilter: emptyFn,
 | 
			
		||||
    deleteDiagramFilter: emptyFn,
 | 
			
		||||
 | 
			
		||||
    addDiagram: emptyFn,
 | 
			
		||||
    listDiagrams: emptyFn,
 | 
			
		||||
    getDiagram: emptyFn,
 | 
			
		||||
@@ -119,6 +172,21 @@ export const storageInitialValue: StorageContext = {
 | 
			
		||||
    deleteDependency: emptyFn,
 | 
			
		||||
    listDependencies: emptyFn,
 | 
			
		||||
    deleteDiagramDependencies: emptyFn,
 | 
			
		||||
 | 
			
		||||
    addArea: emptyFn,
 | 
			
		||||
    getArea: emptyFn,
 | 
			
		||||
    updateArea: emptyFn,
 | 
			
		||||
    deleteArea: emptyFn,
 | 
			
		||||
    listAreas: emptyFn,
 | 
			
		||||
    deleteDiagramAreas: emptyFn,
 | 
			
		||||
 | 
			
		||||
    // Custom type operations
 | 
			
		||||
    addCustomType: emptyFn,
 | 
			
		||||
    getCustomType: emptyFn,
 | 
			
		||||
    updateCustomType: emptyFn,
 | 
			
		||||
    deleteCustomType: emptyFn,
 | 
			
		||||
    listCustomTypes: emptyFn,
 | 
			
		||||
    deleteDiagramCustomTypes: emptyFn,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const storageContext =
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,7 @@
 | 
			
		||||
import { createContext } from 'react';
 | 
			
		||||
import { emptyFn } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
export type Theme = 'light' | 'dark' | 'system';
 | 
			
		||||
export type EffectiveTheme = Exclude<Theme, 'system'>;
 | 
			
		||||
import type { Theme, EffectiveTheme } from '@/lib/types';
 | 
			
		||||
export type { Theme, EffectiveTheme };
 | 
			
		||||
 | 
			
		||||
export interface ThemeContext {
 | 
			
		||||
    theme: Theme;
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,7 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        handleThemeToggle,
 | 
			
		||||
        {
 | 
			
		||||
            preventDefault: true,
 | 
			
		||||
            enableOnFormTags: true,
 | 
			
		||||
        },
 | 
			
		||||
        [handleThemeToggle]
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,10 @@
 | 
			
		||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import React, {
 | 
			
		||||
    Suspense,
 | 
			
		||||
    useCallback,
 | 
			
		||||
    useEffect,
 | 
			
		||||
    useState,
 | 
			
		||||
    useRef,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import {
 | 
			
		||||
    DialogClose,
 | 
			
		||||
@@ -8,32 +14,10 @@ import {
 | 
			
		||||
    DialogInternalContent,
 | 
			
		||||
    DialogTitle,
 | 
			
		||||
} from '@/components/dialog/dialog';
 | 
			
		||||
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import { databaseSecondaryLogoMap } from '@/lib/databases';
 | 
			
		||||
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
 | 
			
		||||
import { Textarea } from '@/components/textarea/textarea';
 | 
			
		||||
import type { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import { Editor } from '@/components/code-snippet/code-snippet';
 | 
			
		||||
import type { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
			
		||||
import {
 | 
			
		||||
    databaseEditionToImageMap,
 | 
			
		||||
    databaseEditionToLabelMap,
 | 
			
		||||
    databaseTypeToEditionMap,
 | 
			
		||||
} from '@/lib/domain/database-edition';
 | 
			
		||||
import {
 | 
			
		||||
    Avatar,
 | 
			
		||||
    AvatarFallback,
 | 
			
		||||
    AvatarImage,
 | 
			
		||||
} from '@/components/avatar/avatar';
 | 
			
		||||
import { SSMSInfo } from './ssms-info/ssms-info';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Tabs, TabsList, TabsTrigger } from '@/components/tabs/tabs';
 | 
			
		||||
import type { DatabaseClient } from '@/lib/domain/database-clients';
 | 
			
		||||
import {
 | 
			
		||||
    databaseClientToLabelMap,
 | 
			
		||||
    databaseTypeToClientsMap,
 | 
			
		||||
    databaseEditionToClientsMap,
 | 
			
		||||
} from '@/lib/domain/database-clients';
 | 
			
		||||
import type { ImportMetadataScripts } from '@/lib/data/import-metadata/scripts/scripts';
 | 
			
		||||
import { ZoomableImage } from '@/components/zoomable-image/zoomable-image';
 | 
			
		||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
 | 
			
		||||
import { Spinner } from '@/components/spinner/spinner';
 | 
			
		||||
@@ -41,9 +25,43 @@ import {
 | 
			
		||||
    fixMetadataJson,
 | 
			
		||||
    isStringMetadataJson,
 | 
			
		||||
} from '@/lib/data/import-metadata/utils';
 | 
			
		||||
import {
 | 
			
		||||
    ResizableHandle,
 | 
			
		||||
    ResizablePanel,
 | 
			
		||||
    ResizablePanelGroup,
 | 
			
		||||
} from '@/components/resizable/resizable';
 | 
			
		||||
import { useTheme } from '@/hooks/use-theme';
 | 
			
		||||
import type { OnChange } from '@monaco-editor/react';
 | 
			
		||||
import { useDebounce } from '@/hooks/use-debounce-v2';
 | 
			
		||||
import { InstructionsSection } from './instructions-section/instructions-section';
 | 
			
		||||
import { parseSQLError } from '@/lib/data/sql-import';
 | 
			
		||||
import type { editor, IDisposable } from 'monaco-editor';
 | 
			
		||||
import { waitFor } from '@/lib/utils';
 | 
			
		||||
import {
 | 
			
		||||
    validateSQL,
 | 
			
		||||
    type ValidationResult,
 | 
			
		||||
} from '@/lib/data/sql-import/sql-validator';
 | 
			
		||||
import { SQLValidationStatus } from './sql-validation-status';
 | 
			
		||||
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
 | 
			
		||||
import type { ImportMethod } from '@/lib/import-method/import-method';
 | 
			
		||||
import { detectImportMethod } from '@/lib/import-method/detect-import-method';
 | 
			
		||||
import { verifyDBML } from '@/lib/dbml/dbml-import/verify-dbml';
 | 
			
		||||
import {
 | 
			
		||||
    clearErrorHighlight,
 | 
			
		||||
    highlightErrorLine,
 | 
			
		||||
} from '@/components/code-snippet/dbml/utils';
 | 
			
		||||
 | 
			
		||||
const calculateContentSizeMB = (content: string): number => {
 | 
			
		||||
    return content.length / (1024 * 1024); // Convert to MB
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const calculateIsLargeFile = (content: string): boolean => {
 | 
			
		||||
    const contentSizeMB = calculateContentSizeMB(content);
 | 
			
		||||
    return contentSizeMB > 2; // Consider large if over 2MB
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const errorScriptOutputMessage =
 | 
			
		||||
    'Invalid JSON. Please correct it or contact us at chartdb.io@gmail.com for help.';
 | 
			
		||||
    'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.';
 | 
			
		||||
 | 
			
		||||
export interface ImportDatabaseProps {
 | 
			
		||||
    goBack?: () => void;
 | 
			
		||||
@@ -58,6 +76,8 @@ export interface ImportDatabaseProps {
 | 
			
		||||
    >;
 | 
			
		||||
    keepDialogAfterImport?: boolean;
 | 
			
		||||
    title: string;
 | 
			
		||||
    importMethod: ImportMethod;
 | 
			
		||||
    setImportMethod: (method: ImportMethod) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
@@ -71,42 +91,131 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
    setDatabaseEdition,
 | 
			
		||||
    keepDialogAfterImport,
 | 
			
		||||
    title,
 | 
			
		||||
    importMethod,
 | 
			
		||||
    setImportMethod,
 | 
			
		||||
}) => {
 | 
			
		||||
    const databaseClients = useMemo(
 | 
			
		||||
        () => [
 | 
			
		||||
            ...databaseTypeToClientsMap[databaseType],
 | 
			
		||||
            ...(databaseEdition
 | 
			
		||||
                ? databaseEditionToClientsMap[databaseEdition]
 | 
			
		||||
                : []),
 | 
			
		||||
        ],
 | 
			
		||||
        [databaseType, databaseEdition]
 | 
			
		||||
    );
 | 
			
		||||
    const { effectiveTheme } = useTheme();
 | 
			
		||||
    const [errorMessage, setErrorMessage] = useState('');
 | 
			
		||||
    const [databaseClient, setDatabaseClient] = useState<
 | 
			
		||||
        DatabaseClient | undefined
 | 
			
		||||
    >();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const [importMetadataScripts, setImportMetadataScripts] =
 | 
			
		||||
        useState<ImportMetadataScripts | null>(null);
 | 
			
		||||
    const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
 | 
			
		||||
    const decorationsCollection = useRef<editor.IEditorDecorationsCollection>();
 | 
			
		||||
    const pasteDisposableRef = useRef<IDisposable | null>(null);
 | 
			
		||||
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { isSm: isDesktop } = useBreakpoint('sm');
 | 
			
		||||
 | 
			
		||||
    const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
 | 
			
		||||
    const [isCheckingJson, setIsCheckingJson] = useState(false);
 | 
			
		||||
 | 
			
		||||
    const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false);
 | 
			
		||||
    const [sqlValidation, setSqlValidation] = useState<ValidationResult | null>(
 | 
			
		||||
        null
 | 
			
		||||
    );
 | 
			
		||||
    const [isAutoFixing, setIsAutoFixing] = useState(false);
 | 
			
		||||
    const [showAutoFixButton, setShowAutoFixButton] = useState(false);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        const loadScripts = async () => {
 | 
			
		||||
            const { importMetadataScripts } = await import(
 | 
			
		||||
                '@/lib/data/import-metadata/scripts/scripts'
 | 
			
		||||
            );
 | 
			
		||||
            setImportMetadataScripts(importMetadataScripts);
 | 
			
		||||
        };
 | 
			
		||||
        loadScripts();
 | 
			
		||||
    const clearDecorations = useCallback(() => {
 | 
			
		||||
        clearErrorHighlight(decorationsCollection.current);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setScriptResult('');
 | 
			
		||||
        setErrorMessage('');
 | 
			
		||||
        setShowCheckJsonButton(false);
 | 
			
		||||
    }, [importMethod, setScriptResult]);
 | 
			
		||||
 | 
			
		||||
    // Check if the ddl or dbml is valid
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        clearDecorations();
 | 
			
		||||
        if (importMethod === 'query') {
 | 
			
		||||
            setSqlValidation(null);
 | 
			
		||||
            setShowAutoFixButton(false);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!scriptResult.trim()) {
 | 
			
		||||
            setSqlValidation(null);
 | 
			
		||||
            setShowAutoFixButton(false);
 | 
			
		||||
            setErrorMessage('');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (importMethod === 'dbml') {
 | 
			
		||||
            // Validate DBML by parsing it
 | 
			
		||||
            const validateResponse = verifyDBML(scriptResult);
 | 
			
		||||
            if (!validateResponse.hasError) {
 | 
			
		||||
                setErrorMessage('');
 | 
			
		||||
                setSqlValidation({
 | 
			
		||||
                    isValid: true,
 | 
			
		||||
                    errors: [],
 | 
			
		||||
                    warnings: [],
 | 
			
		||||
                });
 | 
			
		||||
            } else {
 | 
			
		||||
                let errorMsg = 'Invalid DBML syntax';
 | 
			
		||||
                let line: number = 1;
 | 
			
		||||
 | 
			
		||||
                if (validateResponse.parsedError) {
 | 
			
		||||
                    errorMsg = validateResponse.parsedError.message;
 | 
			
		||||
                    line = validateResponse.parsedError.line;
 | 
			
		||||
                    highlightErrorLine({
 | 
			
		||||
                        error: validateResponse.parsedError,
 | 
			
		||||
                        model: editorRef.current?.getModel(),
 | 
			
		||||
                        editorDecorationsCollection:
 | 
			
		||||
                            decorationsCollection.current,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                setSqlValidation({
 | 
			
		||||
                    isValid: false,
 | 
			
		||||
                    errors: [
 | 
			
		||||
                        {
 | 
			
		||||
                            message: errorMsg,
 | 
			
		||||
                            line: line,
 | 
			
		||||
                            type: 'syntax' as const,
 | 
			
		||||
                        },
 | 
			
		||||
                    ],
 | 
			
		||||
                    warnings: [],
 | 
			
		||||
                });
 | 
			
		||||
                setErrorMessage(errorMsg);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            setShowAutoFixButton(false);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // SQL validation
 | 
			
		||||
        // 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, clearDecorations]);
 | 
			
		||||
 | 
			
		||||
    // Check if the script result is a valid JSON
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (importMethod !== 'query') {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (scriptResult.trim().length === 0) {
 | 
			
		||||
            setErrorMessage('');
 | 
			
		||||
            setShowCheckJsonButton(false);
 | 
			
		||||
@@ -126,7 +235,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
            setErrorMessage(errorScriptOutputMessage);
 | 
			
		||||
            setShowCheckJsonButton(false);
 | 
			
		||||
        }
 | 
			
		||||
    }, [scriptResult]);
 | 
			
		||||
    }, [scriptResult, importMethod]);
 | 
			
		||||
 | 
			
		||||
    const handleImport = useCallback(() => {
 | 
			
		||||
        if (errorMessage.length === 0 && scriptResult.trim().length !== 0) {
 | 
			
		||||
@@ -134,35 +243,152 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
        }
 | 
			
		||||
    }, [errorMessage.length, onImport, scriptResult]);
 | 
			
		||||
 | 
			
		||||
    const handleInputChange = useCallback(
 | 
			
		||||
        (e: React.ChangeEvent<HTMLTextAreaElement>) => {
 | 
			
		||||
            const inputValue = e.target.value;
 | 
			
		||||
            setScriptResult(inputValue);
 | 
			
		||||
    const handleAutoFix = useCallback(() => {
 | 
			
		||||
        if (sqlValidation?.fixedSQL) {
 | 
			
		||||
            setIsAutoFixing(true);
 | 
			
		||||
            setShowAutoFixButton(false);
 | 
			
		||||
            setErrorMessage('');
 | 
			
		||||
 | 
			
		||||
            // Apply the fix with a delay so user sees the fixing message
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                setScriptResult(sqlValidation.fixedSQL!);
 | 
			
		||||
 | 
			
		||||
                setTimeout(() => {
 | 
			
		||||
                    setIsAutoFixing(false);
 | 
			
		||||
                }, 100);
 | 
			
		||||
            }, 1000);
 | 
			
		||||
        }
 | 
			
		||||
    }, [sqlValidation, setScriptResult]);
 | 
			
		||||
 | 
			
		||||
    const handleErrorClick = useCallback((line: number) => {
 | 
			
		||||
        if (editorRef.current) {
 | 
			
		||||
            // Set cursor to the error line
 | 
			
		||||
            editorRef.current.setPosition({ lineNumber: line, column: 1 });
 | 
			
		||||
            editorRef.current.revealLineInCenter(line);
 | 
			
		||||
            editorRef.current.focus();
 | 
			
		||||
        }
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const formatEditor = useCallback(() => {
 | 
			
		||||
        if (editorRef.current) {
 | 
			
		||||
            const model = editorRef.current.getModel();
 | 
			
		||||
            if (model) {
 | 
			
		||||
                const content = model.getValue();
 | 
			
		||||
 | 
			
		||||
                // Skip formatting for large files (> 2MB)
 | 
			
		||||
                if (calculateIsLargeFile(content)) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                editorRef.current
 | 
			
		||||
                    ?.getAction('editor.action.formatDocument')
 | 
			
		||||
                    ?.run();
 | 
			
		||||
            }, 50);
 | 
			
		||||
        }
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const handleInputChange: OnChange = useCallback(
 | 
			
		||||
        (inputValue) => {
 | 
			
		||||
            setScriptResult(inputValue ?? '');
 | 
			
		||||
 | 
			
		||||
            // Automatically open SSMS info when input length is exactly 65535
 | 
			
		||||
            if (inputValue.length === 65535) {
 | 
			
		||||
            if ((inputValue ?? '').length === 65535) {
 | 
			
		||||
                setShowSSMSInfoDialog(true);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [setScriptResult]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const debouncedHandleInputChange = useDebounce(handleInputChange, 500);
 | 
			
		||||
 | 
			
		||||
    const handleCheckJson = useCallback(async () => {
 | 
			
		||||
        setIsCheckingJson(true);
 | 
			
		||||
 | 
			
		||||
        const fixedJson = await fixMetadataJson(scriptResult);
 | 
			
		||||
        await waitFor(1000);
 | 
			
		||||
        const fixedJson = fixMetadataJson(scriptResult);
 | 
			
		||||
 | 
			
		||||
        if (isStringMetadataJson(fixedJson)) {
 | 
			
		||||
            setScriptResult(fixedJson);
 | 
			
		||||
            setErrorMessage('');
 | 
			
		||||
            formatEditor();
 | 
			
		||||
        } else {
 | 
			
		||||
            setScriptResult(fixedJson);
 | 
			
		||||
            setErrorMessage(errorScriptOutputMessage);
 | 
			
		||||
            formatEditor();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setShowCheckJsonButton(false);
 | 
			
		||||
        setIsCheckingJson(false);
 | 
			
		||||
    }, [scriptResult, setScriptResult]);
 | 
			
		||||
    }, [scriptResult, setScriptResult, formatEditor]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        // Cleanup paste handler on unmount
 | 
			
		||||
        return () => {
 | 
			
		||||
            if (pasteDisposableRef.current) {
 | 
			
		||||
                pasteDisposableRef.current.dispose();
 | 
			
		||||
                pasteDisposableRef.current = null;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const handleEditorDidMount = useCallback(
 | 
			
		||||
        (editor: editor.IStandaloneCodeEditor) => {
 | 
			
		||||
            editorRef.current = editor;
 | 
			
		||||
            decorationsCollection.current =
 | 
			
		||||
                editor.createDecorationsCollection();
 | 
			
		||||
 | 
			
		||||
            // 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 = detectImportMethod(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 and DBML modes, do NOT format as it can break the syntax
 | 
			
		||||
                } 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 and DBML modes or large files, do NOT format
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            pasteDisposableRef.current = disposable;
 | 
			
		||||
        },
 | 
			
		||||
        [importMethod, setImportMethod]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const renderHeader = useCallback(() => {
 | 
			
		||||
        return (
 | 
			
		||||
@@ -173,228 +399,147 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
        );
 | 
			
		||||
    }, [title]);
 | 
			
		||||
 | 
			
		||||
    const renderInstructions = useCallback(
 | 
			
		||||
        () => (
 | 
			
		||||
            <InstructionsSection
 | 
			
		||||
                databaseType={databaseType}
 | 
			
		||||
                importMethod={importMethod}
 | 
			
		||||
                setDatabaseEdition={setDatabaseEdition}
 | 
			
		||||
                setImportMethod={setImportMethod}
 | 
			
		||||
                databaseEdition={databaseEdition}
 | 
			
		||||
                setShowSSMSInfoDialog={setShowSSMSInfoDialog}
 | 
			
		||||
                showSSMSInfoDialog={showSSMSInfoDialog}
 | 
			
		||||
            />
 | 
			
		||||
        ),
 | 
			
		||||
        [
 | 
			
		||||
            databaseType,
 | 
			
		||||
            importMethod,
 | 
			
		||||
            setDatabaseEdition,
 | 
			
		||||
            setImportMethod,
 | 
			
		||||
            databaseEdition,
 | 
			
		||||
            setShowSSMSInfoDialog,
 | 
			
		||||
            showSSMSInfoDialog,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const renderOutputTextArea = useCallback(
 | 
			
		||||
        () => (
 | 
			
		||||
            <div className="flex size-full flex-col gap-1 overflow-hidden rounded-md border p-1">
 | 
			
		||||
                <div className="w-full text-center text-xs text-muted-foreground">
 | 
			
		||||
                    {importMethod === 'query'
 | 
			
		||||
                        ? 'Smart Query Output'
 | 
			
		||||
                        : importMethod === 'dbml'
 | 
			
		||||
                          ? 'DBML Script'
 | 
			
		||||
                          : 'SQL Script'}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="flex-1 overflow-hidden">
 | 
			
		||||
                    <Suspense fallback={<Spinner />}>
 | 
			
		||||
                        <Editor
 | 
			
		||||
                            value={scriptResult}
 | 
			
		||||
                            onChange={debouncedHandleInputChange}
 | 
			
		||||
                            language={
 | 
			
		||||
                                importMethod === 'query'
 | 
			
		||||
                                    ? 'json'
 | 
			
		||||
                                    : importMethod === 'dbml'
 | 
			
		||||
                                      ? 'dbml'
 | 
			
		||||
                                      : 'sql'
 | 
			
		||||
                            }
 | 
			
		||||
                            loading={<Spinner />}
 | 
			
		||||
                            onMount={handleEditorDidMount}
 | 
			
		||||
                            beforeMount={setupDBMLLanguage}
 | 
			
		||||
                            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,
 | 
			
		||||
                                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' || importMethod === 'dbml') &&
 | 
			
		||||
                    sqlValidation) ? (
 | 
			
		||||
                    <SQLValidationStatus
 | 
			
		||||
                        validation={sqlValidation}
 | 
			
		||||
                        errorMessage={errorMessage}
 | 
			
		||||
                        isAutoFixing={isAutoFixing}
 | 
			
		||||
                        onErrorClick={handleErrorClick}
 | 
			
		||||
                    />
 | 
			
		||||
                ) : null}
 | 
			
		||||
            </div>
 | 
			
		||||
        ),
 | 
			
		||||
        [
 | 
			
		||||
            errorMessage,
 | 
			
		||||
            scriptResult,
 | 
			
		||||
            importMethod,
 | 
			
		||||
            effectiveTheme,
 | 
			
		||||
            debouncedHandleInputChange,
 | 
			
		||||
            handleEditorDidMount,
 | 
			
		||||
            sqlValidation,
 | 
			
		||||
            isAutoFixing,
 | 
			
		||||
            handleErrorClick,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const renderContent = useCallback(() => {
 | 
			
		||||
        return (
 | 
			
		||||
            <DialogInternalContent>
 | 
			
		||||
                <div className="flex w-full flex-1 flex-col gap-6">
 | 
			
		||||
                    {databaseTypeToEditionMap[databaseType].length > 0 ? (
 | 
			
		||||
                        <div className="flex flex-col gap-1 md:flex-row">
 | 
			
		||||
                            <p className="text-sm leading-6 text-muted-foreground">
 | 
			
		||||
                                {t(
 | 
			
		||||
                                    'new_diagram_dialog.import_database.database_edition'
 | 
			
		||||
                                )}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <ToggleGroup
 | 
			
		||||
                                type="single"
 | 
			
		||||
                                className="ml-1 flex-wrap gap-2"
 | 
			
		||||
                                value={
 | 
			
		||||
                                    !databaseEdition
 | 
			
		||||
                                        ? 'regular'
 | 
			
		||||
                                        : databaseEdition
 | 
			
		||||
                                }
 | 
			
		||||
                                onValueChange={(value) => {
 | 
			
		||||
                                    setDatabaseEdition(
 | 
			
		||||
                                        value === 'regular'
 | 
			
		||||
                                            ? undefined
 | 
			
		||||
                                            : (value as DatabaseEdition)
 | 
			
		||||
                                    );
 | 
			
		||||
                                }}
 | 
			
		||||
                            >
 | 
			
		||||
                                <ToggleGroupItem
 | 
			
		||||
                                    value="regular"
 | 
			
		||||
                                    variant="outline"
 | 
			
		||||
                                    className="h-6 gap-1 p-0 px-2 shadow-none"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <Avatar className="size-4 rounded-none">
 | 
			
		||||
                                        <AvatarImage
 | 
			
		||||
                                            src={
 | 
			
		||||
                                                databaseSecondaryLogoMap[
 | 
			
		||||
                                                    databaseType
 | 
			
		||||
                                                ]
 | 
			
		||||
                                            }
 | 
			
		||||
                                            alt="Regular"
 | 
			
		||||
                                        />
 | 
			
		||||
                                        <AvatarFallback>Regular</AvatarFallback>
 | 
			
		||||
                                    </Avatar>
 | 
			
		||||
                                    Regular
 | 
			
		||||
                                </ToggleGroupItem>
 | 
			
		||||
                                {databaseTypeToEditionMap[databaseType].map(
 | 
			
		||||
                                    (edition) => (
 | 
			
		||||
                                        <ToggleGroupItem
 | 
			
		||||
                                            value={edition}
 | 
			
		||||
                                            key={edition}
 | 
			
		||||
                                            variant="outline"
 | 
			
		||||
                                            className="h-6 gap-1 p-0 px-2 shadow-none"
 | 
			
		||||
                                        >
 | 
			
		||||
                                            <Avatar className="size-4">
 | 
			
		||||
                                                <AvatarImage
 | 
			
		||||
                                                    src={
 | 
			
		||||
                                                        databaseEditionToImageMap[
 | 
			
		||||
                                                            edition
 | 
			
		||||
                                                        ]
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                    alt={
 | 
			
		||||
                                                        databaseEditionToLabelMap[
 | 
			
		||||
                                                            edition
 | 
			
		||||
                                                        ]
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                />
 | 
			
		||||
                                                <AvatarFallback>
 | 
			
		||||
                                                    {
 | 
			
		||||
                                                        databaseEditionToLabelMap[
 | 
			
		||||
                                                            edition
 | 
			
		||||
                                                        ]
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                </AvatarFallback>
 | 
			
		||||
                                            </Avatar>
 | 
			
		||||
                                            {databaseEditionToLabelMap[edition]}
 | 
			
		||||
                                        </ToggleGroupItem>
 | 
			
		||||
                                    )
 | 
			
		||||
                                )}
 | 
			
		||||
                            </ToggleGroup>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                    <div className="flex flex-col gap-1">
 | 
			
		||||
                        <div className="flex flex-col gap-1 text-sm text-muted-foreground md:flex-row md:justify-between">
 | 
			
		||||
                            <div>
 | 
			
		||||
                                1.{' '}
 | 
			
		||||
                                {t('new_diagram_dialog.import_database.step_1')}
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {databaseType === DatabaseType.SQL_SERVER && (
 | 
			
		||||
                                <SSMSInfo
 | 
			
		||||
                                    open={showSSMSInfoDialog}
 | 
			
		||||
                                    setOpen={setShowSSMSInfoDialog}
 | 
			
		||||
                                />
 | 
			
		||||
                            )}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {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"
 | 
			
		||||
                                    loading={!importMetadataScripts}
 | 
			
		||||
                                    code={
 | 
			
		||||
                                        importMetadataScripts?.[databaseType]?.(
 | 
			
		||||
                                            {
 | 
			
		||||
                                                databaseEdition,
 | 
			
		||||
                                                databaseClient,
 | 
			
		||||
                                            }
 | 
			
		||||
                                        ) ?? ''
 | 
			
		||||
                                    }
 | 
			
		||||
                                    language={databaseClient ? 'shell' : 'sql'}
 | 
			
		||||
                                />
 | 
			
		||||
                            </Tabs>
 | 
			
		||||
                        ) : (
 | 
			
		||||
                            <CodeSnippet
 | 
			
		||||
                                className="h-40 w-full flex-auto"
 | 
			
		||||
                                loading={!importMetadataScripts}
 | 
			
		||||
                                code={
 | 
			
		||||
                                    importMetadataScripts?.[databaseType]?.({
 | 
			
		||||
                                        databaseEdition,
 | 
			
		||||
                                    }) ?? ''
 | 
			
		||||
                                }
 | 
			
		||||
                                language="sql"
 | 
			
		||||
                            />
 | 
			
		||||
                        )}
 | 
			
		||||
                {isDesktop ? (
 | 
			
		||||
                    <ResizablePanelGroup
 | 
			
		||||
                        direction={isDesktop ? 'horizontal' : 'vertical'}
 | 
			
		||||
                        className="min-h-[500px]"
 | 
			
		||||
                    >
 | 
			
		||||
                        <ResizablePanel
 | 
			
		||||
                            defaultSize={25}
 | 
			
		||||
                            minSize={25}
 | 
			
		||||
                            maxSize={99}
 | 
			
		||||
                            className="min-h-fit rounded-md bg-gradient-to-b from-slate-50 to-slate-100 p-2 dark:from-slate-900 dark:to-slate-800 md:min-h-fit md:min-w-[350px] md:rounded-l-md md:p-2"
 | 
			
		||||
                        >
 | 
			
		||||
                            {renderInstructions()}
 | 
			
		||||
                        </ResizablePanel>
 | 
			
		||||
                        <ResizableHandle withHandle />
 | 
			
		||||
                        <ResizablePanel className="min-h-40 py-2 md:px-2 md:py-0">
 | 
			
		||||
                            {renderOutputTextArea()}
 | 
			
		||||
                        </ResizablePanel>
 | 
			
		||||
                    </ResizablePanelGroup>
 | 
			
		||||
                ) : (
 | 
			
		||||
                    <div className="flex flex-col gap-2">
 | 
			
		||||
                        {renderInstructions()}
 | 
			
		||||
                        {renderOutputTextArea()}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="flex h-48 flex-col gap-1">
 | 
			
		||||
                        <p className="text-sm text-muted-foreground">
 | 
			
		||||
                            2. {t('new_diagram_dialog.import_database.step_2')}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <Textarea
 | 
			
		||||
                            className="w-full flex-1 rounded-md bg-muted p-2 text-sm"
 | 
			
		||||
                            placeholder={t(
 | 
			
		||||
                                'new_diagram_dialog.import_database.script_results_placeholder'
 | 
			
		||||
                            )}
 | 
			
		||||
                            value={scriptResult}
 | 
			
		||||
                            onChange={handleInputChange}
 | 
			
		||||
                        />
 | 
			
		||||
                        {showCheckJsonButton || errorMessage ? (
 | 
			
		||||
                            <div className="mt-2 flex items-center gap-2">
 | 
			
		||||
                                {showCheckJsonButton ? (
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                        type="button"
 | 
			
		||||
                                        variant="outline"
 | 
			
		||||
                                        size="sm"
 | 
			
		||||
                                        onClick={handleCheckJson}
 | 
			
		||||
                                        disabled={isCheckingJson}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {isCheckingJson ? (
 | 
			
		||||
                                            <Spinner size="small" />
 | 
			
		||||
                                        ) : (
 | 
			
		||||
                                            t(
 | 
			
		||||
                                                'new_diagram_dialog.import_database.check_script_result'
 | 
			
		||||
                                            )
 | 
			
		||||
                                        )}
 | 
			
		||||
                                    </Button>
 | 
			
		||||
                                ) : (
 | 
			
		||||
                                    <p className="text-sm text-red-700">
 | 
			
		||||
                                        {errorMessage}
 | 
			
		||||
                                    </p>
 | 
			
		||||
                                )}
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ) : null}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                )}
 | 
			
		||||
            </DialogInternalContent>
 | 
			
		||||
        );
 | 
			
		||||
    }, [
 | 
			
		||||
        databaseEdition,
 | 
			
		||||
        databaseType,
 | 
			
		||||
        errorMessage,
 | 
			
		||||
        handleInputChange,
 | 
			
		||||
        scriptResult,
 | 
			
		||||
        setDatabaseEdition,
 | 
			
		||||
        databaseClients,
 | 
			
		||||
        databaseClient,
 | 
			
		||||
        importMetadataScripts,
 | 
			
		||||
        t,
 | 
			
		||||
        showCheckJsonButton,
 | 
			
		||||
        isCheckingJson,
 | 
			
		||||
        handleCheckJson,
 | 
			
		||||
        showSSMSInfoDialog,
 | 
			
		||||
        setShowSSMSInfoDialog,
 | 
			
		||||
    ]);
 | 
			
		||||
    }, [renderOutputTextArea, renderInstructions, isDesktop]);
 | 
			
		||||
 | 
			
		||||
    const renderFooter = useCallback(() => {
 | 
			
		||||
        return (
 | 
			
		||||
            <DialogFooter className="mt-4 flex !justify-between gap-2">
 | 
			
		||||
            <DialogFooter className="flex !justify-between gap-2">
 | 
			
		||||
                <div className="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
 | 
			
		||||
                    {goBack && (
 | 
			
		||||
                        <Button
 | 
			
		||||
@@ -428,13 +573,43 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
                        </DialogClose>
 | 
			
		||||
                    )}
 | 
			
		||||
 | 
			
		||||
                    {keepDialogAfterImport ? (
 | 
			
		||||
                    {showCheckJsonButton ? (
 | 
			
		||||
                        <Button
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            variant="default"
 | 
			
		||||
                            onClick={handleCheckJson}
 | 
			
		||||
                            disabled={isCheckingJson}
 | 
			
		||||
                        >
 | 
			
		||||
                            {isCheckingJson ? (
 | 
			
		||||
                                <Spinner size="small" />
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                t(
 | 
			
		||||
                                    'new_diagram_dialog.import_database.check_script_result'
 | 
			
		||||
                                )
 | 
			
		||||
                            )}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    ) : showAutoFixButton && importMethod === 'ddl' ? (
 | 
			
		||||
                        <Button
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            variant="secondary"
 | 
			
		||||
                            onClick={handleAutoFix}
 | 
			
		||||
                            disabled={isAutoFixing}
 | 
			
		||||
                            className="bg-sky-600 text-white hover:bg-sky-700"
 | 
			
		||||
                        >
 | 
			
		||||
                            {isAutoFixing ? (
 | 
			
		||||
                                <Spinner size="small" />
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                'Try auto-fix'
 | 
			
		||||
                            )}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    ) : keepDialogAfterImport ? (
 | 
			
		||||
                        <Button
 | 
			
		||||
                            type="button"
 | 
			
		||||
                            variant="default"
 | 
			
		||||
                            disabled={
 | 
			
		||||
                                scriptResult.trim().length === 0 ||
 | 
			
		||||
                                errorMessage.length > 0
 | 
			
		||||
                                errorMessage.length > 0 ||
 | 
			
		||||
                                isAutoFixing
 | 
			
		||||
                            }
 | 
			
		||||
                            onClick={handleImport}
 | 
			
		||||
                        >
 | 
			
		||||
@@ -446,9 +621,9 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
                                type="button"
 | 
			
		||||
                                variant="default"
 | 
			
		||||
                                disabled={
 | 
			
		||||
                                    showCheckJsonButton ||
 | 
			
		||||
                                    scriptResult.trim().length === 0 ||
 | 
			
		||||
                                    errorMessage.length > 0
 | 
			
		||||
                                    errorMessage.length > 0 ||
 | 
			
		||||
                                    isAutoFixing
 | 
			
		||||
                                }
 | 
			
		||||
                                onClick={handleImport}
 | 
			
		||||
                            >
 | 
			
		||||
@@ -477,8 +652,14 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
        errorMessage.length,
 | 
			
		||||
        scriptResult,
 | 
			
		||||
        showCheckJsonButton,
 | 
			
		||||
        isCheckingJson,
 | 
			
		||||
        handleCheckJson,
 | 
			
		||||
        goBack,
 | 
			
		||||
        t,
 | 
			
		||||
        importMethod,
 | 
			
		||||
        isAutoFixing,
 | 
			
		||||
        showAutoFixButton,
 | 
			
		||||
        handleAutoFix,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,196 @@
 | 
			
		||||
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, FileCode } from 'lucide-react';
 | 
			
		||||
import { SmartQueryInstructions } from './instructions/smart-query-instructions';
 | 
			
		||||
import { DDLInstructions } from './instructions/ddl-instructions';
 | 
			
		||||
import { DBMLInstructions } from './instructions/dbml-instructions';
 | 
			
		||||
import type { ImportMethod } from '@/lib/import-method/import-method';
 | 
			
		||||
 | 
			
		||||
const DatabasesWithoutDDLInstructions: DatabaseType[] = [
 | 
			
		||||
    DatabaseType.CLICKHOUSE,
 | 
			
		||||
    DatabaseType.ORACLE,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export interface InstructionsSectionProps {
 | 
			
		||||
    databaseType: DatabaseType;
 | 
			
		||||
    databaseEdition?: DatabaseEdition;
 | 
			
		||||
    setDatabaseEdition: React.Dispatch<
 | 
			
		||||
        React.SetStateAction<DatabaseEdition | undefined>
 | 
			
		||||
    >;
 | 
			
		||||
    importMethod: ImportMethod;
 | 
			
		||||
    setImportMethod: (method: ImportMethod) => 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: ImportMethod = 'query';
 | 
			
		||||
                            if (value) {
 | 
			
		||||
                                selectedImportMethod = value as ImportMethod;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            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">
 | 
			
		||||
                                <FileCode size={16} />
 | 
			
		||||
                            </Avatar>
 | 
			
		||||
                            SQL Script
 | 
			
		||||
                        </ToggleGroupItem>
 | 
			
		||||
                        <ToggleGroupItem
 | 
			
		||||
                            value="dbml"
 | 
			
		||||
                            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>
 | 
			
		||||
                            DBML
 | 
			
		||||
                        </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}
 | 
			
		||||
                    />
 | 
			
		||||
                ) : importMethod === 'ddl' ? (
 | 
			
		||||
                    <DDLInstructions
 | 
			
		||||
                        databaseType={databaseType}
 | 
			
		||||
                        databaseEdition={databaseEdition}
 | 
			
		||||
                    />
 | 
			
		||||
                ) : (
 | 
			
		||||
                    <DBMLInstructions
 | 
			
		||||
                        databaseType={databaseType}
 | 
			
		||||
                        databaseEdition={databaseEdition}
 | 
			
		||||
                    />
 | 
			
		||||
                )}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,47 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import type { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import type { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
			
		||||
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
 | 
			
		||||
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
 | 
			
		||||
 | 
			
		||||
export interface DBMLInstructionsProps {
 | 
			
		||||
    databaseType: DatabaseType;
 | 
			
		||||
    databaseEdition?: DatabaseEdition;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const DBMLInstructions: React.FC<DBMLInstructionsProps> = () => {
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <div className="flex flex-col gap-1 text-sm text-primary">
 | 
			
		||||
                <div>
 | 
			
		||||
                    Paste your DBML (Database Markup Language) schema definition
 | 
			
		||||
                    here →
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div className="flex h-64 flex-col gap-1 text-sm text-primary">
 | 
			
		||||
                <h4 className="text-xs font-medium">Example:</h4>
 | 
			
		||||
                <CodeSnippet
 | 
			
		||||
                    className="h-full"
 | 
			
		||||
                    allowCopy={false}
 | 
			
		||||
                    editorProps={{
 | 
			
		||||
                        beforeMount: setupDBMLLanguage,
 | 
			
		||||
                    }}
 | 
			
		||||
                    code={`Table users {
 | 
			
		||||
  id int [pk]
 | 
			
		||||
  username varchar
 | 
			
		||||
  email varchar
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table posts {
 | 
			
		||||
  id int [pk]
 | 
			
		||||
  user_id int [ref: > users.id]
 | 
			
		||||
  title varchar
 | 
			
		||||
  content text
 | 
			
		||||
}`}
 | 
			
		||||
                    language={'dbml'}
 | 
			
		||||
                />
 | 
			
		||||
            </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".schema" > <output_file_path>`,
 | 
			
		||||
            example: `sqlite3 my_db.db\n".schema" > schema_export.sql`,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            text: 'Open the exported SQL file, copy its contents, and paste them here.',
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    [DatabaseType.SQL_SERVER]: [
 | 
			
		||||
        {
 | 
			
		||||
            text: 'Download and install SQL Server Management Studio (SSMS).',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            text: 'Connect to your SQL Server instance using SSMS.',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            text: 'Right-click on the database you want to export and select Script Database as > CREATE To > New Query Editor Window.',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            text: 'Copy the generated script and paste it here.',
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    [DatabaseType.CLICKHOUSE]: [],
 | 
			
		||||
    [DatabaseType.COCKROACHDB]: [
 | 
			
		||||
        {
 | 
			
		||||
            text: 'Install pg_dump.',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            text: 'Execute the following command in your terminal (prefix with sudo on Linux if needed):',
 | 
			
		||||
            code: `pg_dump -h <host> -p <port> -d <database_name> \n  -U <username> -s -F p -E UTF-8 \n  -f <output_file_path>`,
 | 
			
		||||
            example: `pg_dump -h localhost -p 5432 -d my_db \n  -U postgres -s -F p -E UTF-8 \n  -f schema_export.sql`,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            text: 'Open the exported SQL file, copy its contents, and paste them here.',
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    [DatabaseType.MARIADB]: [
 | 
			
		||||
        {
 | 
			
		||||
            text: 'Install mysqldump.',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            text: 'Execute the following command in your terminal (prefix with sudo on Linux if needed):',
 | 
			
		||||
            code: `mysqldump -h <host> -u <username>\n-P <port> -p --no-data\n<database_name> > <output_path>`,
 | 
			
		||||
            example: `mysqldump -h localhost -u root -P\n3306 -p --no-data my_db >\nschema_export.sql`,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            text: 'Open the exported SQL file, copy its contents, and paste them here.',
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    [DatabaseType.ORACLE]: [],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface DDLInstructionsProps {
 | 
			
		||||
    databaseType: DatabaseType;
 | 
			
		||||
    databaseEdition?: DatabaseEdition;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const DDLInstructions: React.FC<DDLInstructionsProps> = ({
 | 
			
		||||
    databaseType,
 | 
			
		||||
}) => {
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            {DDLInstructionsMap[databaseType].map((instruction, index) => (
 | 
			
		||||
                <DDLInstructionStep
 | 
			
		||||
                    key={index}
 | 
			
		||||
                    index={index + 1}
 | 
			
		||||
                    text={instruction.text}
 | 
			
		||||
                    code={instruction.code}
 | 
			
		||||
                    example={instruction.example}
 | 
			
		||||
                />
 | 
			
		||||
            ))}
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,147 @@
 | 
			
		||||
import React, { useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
 | 
			
		||||
import type { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
			
		||||
import { SSMSInfo } from './ssms-info/ssms-info';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Tabs, TabsList, TabsTrigger } from '@/components/tabs/tabs';
 | 
			
		||||
import type { DatabaseClient } from '@/lib/domain/database-clients';
 | 
			
		||||
import { minimizeQuery } from '@/lib/data/import-metadata/utils';
 | 
			
		||||
import {
 | 
			
		||||
    databaseClientToLabelMap,
 | 
			
		||||
    databaseTypeToClientsMap,
 | 
			
		||||
    databaseEditionToClientsMap,
 | 
			
		||||
} from '@/lib/domain/database-clients';
 | 
			
		||||
import type { ImportMetadataScripts } from '@/lib/data/import-metadata/scripts/scripts';
 | 
			
		||||
 | 
			
		||||
export interface SmartQueryInstructionsProps {
 | 
			
		||||
    databaseType: DatabaseType;
 | 
			
		||||
    databaseEdition?: DatabaseEdition;
 | 
			
		||||
    showSSMSInfoDialog: boolean;
 | 
			
		||||
    setShowSSMSInfoDialog: (show: boolean) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SmartQueryInstructions: React.FC<SmartQueryInstructionsProps> = ({
 | 
			
		||||
    databaseType,
 | 
			
		||||
    databaseEdition,
 | 
			
		||||
    showSSMSInfoDialog,
 | 
			
		||||
    setShowSSMSInfoDialog,
 | 
			
		||||
}) => {
 | 
			
		||||
    const databaseClients = useMemo(
 | 
			
		||||
        () => [
 | 
			
		||||
            ...databaseTypeToClientsMap[databaseType],
 | 
			
		||||
            ...(databaseEdition
 | 
			
		||||
                ? databaseEditionToClientsMap[databaseEdition]
 | 
			
		||||
                : []),
 | 
			
		||||
        ],
 | 
			
		||||
        [databaseType, databaseEdition]
 | 
			
		||||
    );
 | 
			
		||||
    const [databaseClient, setDatabaseClient] = useState<
 | 
			
		||||
        DatabaseClient | undefined
 | 
			
		||||
    >();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const [importMetadataScripts, setImportMetadataScripts] =
 | 
			
		||||
        useState<ImportMetadataScripts | null>(null);
 | 
			
		||||
 | 
			
		||||
    const code = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
            (databaseClients.length > 0
 | 
			
		||||
                ? importMetadataScripts?.[databaseType]?.({
 | 
			
		||||
                      databaseEdition,
 | 
			
		||||
                      databaseClient,
 | 
			
		||||
                  })
 | 
			
		||||
                : importMetadataScripts?.[databaseType]?.({
 | 
			
		||||
                      databaseEdition,
 | 
			
		||||
                  })) ?? '',
 | 
			
		||||
        [
 | 
			
		||||
            databaseType,
 | 
			
		||||
            databaseEdition,
 | 
			
		||||
            databaseClients,
 | 
			
		||||
            importMetadataScripts,
 | 
			
		||||
            databaseClient,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        const loadScripts = async () => {
 | 
			
		||||
            const { importMetadataScripts } = await import(
 | 
			
		||||
                '@/lib/data/import-metadata/scripts/scripts'
 | 
			
		||||
            );
 | 
			
		||||
            setImportMetadataScripts(importMetadataScripts);
 | 
			
		||||
        };
 | 
			
		||||
        loadScripts();
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <div className="flex flex-col gap-1">
 | 
			
		||||
                <div className="flex flex-col gap-1 text-sm text-primary">
 | 
			
		||||
                    <div>
 | 
			
		||||
                        <span className="font-medium">1.</span>{' '}
 | 
			
		||||
                        {t('new_diagram_dialog.import_database.step_1')}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {databaseType === DatabaseType.SQL_SERVER && (
 | 
			
		||||
                        <SSMSInfo
 | 
			
		||||
                            open={showSSMSInfoDialog}
 | 
			
		||||
                            setOpen={setShowSSMSInfoDialog}
 | 
			
		||||
                        />
 | 
			
		||||
                    )}
 | 
			
		||||
                </div>
 | 
			
		||||
                {databaseClients.length > 0 ? (
 | 
			
		||||
                    <Tabs
 | 
			
		||||
                        value={!databaseClient ? 'dbclient' : databaseClient}
 | 
			
		||||
                        onValueChange={(value) => {
 | 
			
		||||
                            setDatabaseClient(
 | 
			
		||||
                                value === 'dbclient'
 | 
			
		||||
                                    ? undefined
 | 
			
		||||
                                    : (value as DatabaseClient)
 | 
			
		||||
                            );
 | 
			
		||||
                        }}
 | 
			
		||||
                    >
 | 
			
		||||
                        <div className="flex flex-1">
 | 
			
		||||
                            <TabsList className="h-8 justify-start rounded-none rounded-t-sm ">
 | 
			
		||||
                                <TabsTrigger
 | 
			
		||||
                                    value="dbclient"
 | 
			
		||||
                                    className="h-6 w-20"
 | 
			
		||||
                                >
 | 
			
		||||
                                    DB Client
 | 
			
		||||
                                </TabsTrigger>
 | 
			
		||||
 | 
			
		||||
                                {databaseClients?.map((client) => (
 | 
			
		||||
                                    <TabsTrigger
 | 
			
		||||
                                        key={client}
 | 
			
		||||
                                        value={client}
 | 
			
		||||
                                        className="h-6 !w-20"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {databaseClientToLabelMap[client]}
 | 
			
		||||
                                    </TabsTrigger>
 | 
			
		||||
                                )) ?? []}
 | 
			
		||||
                            </TabsList>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <CodeSnippet
 | 
			
		||||
                            className="h-40 w-full md:h-[200px]"
 | 
			
		||||
                            loading={!importMetadataScripts}
 | 
			
		||||
                            code={minimizeQuery(code)}
 | 
			
		||||
                            codeToCopy={code}
 | 
			
		||||
                            language={databaseClient ? 'shell' : 'sql'}
 | 
			
		||||
                        />
 | 
			
		||||
                    </Tabs>
 | 
			
		||||
                ) : (
 | 
			
		||||
                    <CodeSnippet
 | 
			
		||||
                        className="h-40 w-full flex-auto md:h-[200px]"
 | 
			
		||||
                        loading={!importMetadataScripts}
 | 
			
		||||
                        code={minimizeQuery(code)}
 | 
			
		||||
                        codeToCopy={code}
 | 
			
		||||
                        language="sql"
 | 
			
		||||
                    />
 | 
			
		||||
                )}
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="flex flex-col gap-1">
 | 
			
		||||
                <p className="text-sm text-primary">
 | 
			
		||||
                    <span className="font-medium">2.</span>{' '}
 | 
			
		||||
                    {t('new_diagram_dialog.import_database.step_2')}
 | 
			
		||||
                </p>
 | 
			
		||||
            </div>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										179
									
								
								src/dialogs/common/import-database/sql-validation-status.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,179 @@
 | 
			
		||||
import React, { useMemo } from 'react';
 | 
			
		||||
import { CheckCircle, AlertTriangle, MessageCircleWarning } from 'lucide-react';
 | 
			
		||||
import { Alert, AlertDescription } from '@/components/alert/alert';
 | 
			
		||||
import type { ValidationResult } from '@/lib/data/sql-import/sql-validator';
 | 
			
		||||
import { Separator } from '@/components/separator/separator';
 | 
			
		||||
import { ScrollArea } from '@/components/scroll-area/scroll-area';
 | 
			
		||||
import { Spinner } from '@/components/spinner/spinner';
 | 
			
		||||
 | 
			
		||||
interface SQLValidationStatusProps {
 | 
			
		||||
    validation?: ValidationResult | null;
 | 
			
		||||
    errorMessage: string;
 | 
			
		||||
    isAutoFixing?: boolean;
 | 
			
		||||
    onErrorClick?: (line: number) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({
 | 
			
		||||
    validation,
 | 
			
		||||
    errorMessage,
 | 
			
		||||
    isAutoFixing = false,
 | 
			
		||||
    onErrorClick,
 | 
			
		||||
}) => {
 | 
			
		||||
    const hasErrors = useMemo(
 | 
			
		||||
        () => validation?.errors.length && validation.errors.length > 0,
 | 
			
		||||
        [validation?.errors]
 | 
			
		||||
    );
 | 
			
		||||
    const hasWarnings = useMemo(
 | 
			
		||||
        () => validation?.warnings && validation.warnings.length > 0,
 | 
			
		||||
        [validation?.warnings]
 | 
			
		||||
    );
 | 
			
		||||
    const wasAutoFixed = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
            validation?.warnings?.some((w) =>
 | 
			
		||||
                w.message.includes('Auto-fixed')
 | 
			
		||||
            ) || false,
 | 
			
		||||
        [validation?.warnings]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (!validation && !errorMessage && !isAutoFixing) return null;
 | 
			
		||||
 | 
			
		||||
    if (isAutoFixing) {
 | 
			
		||||
        return (
 | 
			
		||||
            <>
 | 
			
		||||
                <Separator className="mb-1 mt-2" />
 | 
			
		||||
                <div className="rounded-md border border-sky-200 bg-sky-50 dark:border-sky-800 dark:bg-sky-950">
 | 
			
		||||
                    <div className="space-y-3 p-3 pt-2 text-sky-700 dark:text-sky-300">
 | 
			
		||||
                        <div className="flex items-start gap-2">
 | 
			
		||||
                            <Spinner className="mt-0.5 size-4 shrink-0 text-sky-700 dark:text-sky-300" />
 | 
			
		||||
                            <div className="flex-1 text-sm text-sky-700 dark:text-sky-300">
 | 
			
		||||
                                Auto-fixing SQL syntax errors...
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If we have parser errors (errorMessage) after validation
 | 
			
		||||
    if (errorMessage && !hasErrors) {
 | 
			
		||||
        return (
 | 
			
		||||
            <>
 | 
			
		||||
                <Separator className="mb-1 mt-2" />
 | 
			
		||||
                <div className="mb-1 flex shrink-0 items-center gap-2">
 | 
			
		||||
                    <p className="text-xs text-red-700">{errorMessage}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <Separator className="mb-1 mt-2" />
 | 
			
		||||
 | 
			
		||||
            {hasErrors ? (
 | 
			
		||||
                <div className="rounded-md border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950">
 | 
			
		||||
                    <ScrollArea className="h-fit max-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-fit max-h-24">
 | 
			
		||||
                        <div className="space-y-3 p-3 pt-2 text-sky-700 dark:text-sky-300">
 | 
			
		||||
                            <div className="flex items-start gap-2">
 | 
			
		||||
                                <AlertTriangle className="mt-0.5 size-4 shrink-0 text-sky-700 dark:text-sky-300" />
 | 
			
		||||
                                <div className="flex-1 text-sm text-sky-700 dark:text-sky-300">
 | 
			
		||||
                                    <div className="mb-1 font-medium">
 | 
			
		||||
                                        Import Info:
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    {validation?.warnings.map(
 | 
			
		||||
                                        (warning, idx) => (
 | 
			
		||||
                                            <div
 | 
			
		||||
                                                key={idx}
 | 
			
		||||
                                                className="ml-2 text-xs"
 | 
			
		||||
                                            >
 | 
			
		||||
                                                • {warning.message}
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        )
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </ScrollArea>
 | 
			
		||||
                </div>
 | 
			
		||||
            ) : null}
 | 
			
		||||
 | 
			
		||||
            {!hasErrors && !hasWarnings && !errorMessage && validation ? (
 | 
			
		||||
                <div className="rounded-md border border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950">
 | 
			
		||||
                    <div className="space-y-3 p-3 pt-2 text-green-700 dark:text-green-300">
 | 
			
		||||
                        <div className="flex items-start gap-2">
 | 
			
		||||
                            <CheckCircle className="mt-0.5 size-4 shrink-0 text-green-700 dark:text-green-300" />
 | 
			
		||||
                            <div className="flex-1 text-sm text-green-700 dark:text-green-300">
 | 
			
		||||
                                SQL syntax validated successfully
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            ) : null}
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										2
									
								
								src/dialogs/common/select-tables/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,2 @@
 | 
			
		||||
export const MAX_TABLES_IN_DIAGRAM = 500;
 | 
			
		||||
export const MAX_TABLES_WITHOUT_SHOWING_FILTER = 50;
 | 
			
		||||
							
								
								
									
										683
									
								
								src/dialogs/common/select-tables/select-tables.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,683 @@
 | 
			
		||||
import React, { useState, useMemo, useEffect, useCallback } from 'react';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { Search, AlertCircle, Check, X, View, Table } from 'lucide-react';
 | 
			
		||||
import { Checkbox } from '@/components/checkbox/checkbox';
 | 
			
		||||
import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
 | 
			
		||||
import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import {
 | 
			
		||||
    DialogDescription,
 | 
			
		||||
    DialogFooter,
 | 
			
		||||
    DialogHeader,
 | 
			
		||||
    DialogInternalContent,
 | 
			
		||||
    DialogTitle,
 | 
			
		||||
} from '@/components/dialog/dialog';
 | 
			
		||||
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
 | 
			
		||||
import { generateTableKey } from '@/lib/domain';
 | 
			
		||||
import { Spinner } from '@/components/spinner/spinner';
 | 
			
		||||
import {
 | 
			
		||||
    Pagination,
 | 
			
		||||
    PaginationContent,
 | 
			
		||||
    PaginationItem,
 | 
			
		||||
    PaginationPrevious,
 | 
			
		||||
    PaginationNext,
 | 
			
		||||
} from '@/components/pagination/pagination';
 | 
			
		||||
import { MAX_TABLES_IN_DIAGRAM } from './constants';
 | 
			
		||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
 | 
			
		||||
export interface SelectTablesProps {
 | 
			
		||||
    databaseMetadata?: DatabaseMetadata;
 | 
			
		||||
    onImport: ({
 | 
			
		||||
        selectedTables,
 | 
			
		||||
        databaseMetadata,
 | 
			
		||||
    }: {
 | 
			
		||||
        selectedTables?: SelectedTable[];
 | 
			
		||||
        databaseMetadata?: DatabaseMetadata;
 | 
			
		||||
    }) => Promise<void>;
 | 
			
		||||
    onBack: () => void;
 | 
			
		||||
    isLoading?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const TABLES_PER_PAGE = 10;
 | 
			
		||||
 | 
			
		||||
interface TableInfo {
 | 
			
		||||
    key: string;
 | 
			
		||||
    schema?: string;
 | 
			
		||||
    tableName: string;
 | 
			
		||||
    fullName: string;
 | 
			
		||||
    type: 'table' | 'view';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SelectTables: React.FC<SelectTablesProps> = ({
 | 
			
		||||
    databaseMetadata,
 | 
			
		||||
    onImport,
 | 
			
		||||
    onBack,
 | 
			
		||||
    isLoading = false,
 | 
			
		||||
}) => {
 | 
			
		||||
    const [searchTerm, setSearchTerm] = useState('');
 | 
			
		||||
    const [currentPage, setCurrentPage] = useState(1);
 | 
			
		||||
    const [showTables, setShowTables] = useState(true);
 | 
			
		||||
    const [showViews, setShowViews] = useState(false);
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const [isImporting, setIsImporting] = useState(false);
 | 
			
		||||
 | 
			
		||||
    // Prepare all tables and views with their metadata
 | 
			
		||||
    const allTables = useMemo(() => {
 | 
			
		||||
        const tables: TableInfo[] = [];
 | 
			
		||||
 | 
			
		||||
        // Add regular tables
 | 
			
		||||
        databaseMetadata?.tables.forEach((table) => {
 | 
			
		||||
            const schema = schemaNameToDomainSchemaName(table.schema);
 | 
			
		||||
            const tableName = table.table;
 | 
			
		||||
 | 
			
		||||
            const key = `table:${generateTableKey({ tableName, schemaName: schema })}`;
 | 
			
		||||
 | 
			
		||||
            tables.push({
 | 
			
		||||
                key,
 | 
			
		||||
                schema,
 | 
			
		||||
                tableName,
 | 
			
		||||
                fullName: schema ? `${schema}.${tableName}` : tableName,
 | 
			
		||||
                type: 'table',
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Add views
 | 
			
		||||
        databaseMetadata?.views?.forEach((view) => {
 | 
			
		||||
            const schema = schemaNameToDomainSchemaName(view.schema);
 | 
			
		||||
            const viewName = view.view_name;
 | 
			
		||||
 | 
			
		||||
            if (!viewName) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const key = `view:${generateTableKey({
 | 
			
		||||
                tableName: viewName,
 | 
			
		||||
                schemaName: schema,
 | 
			
		||||
            })}`;
 | 
			
		||||
 | 
			
		||||
            tables.push({
 | 
			
		||||
                key,
 | 
			
		||||
                schema,
 | 
			
		||||
                tableName: viewName,
 | 
			
		||||
                fullName:
 | 
			
		||||
                    schema === 'default' ? viewName : `${schema}.${viewName}`,
 | 
			
		||||
                type: 'view',
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return tables.sort((a, b) => a.fullName.localeCompare(b.fullName));
 | 
			
		||||
    }, [databaseMetadata?.tables, databaseMetadata?.views]);
 | 
			
		||||
 | 
			
		||||
    // Count tables and views separately
 | 
			
		||||
    const tableCount = useMemo(
 | 
			
		||||
        () => allTables.filter((t) => t.type === 'table').length,
 | 
			
		||||
        [allTables]
 | 
			
		||||
    );
 | 
			
		||||
    const viewCount = useMemo(
 | 
			
		||||
        () => allTables.filter((t) => t.type === 'view').length,
 | 
			
		||||
        [allTables]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Initialize selectedTables with all tables (not views) if less than 100 tables
 | 
			
		||||
    const [selectedTables, setSelectedTables] = useState<Set<string>>(() => {
 | 
			
		||||
        const tables = allTables.filter((t) => t.type === 'table');
 | 
			
		||||
        if (tables.length < MAX_TABLES_IN_DIAGRAM) {
 | 
			
		||||
            return new Set(tables.map((t) => t.key));
 | 
			
		||||
        }
 | 
			
		||||
        return new Set();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Filter tables based on search term and type filters
 | 
			
		||||
    const filteredTables = useMemo(() => {
 | 
			
		||||
        let filtered = allTables;
 | 
			
		||||
 | 
			
		||||
        // Filter by type
 | 
			
		||||
        filtered = filtered.filter((table) => {
 | 
			
		||||
            if (table.type === 'table' && !showTables) return false;
 | 
			
		||||
            if (table.type === 'view' && !showViews) return false;
 | 
			
		||||
            return true;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Filter by search term
 | 
			
		||||
        if (searchTerm.trim()) {
 | 
			
		||||
            const searchLower = searchTerm.toLowerCase();
 | 
			
		||||
            filtered = filtered.filter(
 | 
			
		||||
                (table) =>
 | 
			
		||||
                    table.tableName.toLowerCase().includes(searchLower) ||
 | 
			
		||||
                    table.schema?.toLowerCase().includes(searchLower) ||
 | 
			
		||||
                    table.fullName.toLowerCase().includes(searchLower)
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return filtered;
 | 
			
		||||
    }, [allTables, searchTerm, showTables, showViews]);
 | 
			
		||||
 | 
			
		||||
    // Calculate pagination
 | 
			
		||||
    const totalPages = useMemo(
 | 
			
		||||
        () => Math.max(1, Math.ceil(filteredTables.length / TABLES_PER_PAGE)),
 | 
			
		||||
        [filteredTables.length]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const paginatedTables = useMemo(() => {
 | 
			
		||||
        const startIndex = (currentPage - 1) * TABLES_PER_PAGE;
 | 
			
		||||
        const endIndex = startIndex + TABLES_PER_PAGE;
 | 
			
		||||
        return filteredTables.slice(startIndex, endIndex);
 | 
			
		||||
    }, [filteredTables, currentPage]);
 | 
			
		||||
 | 
			
		||||
    // Get currently visible selected tables
 | 
			
		||||
    const visibleSelectedTables = useMemo(() => {
 | 
			
		||||
        return paginatedTables.filter((table) => selectedTables.has(table.key));
 | 
			
		||||
    }, [paginatedTables, selectedTables]);
 | 
			
		||||
 | 
			
		||||
    const canAddMore = useMemo(
 | 
			
		||||
        () => selectedTables.size < MAX_TABLES_IN_DIAGRAM,
 | 
			
		||||
        [selectedTables.size]
 | 
			
		||||
    );
 | 
			
		||||
    const hasSearchResults = useMemo(
 | 
			
		||||
        () => filteredTables.length > 0,
 | 
			
		||||
        [filteredTables.length]
 | 
			
		||||
    );
 | 
			
		||||
    const allVisibleSelected = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
            visibleSelectedTables.length === paginatedTables.length &&
 | 
			
		||||
            paginatedTables.length > 0,
 | 
			
		||||
        [visibleSelectedTables.length, paginatedTables.length]
 | 
			
		||||
    );
 | 
			
		||||
    const canSelectAllFiltered = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
            filteredTables.length > 0 &&
 | 
			
		||||
            filteredTables.some((table) => !selectedTables.has(table.key)) &&
 | 
			
		||||
            canAddMore,
 | 
			
		||||
        [filteredTables, selectedTables, canAddMore]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Reset to first page when search changes
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setCurrentPage(1);
 | 
			
		||||
    }, [searchTerm]);
 | 
			
		||||
 | 
			
		||||
    const handleTableToggle = useCallback(
 | 
			
		||||
        (tableKey: string) => {
 | 
			
		||||
            const newSelected = new Set(selectedTables);
 | 
			
		||||
 | 
			
		||||
            if (newSelected.has(tableKey)) {
 | 
			
		||||
                newSelected.delete(tableKey);
 | 
			
		||||
            } else if (selectedTables.size < MAX_TABLES_IN_DIAGRAM) {
 | 
			
		||||
                newSelected.add(tableKey);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            setSelectedTables(newSelected);
 | 
			
		||||
        },
 | 
			
		||||
        [selectedTables]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const handleTogglePageSelection = useCallback(() => {
 | 
			
		||||
        const newSelected = new Set(selectedTables);
 | 
			
		||||
 | 
			
		||||
        if (allVisibleSelected) {
 | 
			
		||||
            // Deselect all on current page
 | 
			
		||||
            for (const table of paginatedTables) {
 | 
			
		||||
                newSelected.delete(table.key);
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            // Select all on current page
 | 
			
		||||
            for (const table of paginatedTables) {
 | 
			
		||||
                if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break;
 | 
			
		||||
                newSelected.add(table.key);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setSelectedTables(newSelected);
 | 
			
		||||
    }, [allVisibleSelected, paginatedTables, selectedTables]);
 | 
			
		||||
 | 
			
		||||
    const handleSelectAllFiltered = useCallback(() => {
 | 
			
		||||
        const newSelected = new Set(selectedTables);
 | 
			
		||||
 | 
			
		||||
        for (const table of filteredTables) {
 | 
			
		||||
            if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break;
 | 
			
		||||
            newSelected.add(table.key);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setSelectedTables(newSelected);
 | 
			
		||||
    }, [filteredTables, selectedTables]);
 | 
			
		||||
 | 
			
		||||
    const handleNextPage = useCallback(() => {
 | 
			
		||||
        if (currentPage < totalPages) {
 | 
			
		||||
            setCurrentPage(currentPage + 1);
 | 
			
		||||
        }
 | 
			
		||||
    }, [currentPage, totalPages]);
 | 
			
		||||
 | 
			
		||||
    const handlePrevPage = useCallback(() => {
 | 
			
		||||
        if (currentPage > 1) {
 | 
			
		||||
            setCurrentPage(currentPage - 1);
 | 
			
		||||
        }
 | 
			
		||||
    }, [currentPage]);
 | 
			
		||||
 | 
			
		||||
    const handleClearSelection = useCallback(() => {
 | 
			
		||||
        setSelectedTables(new Set());
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const handleConfirm = useCallback(async () => {
 | 
			
		||||
        if (isImporting) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setIsImporting(true);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const selectedTableObjects: SelectedTable[] = Array.from(
 | 
			
		||||
                selectedTables
 | 
			
		||||
            )
 | 
			
		||||
                .map((key): SelectedTable | null => {
 | 
			
		||||
                    const table = allTables.find((t) => t.key === key);
 | 
			
		||||
                    if (!table) return null;
 | 
			
		||||
 | 
			
		||||
                    return {
 | 
			
		||||
                        schema: table.schema,
 | 
			
		||||
                        table: table.tableName,
 | 
			
		||||
                        type: table.type,
 | 
			
		||||
                    } satisfies SelectedTable;
 | 
			
		||||
                })
 | 
			
		||||
                .filter((t): t is SelectedTable => t !== null);
 | 
			
		||||
 | 
			
		||||
            await onImport({
 | 
			
		||||
                selectedTables: selectedTableObjects,
 | 
			
		||||
                databaseMetadata,
 | 
			
		||||
            });
 | 
			
		||||
        } finally {
 | 
			
		||||
            setIsImporting(false);
 | 
			
		||||
        }
 | 
			
		||||
    }, [selectedTables, allTables, onImport, databaseMetadata, isImporting]);
 | 
			
		||||
 | 
			
		||||
    const { isMd: isDesktop } = useBreakpoint('md');
 | 
			
		||||
 | 
			
		||||
    const renderPagination = useCallback(
 | 
			
		||||
        () => (
 | 
			
		||||
            <Pagination>
 | 
			
		||||
                <PaginationContent>
 | 
			
		||||
                    <PaginationItem>
 | 
			
		||||
                        <PaginationPrevious
 | 
			
		||||
                            onClick={handlePrevPage}
 | 
			
		||||
                            className={cn(
 | 
			
		||||
                                'cursor-pointer',
 | 
			
		||||
                                currentPage === 1 &&
 | 
			
		||||
                                    'pointer-events-none opacity-50'
 | 
			
		||||
                            )}
 | 
			
		||||
                        />
 | 
			
		||||
                    </PaginationItem>
 | 
			
		||||
                    <PaginationItem>
 | 
			
		||||
                        <span className="px-3 text-sm text-muted-foreground">
 | 
			
		||||
                            Page {currentPage} of {totalPages}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </PaginationItem>
 | 
			
		||||
                    <PaginationItem>
 | 
			
		||||
                        <PaginationNext
 | 
			
		||||
                            onClick={handleNextPage}
 | 
			
		||||
                            className={cn(
 | 
			
		||||
                                'cursor-pointer',
 | 
			
		||||
                                (currentPage >= totalPages ||
 | 
			
		||||
                                    filteredTables.length === 0) &&
 | 
			
		||||
                                    'pointer-events-none opacity-50'
 | 
			
		||||
                            )}
 | 
			
		||||
                        />
 | 
			
		||||
                    </PaginationItem>
 | 
			
		||||
                </PaginationContent>
 | 
			
		||||
            </Pagination>
 | 
			
		||||
        ),
 | 
			
		||||
        [
 | 
			
		||||
            currentPage,
 | 
			
		||||
            totalPages,
 | 
			
		||||
            handlePrevPage,
 | 
			
		||||
            handleNextPage,
 | 
			
		||||
            filteredTables.length,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (isLoading) {
 | 
			
		||||
        return (
 | 
			
		||||
            <div className="flex h-[400px] items-center justify-center">
 | 
			
		||||
                <div className="text-center">
 | 
			
		||||
                    <Spinner className="mb-4" />
 | 
			
		||||
                    <p className="text-sm text-muted-foreground">
 | 
			
		||||
                        Parsing database metadata...
 | 
			
		||||
                    </p>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <DialogHeader>
 | 
			
		||||
                <DialogTitle>Select Tables to Import</DialogTitle>
 | 
			
		||||
                <DialogDescription>
 | 
			
		||||
                    {tableCount} {tableCount === 1 ? 'table' : 'tables'}
 | 
			
		||||
                    {viewCount > 0 && (
 | 
			
		||||
                        <>
 | 
			
		||||
                            {' and '}
 | 
			
		||||
                            {viewCount} {viewCount === 1 ? 'view' : 'views'}
 | 
			
		||||
                        </>
 | 
			
		||||
                    )}
 | 
			
		||||
                    {' found. '}
 | 
			
		||||
                    {allTables.length > MAX_TABLES_IN_DIAGRAM
 | 
			
		||||
                        ? `Select up to ${MAX_TABLES_IN_DIAGRAM} to import.`
 | 
			
		||||
                        : 'Choose which ones to import.'}
 | 
			
		||||
                </DialogDescription>
 | 
			
		||||
            </DialogHeader>
 | 
			
		||||
            <DialogInternalContent>
 | 
			
		||||
                <div className="flex h-full flex-col space-y-4">
 | 
			
		||||
                    {/* Warning/Info Banner */}
 | 
			
		||||
                    {allTables.length > MAX_TABLES_IN_DIAGRAM ? (
 | 
			
		||||
                        <div
 | 
			
		||||
                            className={cn(
 | 
			
		||||
                                'flex items-center gap-2 rounded-lg p-3 text-sm',
 | 
			
		||||
                                'bg-amber-50 text-amber-800 dark:bg-amber-950 dark:text-amber-200'
 | 
			
		||||
                            )}
 | 
			
		||||
                        >
 | 
			
		||||
                            <AlertCircle className="size-4 shrink-0" />
 | 
			
		||||
                            <span>
 | 
			
		||||
                                Due to performance limitations, you can import a
 | 
			
		||||
                                maximum of {MAX_TABLES_IN_DIAGRAM} tables.
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                    {/* Search Input */}
 | 
			
		||||
                    <div className="relative">
 | 
			
		||||
                        <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
 | 
			
		||||
                        <Input
 | 
			
		||||
                            placeholder="Search tables..."
 | 
			
		||||
                            value={searchTerm}
 | 
			
		||||
                            onChange={(e) => setSearchTerm(e.target.value)}
 | 
			
		||||
                            className="px-9"
 | 
			
		||||
                        />
 | 
			
		||||
                        {searchTerm && (
 | 
			
		||||
                            <button
 | 
			
		||||
                                onClick={() => setSearchTerm('')}
 | 
			
		||||
                                className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
 | 
			
		||||
                            >
 | 
			
		||||
                                <X className="size-4" />
 | 
			
		||||
                            </button>
 | 
			
		||||
                        )}
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    {/* Selection Status and Actions - Responsive layout */}
 | 
			
		||||
                    <div className="flex flex-col items-center gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
 | 
			
		||||
                        {/* Left side: selection count -> checkboxes -> results found */}
 | 
			
		||||
                        <div className="flex flex-col items-center gap-3 text-sm sm:flex-row sm:items-center sm:gap-4">
 | 
			
		||||
                            <div className="flex flex-col items-center gap-1 sm:flex-row sm:items-center sm:gap-4">
 | 
			
		||||
                                <span className="text-center font-medium">
 | 
			
		||||
                                    {selectedTables.size} /{' '}
 | 
			
		||||
                                    {Math.min(
 | 
			
		||||
                                        MAX_TABLES_IN_DIAGRAM,
 | 
			
		||||
                                        allTables.length
 | 
			
		||||
                                    )}{' '}
 | 
			
		||||
                                    items selected
 | 
			
		||||
                                </span>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <div className="flex items-center gap-3 sm:border-x sm:px-4">
 | 
			
		||||
                                <div className="flex items-center gap-2">
 | 
			
		||||
                                    <Checkbox
 | 
			
		||||
                                        checked={showTables}
 | 
			
		||||
                                        onCheckedChange={(checked) => {
 | 
			
		||||
                                            // Prevent unchecking if it's the only one checked
 | 
			
		||||
                                            if (!checked && !showViews) return;
 | 
			
		||||
                                            setShowTables(!!checked);
 | 
			
		||||
                                        }}
 | 
			
		||||
                                    />
 | 
			
		||||
                                    <Table
 | 
			
		||||
                                        className="size-4"
 | 
			
		||||
                                        strokeWidth={1.5}
 | 
			
		||||
                                    />
 | 
			
		||||
                                    <span>tables</span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div className="flex items-center gap-2">
 | 
			
		||||
                                    <Checkbox
 | 
			
		||||
                                        checked={showViews}
 | 
			
		||||
                                        onCheckedChange={(checked) => {
 | 
			
		||||
                                            // Prevent unchecking if it's the only one checked
 | 
			
		||||
                                            if (!checked && !showTables) return;
 | 
			
		||||
                                            setShowViews(!!checked);
 | 
			
		||||
                                        }}
 | 
			
		||||
                                    />
 | 
			
		||||
                                    <View
 | 
			
		||||
                                        className="size-4"
 | 
			
		||||
                                        strokeWidth={1.5}
 | 
			
		||||
                                    />
 | 
			
		||||
                                    <span>views</span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
 | 
			
		||||
                            <span className="hidden text-muted-foreground sm:inline">
 | 
			
		||||
                                {filteredTables.length}{' '}
 | 
			
		||||
                                {filteredTables.length === 1
 | 
			
		||||
                                    ? 'result'
 | 
			
		||||
                                    : 'results'}{' '}
 | 
			
		||||
                                found
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        {/* Right side: action buttons */}
 | 
			
		||||
                        <div className="flex flex-wrap items-center justify-center gap-2">
 | 
			
		||||
                            {hasSearchResults && (
 | 
			
		||||
                                <>
 | 
			
		||||
                                    {/* Show page selection button when not searching and no selection */}
 | 
			
		||||
                                    {!searchTerm &&
 | 
			
		||||
                                        selectedTables.size === 0 && (
 | 
			
		||||
                                            <Button
 | 
			
		||||
                                                variant="outline"
 | 
			
		||||
                                                size="sm"
 | 
			
		||||
                                                onClick={
 | 
			
		||||
                                                    handleTogglePageSelection
 | 
			
		||||
                                                }
 | 
			
		||||
                                                disabled={
 | 
			
		||||
                                                    paginatedTables.length === 0
 | 
			
		||||
                                                }
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {allVisibleSelected
 | 
			
		||||
                                                    ? 'Deselect'
 | 
			
		||||
                                                    : 'Select'}{' '}
 | 
			
		||||
                                                page
 | 
			
		||||
                                            </Button>
 | 
			
		||||
                                        )}
 | 
			
		||||
                                    {/* Show Select all button when there are unselected tables */}
 | 
			
		||||
                                    {canSelectAllFiltered &&
 | 
			
		||||
                                        selectedTables.size === 0 && (
 | 
			
		||||
                                            <Button
 | 
			
		||||
                                                variant="outline"
 | 
			
		||||
                                                size="sm"
 | 
			
		||||
                                                onClick={
 | 
			
		||||
                                                    handleSelectAllFiltered
 | 
			
		||||
                                                }
 | 
			
		||||
                                                disabled={!canSelectAllFiltered}
 | 
			
		||||
                                                title={(() => {
 | 
			
		||||
                                                    const unselectedCount =
 | 
			
		||||
                                                        filteredTables.filter(
 | 
			
		||||
                                                            (table) =>
 | 
			
		||||
                                                                !selectedTables.has(
 | 
			
		||||
                                                                    table.key
 | 
			
		||||
                                                                )
 | 
			
		||||
                                                        ).length;
 | 
			
		||||
                                                    const remainingCapacity =
 | 
			
		||||
                                                        MAX_TABLES_IN_DIAGRAM -
 | 
			
		||||
                                                        selectedTables.size;
 | 
			
		||||
                                                    if (
 | 
			
		||||
                                                        unselectedCount >
 | 
			
		||||
                                                        remainingCapacity
 | 
			
		||||
                                                    ) {
 | 
			
		||||
                                                        return `Can only select ${remainingCapacity} more tables (${MAX_TABLES_IN_DIAGRAM} max limit)`;
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                    return undefined;
 | 
			
		||||
                                                })()}
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {(() => {
 | 
			
		||||
                                                    const unselectedCount =
 | 
			
		||||
                                                        filteredTables.filter(
 | 
			
		||||
                                                            (table) =>
 | 
			
		||||
                                                                !selectedTables.has(
 | 
			
		||||
                                                                    table.key
 | 
			
		||||
                                                                )
 | 
			
		||||
                                                        ).length;
 | 
			
		||||
                                                    const remainingCapacity =
 | 
			
		||||
                                                        MAX_TABLES_IN_DIAGRAM -
 | 
			
		||||
                                                        selectedTables.size;
 | 
			
		||||
                                                    if (
 | 
			
		||||
                                                        unselectedCount >
 | 
			
		||||
                                                        remainingCapacity
 | 
			
		||||
                                                    ) {
 | 
			
		||||
                                                        return `Select ${remainingCapacity} of ${unselectedCount}`;
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                    return `Select all ${unselectedCount}`;
 | 
			
		||||
                                                })()}
 | 
			
		||||
                                            </Button>
 | 
			
		||||
                                        )}
 | 
			
		||||
                                </>
 | 
			
		||||
                            )}
 | 
			
		||||
                            {selectedTables.size > 0 && (
 | 
			
		||||
                                <>
 | 
			
		||||
                                    {/* Show page selection/deselection button when user has selections */}
 | 
			
		||||
                                    {paginatedTables.length > 0 && (
 | 
			
		||||
                                        <Button
 | 
			
		||||
                                            variant="outline"
 | 
			
		||||
                                            size="sm"
 | 
			
		||||
                                            onClick={handleTogglePageSelection}
 | 
			
		||||
                                        >
 | 
			
		||||
                                            {allVisibleSelected
 | 
			
		||||
                                                ? 'Deselect'
 | 
			
		||||
                                                : 'Select'}{' '}
 | 
			
		||||
                                            page
 | 
			
		||||
                                        </Button>
 | 
			
		||||
                                    )}
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                        variant="outline"
 | 
			
		||||
                                        size="sm"
 | 
			
		||||
                                        onClick={handleClearSelection}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        Clear selection
 | 
			
		||||
                                    </Button>
 | 
			
		||||
                                </>
 | 
			
		||||
                            )}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                {/* Table List */}
 | 
			
		||||
                <div className="flex min-h-[428px] flex-1 flex-col">
 | 
			
		||||
                    {hasSearchResults ? (
 | 
			
		||||
                        <>
 | 
			
		||||
                            <div className="flex-1 py-4">
 | 
			
		||||
                                <div className="space-y-1">
 | 
			
		||||
                                    {paginatedTables.map((table) => {
 | 
			
		||||
                                        const isSelected = selectedTables.has(
 | 
			
		||||
                                            table.key
 | 
			
		||||
                                        );
 | 
			
		||||
                                        const isDisabled =
 | 
			
		||||
                                            !isSelected &&
 | 
			
		||||
                                            selectedTables.size >=
 | 
			
		||||
                                                MAX_TABLES_IN_DIAGRAM;
 | 
			
		||||
 | 
			
		||||
                                        return (
 | 
			
		||||
                                            <div
 | 
			
		||||
                                                key={table.key}
 | 
			
		||||
                                                className={cn(
 | 
			
		||||
                                                    'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors',
 | 
			
		||||
                                                    {
 | 
			
		||||
                                                        'cursor-not-allowed':
 | 
			
		||||
                                                            isDisabled,
 | 
			
		||||
 | 
			
		||||
                                                        'bg-muted hover:bg-muted/80':
 | 
			
		||||
                                                            isSelected,
 | 
			
		||||
                                                        'hover:bg-accent':
 | 
			
		||||
                                                            !isSelected &&
 | 
			
		||||
                                                            !isDisabled,
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                )}
 | 
			
		||||
                                            >
 | 
			
		||||
                                                <Checkbox
 | 
			
		||||
                                                    checked={isSelected}
 | 
			
		||||
                                                    disabled={isDisabled}
 | 
			
		||||
                                                    onCheckedChange={() =>
 | 
			
		||||
                                                        handleTableToggle(
 | 
			
		||||
                                                            table.key
 | 
			
		||||
                                                        )
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                />
 | 
			
		||||
                                                {table.type === 'view' ? (
 | 
			
		||||
                                                    <View
 | 
			
		||||
                                                        className="size-4"
 | 
			
		||||
                                                        strokeWidth={1.5}
 | 
			
		||||
                                                    />
 | 
			
		||||
                                                ) : (
 | 
			
		||||
                                                    <Table
 | 
			
		||||
                                                        className="size-4"
 | 
			
		||||
                                                        strokeWidth={1.5}
 | 
			
		||||
                                                    />
 | 
			
		||||
                                                )}
 | 
			
		||||
                                                <span className="flex-1">
 | 
			
		||||
                                                    {table.schema ? (
 | 
			
		||||
                                                        <span className="text-muted-foreground">
 | 
			
		||||
                                                            {table.schema}.
 | 
			
		||||
                                                        </span>
 | 
			
		||||
                                                    ) : null}
 | 
			
		||||
                                                    <span className="font-medium">
 | 
			
		||||
                                                        {table.tableName}
 | 
			
		||||
                                                    </span>
 | 
			
		||||
                                                    {table.type === 'view' && (
 | 
			
		||||
                                                        <span className="ml-2 text-xs text-muted-foreground">
 | 
			
		||||
                                                            (view)
 | 
			
		||||
                                                        </span>
 | 
			
		||||
                                                    )}
 | 
			
		||||
                                                </span>
 | 
			
		||||
                                                {isSelected && (
 | 
			
		||||
                                                    <Check className="size-4 text-pink-600" />
 | 
			
		||||
                                                )}
 | 
			
		||||
                                            </div>
 | 
			
		||||
                                        );
 | 
			
		||||
                                    })}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </>
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        <div className="flex h-full items-center justify-center py-4">
 | 
			
		||||
                            <p className="text-sm text-muted-foreground">
 | 
			
		||||
                                {searchTerm
 | 
			
		||||
                                    ? 'No tables found matching your search.'
 | 
			
		||||
                                    : 'Start typing to search for tables...'}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    )}
 | 
			
		||||
                </div>
 | 
			
		||||
                {isDesktop ? renderPagination() : null}
 | 
			
		||||
            </DialogInternalContent>
 | 
			
		||||
            <DialogFooter className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2 md:justify-between md:gap-0">
 | 
			
		||||
                <Button
 | 
			
		||||
                    type="button"
 | 
			
		||||
                    variant="secondary"
 | 
			
		||||
                    onClick={onBack}
 | 
			
		||||
                    disabled={isImporting}
 | 
			
		||||
                >
 | 
			
		||||
                    {t('new_diagram_dialog.back')}
 | 
			
		||||
                </Button>
 | 
			
		||||
 | 
			
		||||
                <Button
 | 
			
		||||
                    onClick={handleConfirm}
 | 
			
		||||
                    disabled={selectedTables.size === 0 || isImporting}
 | 
			
		||||
                    className="bg-pink-500 text-white hover:bg-pink-600"
 | 
			
		||||
                >
 | 
			
		||||
                    {isImporting ? (
 | 
			
		||||
                        <>
 | 
			
		||||
                            <Spinner className="mr-2 size-4 text-white" />
 | 
			
		||||
                            Importing...
 | 
			
		||||
                        </>
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        `Import ${selectedTables.size} Tables`
 | 
			
		||||
                    )}
 | 
			
		||||
                </Button>
 | 
			
		||||
 | 
			
		||||
                {!isDesktop ? renderPagination() : null}
 | 
			
		||||
            </DialogFooter>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
export enum CreateDiagramDialogStep {
 | 
			
		||||
    SELECT_DATABASE = 'SELECT_DATABASE',
 | 
			
		||||
    IMPORT_DATABASE = 'IMPORT_DATABASE',
 | 
			
		||||
    SELECT_TABLES = 'SELECT_TABLES',
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import { Dialog, DialogContent } from '@/components/dialog/dialog';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import { useStorage } from '@/hooks/use-storage';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import { loadFromDatabaseMetadata } from '@/lib/domain/diagram';
 | 
			
		||||
import { loadFromDatabaseMetadata } from '@/lib/data/import-metadata/import';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import { useConfig } from '@/hooks/use-config';
 | 
			
		||||
import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
 | 
			
		||||
@@ -15,8 +15,18 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
			
		||||
import { SelectDatabase } from './select-database/select-database';
 | 
			
		||||
import { CreateDiagramDialogStep } from './create-diagram-dialog-step';
 | 
			
		||||
import { ImportDatabase } from '../common/import-database/import-database';
 | 
			
		||||
import { SelectTables } from '../common/select-tables/select-tables';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
			
		||||
import { sqlImportToDiagram } from '@/lib/data/sql-import';
 | 
			
		||||
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
 | 
			
		||||
import { filterMetadataByTables } from '@/lib/data/import-metadata/filter-metadata';
 | 
			
		||||
import { MAX_TABLES_WITHOUT_SHOWING_FILTER } from '../common/select-tables/constants';
 | 
			
		||||
import {
 | 
			
		||||
    defaultDBMLDiagramName,
 | 
			
		||||
    importDBMLToDiagram,
 | 
			
		||||
} from '@/lib/dbml/dbml-import/dbml-import';
 | 
			
		||||
import type { ImportMethod } from '@/lib/import-method/import-method';
 | 
			
		||||
 | 
			
		||||
export interface CreateDiagramDialogProps extends BaseDialogProps {}
 | 
			
		||||
 | 
			
		||||
@@ -25,10 +35,11 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
}) => {
 | 
			
		||||
    const { diagramId } = useChartDB();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const [importMethod, setImportMethod] = useState<ImportMethod>('query');
 | 
			
		||||
    const [databaseType, setDatabaseType] = useState<DatabaseType>(
 | 
			
		||||
        DatabaseType.GENERIC
 | 
			
		||||
    );
 | 
			
		||||
    const { closeCreateDiagramDialog, openImportDBMLDialog } = useDialog();
 | 
			
		||||
    const { closeCreateDiagramDialog } = useDialog();
 | 
			
		||||
    const { updateConfig } = useConfig();
 | 
			
		||||
    const [scriptResult, setScriptResult] = useState('');
 | 
			
		||||
    const [databaseEdition, setDatabaseEdition] = useState<
 | 
			
		||||
@@ -40,9 +51,12 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
    const { listDiagrams, addDiagram } = useStorage();
 | 
			
		||||
    const [diagramNumber, setDiagramNumber] = useState<number>(1);
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
    const [parsedMetadata, setParsedMetadata] = useState<DatabaseMetadata>();
 | 
			
		||||
    const [isParsingMetadata, setIsParsingMetadata] = useState(false);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setDatabaseEdition(undefined);
 | 
			
		||||
        setImportMethod('query');
 | 
			
		||||
    }, [databaseType]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
@@ -58,38 +72,81 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
        setDatabaseType(DatabaseType.GENERIC);
 | 
			
		||||
        setDatabaseEdition(undefined);
 | 
			
		||||
        setScriptResult('');
 | 
			
		||||
        setImportMethod('query');
 | 
			
		||||
        setParsedMetadata(undefined);
 | 
			
		||||
    }, [dialog.open]);
 | 
			
		||||
 | 
			
		||||
    const hasExistingDiagram = (diagramId ?? '').trim().length !== 0;
 | 
			
		||||
 | 
			
		||||
    const importNewDiagram = useCallback(async () => {
 | 
			
		||||
        const databaseMetadata: DatabaseMetadata =
 | 
			
		||||
            loadDatabaseMetadata(scriptResult);
 | 
			
		||||
 | 
			
		||||
        const diagram = await loadFromDatabaseMetadata({
 | 
			
		||||
            databaseType,
 | 
			
		||||
    const importNewDiagram = useCallback(
 | 
			
		||||
        async ({
 | 
			
		||||
            selectedTables,
 | 
			
		||||
            databaseMetadata,
 | 
			
		||||
            diagramNumber,
 | 
			
		||||
            databaseEdition:
 | 
			
		||||
                databaseEdition?.trim().length === 0
 | 
			
		||||
                    ? undefined
 | 
			
		||||
                    : databaseEdition,
 | 
			
		||||
        });
 | 
			
		||||
        }: {
 | 
			
		||||
            selectedTables?: SelectedTable[];
 | 
			
		||||
            databaseMetadata?: DatabaseMetadata;
 | 
			
		||||
        } = {}) => {
 | 
			
		||||
            let diagram: Diagram | undefined;
 | 
			
		||||
 | 
			
		||||
        await addDiagram({ diagram });
 | 
			
		||||
        await updateConfig({ defaultDiagramId: diagram.id });
 | 
			
		||||
        closeCreateDiagramDialog();
 | 
			
		||||
        navigate(`/diagrams/${diagram.id}`);
 | 
			
		||||
    }, [
 | 
			
		||||
        databaseType,
 | 
			
		||||
        addDiagram,
 | 
			
		||||
        databaseEdition,
 | 
			
		||||
        closeCreateDiagramDialog,
 | 
			
		||||
        navigate,
 | 
			
		||||
        updateConfig,
 | 
			
		||||
        scriptResult,
 | 
			
		||||
        diagramNumber,
 | 
			
		||||
    ]);
 | 
			
		||||
            if (importMethod === 'ddl') {
 | 
			
		||||
                diagram = await sqlImportToDiagram({
 | 
			
		||||
                    sqlContent: scriptResult,
 | 
			
		||||
                    sourceDatabaseType: databaseType,
 | 
			
		||||
                    targetDatabaseType: databaseType,
 | 
			
		||||
                });
 | 
			
		||||
            } else if (importMethod === 'dbml') {
 | 
			
		||||
                diagram = await importDBMLToDiagram(scriptResult, {
 | 
			
		||||
                    databaseType,
 | 
			
		||||
                });
 | 
			
		||||
                // Update the diagram name if it's the default
 | 
			
		||||
                if (diagram.name === defaultDBMLDiagramName) {
 | 
			
		||||
                    diagram.name = `Diagram ${diagramNumber}`;
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                let metadata: DatabaseMetadata | undefined = databaseMetadata;
 | 
			
		||||
 | 
			
		||||
                if (!metadata) {
 | 
			
		||||
                    metadata = loadDatabaseMetadata(scriptResult);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (selectedTables && selectedTables.length > 0) {
 | 
			
		||||
                    metadata = filterMetadataByTables({
 | 
			
		||||
                        metadata,
 | 
			
		||||
                        selectedTables,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                diagram = await loadFromDatabaseMetadata({
 | 
			
		||||
                    databaseType,
 | 
			
		||||
                    databaseMetadata: metadata,
 | 
			
		||||
                    diagramNumber,
 | 
			
		||||
                    databaseEdition:
 | 
			
		||||
                        databaseEdition?.trim().length === 0
 | 
			
		||||
                            ? undefined
 | 
			
		||||
                            : databaseEdition,
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await addDiagram({ diagram });
 | 
			
		||||
            await updateConfig({
 | 
			
		||||
                config: { defaultDiagramId: diagram.id },
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            closeCreateDiagramDialog();
 | 
			
		||||
            navigate(`/diagrams/${diagram.id}`);
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
            importMethod,
 | 
			
		||||
            databaseType,
 | 
			
		||||
            addDiagram,
 | 
			
		||||
            databaseEdition,
 | 
			
		||||
            closeCreateDiagramDialog,
 | 
			
		||||
            navigate,
 | 
			
		||||
            updateConfig,
 | 
			
		||||
            scriptResult,
 | 
			
		||||
            diagramNumber,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const createEmptyDiagram = useCallback(async () => {
 | 
			
		||||
        const diagram: Diagram = {
 | 
			
		||||
@@ -105,13 +162,9 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        await addDiagram({ diagram });
 | 
			
		||||
        await updateConfig({ defaultDiagramId: diagram.id });
 | 
			
		||||
        await updateConfig({ config: { defaultDiagramId: diagram.id } });
 | 
			
		||||
        closeCreateDiagramDialog();
 | 
			
		||||
        navigate(`/diagrams/${diagram.id}`);
 | 
			
		||||
        setTimeout(
 | 
			
		||||
            () => openImportDBMLDialog({ withCreateEmptyDiagram: true }),
 | 
			
		||||
            700
 | 
			
		||||
        );
 | 
			
		||||
    }, [
 | 
			
		||||
        databaseType,
 | 
			
		||||
        addDiagram,
 | 
			
		||||
@@ -120,13 +173,58 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
        navigate,
 | 
			
		||||
        updateConfig,
 | 
			
		||||
        diagramNumber,
 | 
			
		||||
        openImportDBMLDialog,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const importNewDiagramOrFilterTables = useCallback(async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            setIsParsingMetadata(true);
 | 
			
		||||
 | 
			
		||||
            if (importMethod === 'ddl' || importMethod === 'dbml') {
 | 
			
		||||
                await importNewDiagram();
 | 
			
		||||
            } else {
 | 
			
		||||
                // Parse metadata asynchronously to avoid blocking the UI
 | 
			
		||||
                const metadata = await new Promise<DatabaseMetadata>(
 | 
			
		||||
                    (resolve, reject) => {
 | 
			
		||||
                        setTimeout(() => {
 | 
			
		||||
                            try {
 | 
			
		||||
                                const result =
 | 
			
		||||
                                    loadDatabaseMetadata(scriptResult);
 | 
			
		||||
                                resolve(result);
 | 
			
		||||
                            } catch (err) {
 | 
			
		||||
                                reject(err);
 | 
			
		||||
                            }
 | 
			
		||||
                        }, 0);
 | 
			
		||||
                    }
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                const totalTablesAndViews =
 | 
			
		||||
                    metadata.tables.length + (metadata.views?.length || 0);
 | 
			
		||||
 | 
			
		||||
                setParsedMetadata(metadata);
 | 
			
		||||
 | 
			
		||||
                // Check if it's a large database that needs table selection
 | 
			
		||||
                if (totalTablesAndViews > MAX_TABLES_WITHOUT_SHOWING_FILTER) {
 | 
			
		||||
                    setStep(CreateDiagramDialogStep.SELECT_TABLES);
 | 
			
		||||
                } else {
 | 
			
		||||
                    await importNewDiagram({
 | 
			
		||||
                        databaseMetadata: metadata,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } finally {
 | 
			
		||||
            setIsParsingMetadata(false);
 | 
			
		||||
        }
 | 
			
		||||
    }, [importMethod, scriptResult, importNewDiagram]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Dialog
 | 
			
		||||
            {...dialog}
 | 
			
		||||
            onOpenChange={(open) => {
 | 
			
		||||
                // Don't allow closing while parsing metadata
 | 
			
		||||
                if (isParsingMetadata) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (!hasExistingDiagram) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
@@ -137,8 +235,10 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent
 | 
			
		||||
                className="flex max-h-screen w-[90vw] max-w-[90vw] flex-col overflow-y-auto md:overflow-visible lg:max-w-[60vw] xl:lg:max-w-lg xl:min-w-[45vw]"
 | 
			
		||||
                className="flex max-h-dvh w-full flex-col md:max-w-[900px]"
 | 
			
		||||
                showClose={hasExistingDiagram}
 | 
			
		||||
                onInteractOutside={(e) => e.preventDefault()}
 | 
			
		||||
                onEscapeKeyDown={(e) => e.preventDefault()}
 | 
			
		||||
            >
 | 
			
		||||
                {step === CreateDiagramDialogStep.SELECT_DATABASE ? (
 | 
			
		||||
                    <SelectDatabase
 | 
			
		||||
@@ -150,9 +250,9 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
                            setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                ) : (
 | 
			
		||||
                ) : step === CreateDiagramDialogStep.IMPORT_DATABASE ? (
 | 
			
		||||
                    <ImportDatabase
 | 
			
		||||
                        onImport={importNewDiagram}
 | 
			
		||||
                        onImport={importNewDiagramOrFilterTables}
 | 
			
		||||
                        onCreateEmptyDiagram={createEmptyDiagram}
 | 
			
		||||
                        databaseEdition={databaseEdition}
 | 
			
		||||
                        databaseType={databaseType}
 | 
			
		||||
@@ -163,8 +263,20 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
                        }
 | 
			
		||||
                        setScriptResult={setScriptResult}
 | 
			
		||||
                        title={t('new_diagram_dialog.import_database.title')}
 | 
			
		||||
                        importMethod={importMethod}
 | 
			
		||||
                        setImportMethod={setImportMethod}
 | 
			
		||||
                        keepDialogAfterImport={true}
 | 
			
		||||
                    />
 | 
			
		||||
                )}
 | 
			
		||||
                ) : step === CreateDiagramDialogStep.SELECT_TABLES ? (
 | 
			
		||||
                    <SelectTables
 | 
			
		||||
                        isLoading={isParsingMetadata || !parsedMetadata}
 | 
			
		||||
                        databaseMetadata={parsedMetadata}
 | 
			
		||||
                        onImport={importNewDiagram}
 | 
			
		||||
                        onBack={() =>
 | 
			
		||||
                            setStep(CreateDiagramDialogStep.IMPORT_DATABASE)
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                ) : null}
 | 
			
		||||
            </DialogContent>
 | 
			
		||||
        </Dialog>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ const SUPPORTED_DB_TYPES: DatabaseType[] = [
 | 
			
		||||
    DatabaseType.MARIADB,
 | 
			
		||||
    DatabaseType.SQLITE,
 | 
			
		||||
    DatabaseType.SQL_SERVER,
 | 
			
		||||
    DatabaseType.ORACLE,
 | 
			
		||||
    DatabaseType.COCKROACHDB,
 | 
			
		||||
    DatabaseType.CLICKHOUSE,
 | 
			
		||||
];
 | 
			
		||||
 
 | 
			
		||||
@@ -69,6 +69,7 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({
 | 
			
		||||
                        type="button"
 | 
			
		||||
                        variant="outline"
 | 
			
		||||
                        onClick={createNewDiagram}
 | 
			
		||||
                        disabled={databaseType === DatabaseType.GENERIC}
 | 
			
		||||
                    >
 | 
			
		||||
                        {t('new_diagram_dialog.empty_diagram')}
 | 
			
		||||
                    </Button>
 | 
			
		||||
 
 | 
			
		||||
@@ -218,8 +218,14 @@ export const CreateRelationshipDialog: React.FC<
 | 
			
		||||
                    closeCreateRelationshipDialog();
 | 
			
		||||
                }
 | 
			
		||||
            }}
 | 
			
		||||
            modal={false}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent className="flex flex-col overflow-y-auto" showClose>
 | 
			
		||||
            <DialogContent
 | 
			
		||||
                className="flex flex-col overflow-y-auto"
 | 
			
		||||
                showClose
 | 
			
		||||
                forceOverlay
 | 
			
		||||
                onInteractOutside={(e) => e.preventDefault()}
 | 
			
		||||
            >
 | 
			
		||||
                <DialogHeader>
 | 
			
		||||
                    <DialogTitle>
 | 
			
		||||
                        {t('create_relationship_dialog.title')}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,11 +16,20 @@ import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import type { ImageType } from '@/context/export-image-context/export-image-context';
 | 
			
		||||
import { useExportImage } from '@/hooks/use-export-image';
 | 
			
		||||
import { Checkbox } from '@/components/checkbox/checkbox';
 | 
			
		||||
import {
 | 
			
		||||
    Accordion,
 | 
			
		||||
    AccordionContent,
 | 
			
		||||
    AccordionItem,
 | 
			
		||||
    AccordionTrigger,
 | 
			
		||||
} from '@/components/accordion/accordion';
 | 
			
		||||
 | 
			
		||||
export interface ExportImageDialogProps extends BaseDialogProps {
 | 
			
		||||
    format: ImageType;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const DEFAULT_INCLUDE_PATTERN_BG = true;
 | 
			
		||||
const DEFAULT_TRANSPARENT = false;
 | 
			
		||||
const DEFAULT_SCALE = '2';
 | 
			
		||||
export const ExportImageDialog: React.FC<ExportImageDialogProps> = ({
 | 
			
		||||
    dialog,
 | 
			
		||||
@@ -28,17 +37,28 @@ export const ExportImageDialog: React.FC<ExportImageDialogProps> = ({
 | 
			
		||||
}) => {
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const [scale, setScale] = useState<string>(DEFAULT_SCALE);
 | 
			
		||||
    const [includePatternBG, setIncludePatternBG] = useState<boolean>(
 | 
			
		||||
        DEFAULT_INCLUDE_PATTERN_BG
 | 
			
		||||
    );
 | 
			
		||||
    const [transparent, setTransparent] =
 | 
			
		||||
        useState<boolean>(DEFAULT_TRANSPARENT);
 | 
			
		||||
    const { exportImage } = useExportImage();
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!dialog.open) return;
 | 
			
		||||
        setScale(DEFAULT_SCALE);
 | 
			
		||||
        setIncludePatternBG(DEFAULT_INCLUDE_PATTERN_BG);
 | 
			
		||||
        setTransparent(DEFAULT_TRANSPARENT);
 | 
			
		||||
    }, [dialog.open]);
 | 
			
		||||
    const { closeExportImageDialog } = useDialog();
 | 
			
		||||
 | 
			
		||||
    const handleExport = useCallback(() => {
 | 
			
		||||
        exportImage(format, Number(scale));
 | 
			
		||||
    }, [exportImage, format, scale]);
 | 
			
		||||
        exportImage(format, {
 | 
			
		||||
            transparent,
 | 
			
		||||
            includePatternBG,
 | 
			
		||||
            scale: Number(scale),
 | 
			
		||||
        });
 | 
			
		||||
    }, [exportImage, format, includePatternBG, transparent, scale]);
 | 
			
		||||
 | 
			
		||||
    const scaleOptions: SelectBoxOption[] = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
@@ -65,15 +85,79 @@ export const ExportImageDialog: React.FC<ExportImageDialogProps> = ({
 | 
			
		||||
                        {t('export_image_dialog.description')}
 | 
			
		||||
                    </DialogDescription>
 | 
			
		||||
                </DialogHeader>
 | 
			
		||||
                <div className="grid gap-4 py-1">
 | 
			
		||||
                    <div className="grid w-full items-center gap-4">
 | 
			
		||||
                        <SelectBox
 | 
			
		||||
                            options={scaleOptions}
 | 
			
		||||
                            multiple={false}
 | 
			
		||||
                            value={scale}
 | 
			
		||||
                            onChange={(value) => setScale(value as string)}
 | 
			
		||||
                        />
 | 
			
		||||
                    </div>
 | 
			
		||||
                <div className="flex flex-col gap-4 py-1">
 | 
			
		||||
                    <SelectBox
 | 
			
		||||
                        options={scaleOptions}
 | 
			
		||||
                        multiple={false}
 | 
			
		||||
                        value={scale}
 | 
			
		||||
                        onChange={(value) => setScale(value as string)}
 | 
			
		||||
                    />
 | 
			
		||||
                    <Accordion type="single" collapsible className="w-full">
 | 
			
		||||
                        <AccordionItem value="settings" className="border-0">
 | 
			
		||||
                            <AccordionTrigger
 | 
			
		||||
                                className="py-1.5"
 | 
			
		||||
                                iconPosition="right"
 | 
			
		||||
                            >
 | 
			
		||||
                                {t('export_image_dialog.advanced_options')}
 | 
			
		||||
                            </AccordionTrigger>
 | 
			
		||||
                            <AccordionContent>
 | 
			
		||||
                                <div className="flex flex-col gap-3 py-2">
 | 
			
		||||
                                    <div className="flex items-start gap-3">
 | 
			
		||||
                                        <Checkbox
 | 
			
		||||
                                            id="pattern-checkbox"
 | 
			
		||||
                                            className="mt-1 data-[state=checked]:border-pink-600 data-[state=checked]:bg-pink-600 data-[state=checked]:text-white"
 | 
			
		||||
                                            checked={includePatternBG}
 | 
			
		||||
                                            onCheckedChange={(value) =>
 | 
			
		||||
                                                setIncludePatternBG(
 | 
			
		||||
                                                    value as boolean
 | 
			
		||||
                                                )
 | 
			
		||||
                                            }
 | 
			
		||||
                                        />
 | 
			
		||||
                                        <div className="flex flex-col">
 | 
			
		||||
                                            <label
 | 
			
		||||
                                                htmlFor="pattern-checkbox"
 | 
			
		||||
                                                className="cursor-pointer font-medium"
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {t(
 | 
			
		||||
                                                    'export_image_dialog.pattern'
 | 
			
		||||
                                                )}
 | 
			
		||||
                                            </label>
 | 
			
		||||
                                            <span className="text-sm text-muted-foreground">
 | 
			
		||||
                                                {t(
 | 
			
		||||
                                                    'export_image_dialog.pattern_description'
 | 
			
		||||
                                                )}
 | 
			
		||||
                                            </span>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                    <div className="flex items-start gap-3">
 | 
			
		||||
                                        <Checkbox
 | 
			
		||||
                                            id="transparent-checkbox"
 | 
			
		||||
                                            className="mt-1 data-[state=checked]:border-pink-600 data-[state=checked]:bg-pink-600 data-[state=checked]:text-white"
 | 
			
		||||
                                            checked={transparent}
 | 
			
		||||
                                            onCheckedChange={(value) =>
 | 
			
		||||
                                                setTransparent(value as boolean)
 | 
			
		||||
                                            }
 | 
			
		||||
                                        />
 | 
			
		||||
                                        <div className="flex flex-col">
 | 
			
		||||
                                            <label
 | 
			
		||||
                                                htmlFor="transparent-checkbox"
 | 
			
		||||
                                                className="cursor-pointer font-medium"
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {t(
 | 
			
		||||
                                                    'export_image_dialog.transparent'
 | 
			
		||||
                                                )}
 | 
			
		||||
                                            </label>
 | 
			
		||||
                                            <span className="text-sm text-muted-foreground">
 | 
			
		||||
                                                {t(
 | 
			
		||||
                                                    'export_image_dialog.transparent_description'
 | 
			
		||||
                                                )}
 | 
			
		||||
                                            </span>
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </AccordionContent>
 | 
			
		||||
                        </AccordionItem>
 | 
			
		||||
                    </Accordion>
 | 
			
		||||
                </div>
 | 
			
		||||
                <DialogFooter className="flex gap-1 md:justify-between">
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
 
 | 
			
		||||
@@ -17,15 +17,21 @@ import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import {
 | 
			
		||||
    exportBaseSQL,
 | 
			
		||||
    exportSQL,
 | 
			
		||||
} from '@/lib/data/export-metadata/export-sql-script';
 | 
			
		||||
} from '@/lib/data/sql-export/export-sql-script';
 | 
			
		||||
import { databaseTypeToLabelMap } from '@/lib/databases';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
 | 
			
		||||
import { Annoyed, Sparkles } from 'lucide-react';
 | 
			
		||||
import React, { useCallback, useEffect, useRef } from 'react';
 | 
			
		||||
import { Trans, useTranslation } from 'react-i18next';
 | 
			
		||||
import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
 | 
			
		||||
import {
 | 
			
		||||
    filterDependency,
 | 
			
		||||
    filterRelationship,
 | 
			
		||||
    filterTable,
 | 
			
		||||
} from '@/lib/domain/diagram-filter/filter';
 | 
			
		||||
import { defaultSchemas } from '@/lib/data/default-schemas';
 | 
			
		||||
 | 
			
		||||
export interface ExportSQLDialogProps extends BaseDialogProps {
 | 
			
		||||
    targetDatabaseType: DatabaseType;
 | 
			
		||||
@@ -36,7 +42,8 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
 | 
			
		||||
    targetDatabaseType,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { closeExportSQLDialog } = useDialog();
 | 
			
		||||
    const { currentDiagram, filteredSchemas } = useChartDB();
 | 
			
		||||
    const { currentDiagram } = useChartDB();
 | 
			
		||||
    const { filter } = useDiagramFilter();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const [script, setScript] = React.useState<string>();
 | 
			
		||||
    const [error, setError] = React.useState<boolean>(false);
 | 
			
		||||
@@ -48,7 +55,16 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
 | 
			
		||||
        const filteredDiagram: Diagram = {
 | 
			
		||||
            ...currentDiagram,
 | 
			
		||||
            tables: currentDiagram.tables?.filter((table) =>
 | 
			
		||||
                shouldShowTablesBySchemaFilter(table, filteredSchemas)
 | 
			
		||||
                filterTable({
 | 
			
		||||
                    table: {
 | 
			
		||||
                        id: table.id,
 | 
			
		||||
                        schema: table.schema,
 | 
			
		||||
                    },
 | 
			
		||||
                    filter,
 | 
			
		||||
                    options: {
 | 
			
		||||
                        defaultSchema: defaultSchemas[targetDatabaseType],
 | 
			
		||||
                    },
 | 
			
		||||
                })
 | 
			
		||||
            ),
 | 
			
		||||
            relationships: currentDiagram.relationships?.filter((rel) => {
 | 
			
		||||
                const sourceTable = currentDiagram.tables?.find(
 | 
			
		||||
@@ -60,11 +76,20 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
 | 
			
		||||
                return (
 | 
			
		||||
                    sourceTable &&
 | 
			
		||||
                    targetTable &&
 | 
			
		||||
                    shouldShowTablesBySchemaFilter(
 | 
			
		||||
                        sourceTable,
 | 
			
		||||
                        filteredSchemas
 | 
			
		||||
                    ) &&
 | 
			
		||||
                    shouldShowTablesBySchemaFilter(targetTable, filteredSchemas)
 | 
			
		||||
                    filterRelationship({
 | 
			
		||||
                        tableA: {
 | 
			
		||||
                            id: sourceTable.id,
 | 
			
		||||
                            schema: sourceTable.schema,
 | 
			
		||||
                        },
 | 
			
		||||
                        tableB: {
 | 
			
		||||
                            id: targetTable.id,
 | 
			
		||||
                            schema: targetTable.schema,
 | 
			
		||||
                        },
 | 
			
		||||
                        filter,
 | 
			
		||||
                        options: {
 | 
			
		||||
                            defaultSchema: defaultSchemas[targetDatabaseType],
 | 
			
		||||
                        },
 | 
			
		||||
                    })
 | 
			
		||||
                );
 | 
			
		||||
            }),
 | 
			
		||||
            dependencies: currentDiagram.dependencies?.filter((dep) => {
 | 
			
		||||
@@ -77,11 +102,20 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
 | 
			
		||||
                return (
 | 
			
		||||
                    table &&
 | 
			
		||||
                    dependentTable &&
 | 
			
		||||
                    shouldShowTablesBySchemaFilter(table, filteredSchemas) &&
 | 
			
		||||
                    shouldShowTablesBySchemaFilter(
 | 
			
		||||
                        dependentTable,
 | 
			
		||||
                        filteredSchemas
 | 
			
		||||
                    )
 | 
			
		||||
                    filterDependency({
 | 
			
		||||
                        tableA: {
 | 
			
		||||
                            id: table.id,
 | 
			
		||||
                            schema: table.schema,
 | 
			
		||||
                        },
 | 
			
		||||
                        tableB: {
 | 
			
		||||
                            id: dependentTable.id,
 | 
			
		||||
                            schema: dependentTable.schema,
 | 
			
		||||
                        },
 | 
			
		||||
                        filter,
 | 
			
		||||
                        options: {
 | 
			
		||||
                            defaultSchema: defaultSchemas[targetDatabaseType],
 | 
			
		||||
                        },
 | 
			
		||||
                    })
 | 
			
		||||
                );
 | 
			
		||||
            }),
 | 
			
		||||
        };
 | 
			
		||||
@@ -101,7 +135,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
 | 
			
		||||
                signal: abortControllerRef.current?.signal,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }, [targetDatabaseType, currentDiagram, filteredSchemas]);
 | 
			
		||||
    }, [targetDatabaseType, currentDiagram, filter]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!dialog.open) {
 | 
			
		||||
@@ -140,7 +174,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
 | 
			
		||||
                            components={[
 | 
			
		||||
                                <a
 | 
			
		||||
                                    key={0}
 | 
			
		||||
                                    href="mailto:chartdb.io@gmail.com"
 | 
			
		||||
                                    href="mailto:support@chartdb.io"
 | 
			
		||||
                                    target="_blank"
 | 
			
		||||
                                    className="text-pink-600 hover:underline"
 | 
			
		||||
                                    rel="noreferrer"
 | 
			
		||||
 
 | 
			
		||||
@@ -6,13 +6,17 @@ import { ImportDatabase } from '../common/import-database/import-database';
 | 
			
		||||
import type { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
			
		||||
import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
 | 
			
		||||
import { loadDatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
 | 
			
		||||
import { loadFromDatabaseMetadata } from '@/lib/domain/diagram';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import { loadFromDatabaseMetadata } from '@/lib/data/import-metadata/import';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
 | 
			
		||||
import { Trans, useTranslation } from 'react-i18next';
 | 
			
		||||
import { useReactFlow } from '@xyflow/react';
 | 
			
		||||
import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
			
		||||
import { useAlert } from '@/context/alert-context/alert-context';
 | 
			
		||||
import { sqlImportToDiagram } from '@/lib/data/sql-import';
 | 
			
		||||
import { importDBMLToDiagram } from '@/lib/dbml/dbml-import/dbml-import';
 | 
			
		||||
import type { ImportMethod } from '@/lib/import-method/import-method';
 | 
			
		||||
 | 
			
		||||
export interface ImportDatabaseDialogProps extends BaseDialogProps {
 | 
			
		||||
    databaseType: DatabaseType;
 | 
			
		||||
@@ -22,6 +26,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
 | 
			
		||||
    dialog,
 | 
			
		||||
    databaseType,
 | 
			
		||||
}) => {
 | 
			
		||||
    const [importMethod, setImportMethod] = useState<ImportMethod>('query');
 | 
			
		||||
    const { closeImportDatabaseDialog } = useDialog();
 | 
			
		||||
    const { showAlert } = useAlert();
 | 
			
		||||
    const {
 | 
			
		||||
@@ -54,17 +59,31 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
 | 
			
		||||
    }, [dialog.open]);
 | 
			
		||||
 | 
			
		||||
    const importDatabase = useCallback(async () => {
 | 
			
		||||
        const databaseMetadata: DatabaseMetadata =
 | 
			
		||||
            loadDatabaseMetadata(scriptResult);
 | 
			
		||||
        let diagram: Diagram | undefined;
 | 
			
		||||
 | 
			
		||||
        const diagram = await loadFromDatabaseMetadata({
 | 
			
		||||
            databaseType,
 | 
			
		||||
            databaseMetadata,
 | 
			
		||||
            databaseEdition:
 | 
			
		||||
                databaseEdition?.trim().length === 0
 | 
			
		||||
                    ? undefined
 | 
			
		||||
                    : databaseEdition,
 | 
			
		||||
        });
 | 
			
		||||
        if (importMethod === 'ddl') {
 | 
			
		||||
            diagram = await sqlImportToDiagram({
 | 
			
		||||
                sqlContent: scriptResult,
 | 
			
		||||
                sourceDatabaseType: databaseType,
 | 
			
		||||
                targetDatabaseType: databaseType,
 | 
			
		||||
            });
 | 
			
		||||
        } else if (importMethod === 'dbml') {
 | 
			
		||||
            diagram = await importDBMLToDiagram(scriptResult, {
 | 
			
		||||
                databaseType,
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            const databaseMetadata: DatabaseMetadata =
 | 
			
		||||
                loadDatabaseMetadata(scriptResult);
 | 
			
		||||
 | 
			
		||||
            diagram = await loadFromDatabaseMetadata({
 | 
			
		||||
                databaseType,
 | 
			
		||||
                databaseMetadata,
 | 
			
		||||
                databaseEdition:
 | 
			
		||||
                    databaseEdition?.trim().length === 0
 | 
			
		||||
                        ? undefined
 | 
			
		||||
                        : databaseEdition,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const tableIdsToRemove = tables
 | 
			
		||||
            .filter((table) =>
 | 
			
		||||
@@ -308,6 +327,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
 | 
			
		||||
 | 
			
		||||
        closeImportDatabaseDialog();
 | 
			
		||||
    }, [
 | 
			
		||||
        importMethod,
 | 
			
		||||
        databaseEdition,
 | 
			
		||||
        currentDatabaseType,
 | 
			
		||||
        updateDatabaseType,
 | 
			
		||||
@@ -337,7 +357,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent
 | 
			
		||||
                className="flex max-h-screen w-[90vw] flex-col overflow-y-auto md:overflow-visible xl:min-w-[45vw]"
 | 
			
		||||
                className="flex max-h-screen w-full flex-col md:max-w-[900px]"
 | 
			
		||||
                showClose
 | 
			
		||||
            >
 | 
			
		||||
                <ImportDatabase
 | 
			
		||||
@@ -349,6 +369,8 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
 | 
			
		||||
                    setScriptResult={setScriptResult}
 | 
			
		||||
                    keepDialogAfterImport
 | 
			
		||||
                    title={t('import_database_dialog.title', { diagramName })}
 | 
			
		||||
                    importMethod={importMethod}
 | 
			
		||||
                    setImportMethod={setImportMethod}
 | 
			
		||||
                />
 | 
			
		||||
            </DialogContent>
 | 
			
		||||
        </Dialog>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,410 +0,0 @@
 | 
			
		||||
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 } from '@/lib/dbml-import';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { Parser } from '@dbml/core';
 | 
			
		||||
import { useCanvas } from '@/hooks/use-canvas';
 | 
			
		||||
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
 | 
			
		||||
import { useToast } from '@/components/toast/use-toast';
 | 
			
		||||
import { Spinner } from '@/components/spinner/spinner';
 | 
			
		||||
import { debounce } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
interface DBMLError {
 | 
			
		||||
    message: string;
 | 
			
		||||
    line: number;
 | 
			
		||||
    column: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function parseDBMLError(error: unknown): DBMLError | null {
 | 
			
		||||
    try {
 | 
			
		||||
        if (typeof error === 'string') {
 | 
			
		||||
            const parsed = JSON.parse(error);
 | 
			
		||||
            if (parsed.diags?.[0]) {
 | 
			
		||||
                const diag = parsed.diags[0];
 | 
			
		||||
                return {
 | 
			
		||||
                    message: diag.message,
 | 
			
		||||
                    line: diag.location.start.line,
 | 
			
		||||
                    column: diag.location.start.column,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        } else if (error && typeof error === 'object' && 'diags' in error) {
 | 
			
		||||
            const parsed = error as {
 | 
			
		||||
                diags: Array<{
 | 
			
		||||
                    message: string;
 | 
			
		||||
                    location: { start: { line: number; column: number } };
 | 
			
		||||
                }>;
 | 
			
		||||
            };
 | 
			
		||||
            if (parsed.diags?.[0]) {
 | 
			
		||||
                return {
 | 
			
		||||
                    message: parsed.diags[0].message,
 | 
			
		||||
                    line: parsed.diags[0].location.start.line,
 | 
			
		||||
                    column: parsed.diags[0].location.start.column,
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
        console.error('Error parsing DBML error:', e);
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ImportDBMLDialogProps extends BaseDialogProps {
 | 
			
		||||
    withCreateEmptyDiagram?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ImportDBMLDialog: React.FC<ImportDBMLDialogProps> = ({
 | 
			
		||||
    dialog,
 | 
			
		||||
    withCreateEmptyDiagram,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const initialDBML = `// Use DBML to define your database structure
 | 
			
		||||
// Simple Blog System with Comments Example
 | 
			
		||||
 | 
			
		||||
Table users {
 | 
			
		||||
  id integer [primary key]
 | 
			
		||||
  name varchar
 | 
			
		||||
  email varchar
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table posts {
 | 
			
		||||
  id integer [primary key]
 | 
			
		||||
  title varchar
 | 
			
		||||
  content text
 | 
			
		||||
  user_id integer
 | 
			
		||||
  created_at timestamp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Table comments {
 | 
			
		||||
  id integer [primary key]
 | 
			
		||||
  content text
 | 
			
		||||
  post_id integer
 | 
			
		||||
  user_id integer
 | 
			
		||||
  created_at timestamp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Relationships
 | 
			
		||||
Ref: posts.user_id > users.id // Each post belongs to one user
 | 
			
		||||
Ref: comments.post_id > posts.id // Each comment belongs to one post
 | 
			
		||||
Ref: comments.user_id > users.id // Each comment is written by one user`;
 | 
			
		||||
 | 
			
		||||
    const [dbmlContent, setDBMLContent] = useState<string>(initialDBML);
 | 
			
		||||
    const { closeImportDBMLDialog } = useDialog();
 | 
			
		||||
    const [errorMessage, setErrorMessage] = useState<string | undefined>();
 | 
			
		||||
    const { effectiveTheme } = useTheme();
 | 
			
		||||
    const { toast } = useToast();
 | 
			
		||||
    const {
 | 
			
		||||
        addTables,
 | 
			
		||||
        addRelationships,
 | 
			
		||||
        tables,
 | 
			
		||||
        relationships,
 | 
			
		||||
        removeTables,
 | 
			
		||||
        removeRelationships,
 | 
			
		||||
    } = useChartDB();
 | 
			
		||||
    const { reorderTables } = useCanvas();
 | 
			
		||||
    const [reorder, setReorder] = useState(false);
 | 
			
		||||
    const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
 | 
			
		||||
    const decorationsCollection =
 | 
			
		||||
        useRef<monaco.editor.IEditorDecorationsCollection>();
 | 
			
		||||
 | 
			
		||||
    const handleEditorDidMount = (
 | 
			
		||||
        editor: monaco.editor.IStandaloneCodeEditor
 | 
			
		||||
    ) => {
 | 
			
		||||
        editorRef.current = editor;
 | 
			
		||||
        decorationsCollection.current = editor.createDecorationsCollection();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (reorder) {
 | 
			
		||||
            reorderTables({
 | 
			
		||||
                updateHistory: false,
 | 
			
		||||
            });
 | 
			
		||||
            setReorder(false);
 | 
			
		||||
        }
 | 
			
		||||
    }, [reorder, reorderTables]);
 | 
			
		||||
 | 
			
		||||
    const highlightErrorLine = useCallback((error: DBMLError) => {
 | 
			
		||||
        if (!editorRef.current) return;
 | 
			
		||||
 | 
			
		||||
        const model = editorRef.current.getModel();
 | 
			
		||||
        if (!model) return;
 | 
			
		||||
 | 
			
		||||
        const decorations = [
 | 
			
		||||
            {
 | 
			
		||||
                range: new monaco.Range(
 | 
			
		||||
                    error.line,
 | 
			
		||||
                    1,
 | 
			
		||||
                    error.line,
 | 
			
		||||
                    model.getLineMaxColumn(error.line)
 | 
			
		||||
                ),
 | 
			
		||||
                options: {
 | 
			
		||||
                    isWholeLine: true,
 | 
			
		||||
                    className: 'dbml-error-line',
 | 
			
		||||
                    glyphMarginClassName: 'dbml-error-glyph',
 | 
			
		||||
                    hoverMessage: { value: error.message },
 | 
			
		||||
                    overviewRuler: {
 | 
			
		||||
                        color: '#ff0000',
 | 
			
		||||
                        position: monaco.editor.OverviewRulerLane.Right,
 | 
			
		||||
                        darkColor: '#ff0000',
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        decorationsCollection.current?.set(decorations);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const clearDecorations = useCallback(() => {
 | 
			
		||||
        decorationsCollection.current?.clear();
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const validateDBML = useCallback(
 | 
			
		||||
        async (content: string) => {
 | 
			
		||||
            // Clear previous errors
 | 
			
		||||
            setErrorMessage(undefined);
 | 
			
		||||
            clearDecorations();
 | 
			
		||||
 | 
			
		||||
            if (!content.trim()) return;
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                const parser = new Parser();
 | 
			
		||||
                parser.parse(content, '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) =>
 | 
			
		||||
                            t.name === table.name && t.schema === table.schema
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
                .map((table) => table.id);
 | 
			
		||||
            // Find relationships that need to be removed
 | 
			
		||||
            const relationshipIdsToRemove = relationships
 | 
			
		||||
                .filter((relationship) => {
 | 
			
		||||
                    const sourceTable = tables.find(
 | 
			
		||||
                        (table) => table.id === relationship.sourceTableId
 | 
			
		||||
                    );
 | 
			
		||||
                    const targetTable = tables.find(
 | 
			
		||||
                        (table) => table.id === relationship.targetTableId
 | 
			
		||||
                    );
 | 
			
		||||
                    if (!sourceTable || !targetTable) return true;
 | 
			
		||||
                    const replacementSourceTable = importedDiagram.tables?.find(
 | 
			
		||||
                        (table) =>
 | 
			
		||||
                            table.name === sourceTable.name &&
 | 
			
		||||
                            table.schema === sourceTable.schema
 | 
			
		||||
                    );
 | 
			
		||||
                    const replacementTargetTable = importedDiagram.tables?.find(
 | 
			
		||||
                        (table) =>
 | 
			
		||||
                            table.name === targetTable.name &&
 | 
			
		||||
                            table.schema === targetTable.schema
 | 
			
		||||
                    );
 | 
			
		||||
                    return replacementSourceTable || replacementTargetTable;
 | 
			
		||||
                })
 | 
			
		||||
                .map((relationship) => relationship.id);
 | 
			
		||||
 | 
			
		||||
            // Remove existing items
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                removeTables(tableIdsToRemove, { updateHistory: false }),
 | 
			
		||||
                removeRelationships(relationshipIdsToRemove, {
 | 
			
		||||
                    updateHistory: false,
 | 
			
		||||
                }),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            // Add new items
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                addTables(importedDiagram.tables ?? [], {
 | 
			
		||||
                    updateHistory: false,
 | 
			
		||||
                }),
 | 
			
		||||
                addRelationships(importedDiagram.relationships ?? [], {
 | 
			
		||||
                    updateHistory: false,
 | 
			
		||||
                }),
 | 
			
		||||
            ]);
 | 
			
		||||
            setReorder(true);
 | 
			
		||||
            closeImportDBMLDialog();
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            toast({
 | 
			
		||||
                title: t('import_dbml_dialog.error.title'),
 | 
			
		||||
                variant: 'destructive',
 | 
			
		||||
                description: (
 | 
			
		||||
                    <>
 | 
			
		||||
                        <div>{t('import_dbml_dialog.error.description')}</div>
 | 
			
		||||
                        {e instanceof Error ? e.message : JSON.stringify(e)}
 | 
			
		||||
                    </>
 | 
			
		||||
                ),
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }, [
 | 
			
		||||
        dbmlContent,
 | 
			
		||||
        closeImportDBMLDialog,
 | 
			
		||||
        tables,
 | 
			
		||||
        relationships,
 | 
			
		||||
        removeTables,
 | 
			
		||||
        removeRelationships,
 | 
			
		||||
        addTables,
 | 
			
		||||
        addRelationships,
 | 
			
		||||
        errorMessage,
 | 
			
		||||
        toast,
 | 
			
		||||
        setReorder,
 | 
			
		||||
        t,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Dialog
 | 
			
		||||
            {...dialog}
 | 
			
		||||
            onOpenChange={(open) => {
 | 
			
		||||
                if (!open) {
 | 
			
		||||
                    closeImportDBMLDialog();
 | 
			
		||||
                }
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent
 | 
			
		||||
                className="flex h-[80vh] max-h-screen flex-col"
 | 
			
		||||
                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>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,98 @@
 | 
			
		||||
import React, { useCallback } from 'react';
 | 
			
		||||
import {
 | 
			
		||||
    DropdownMenu,
 | 
			
		||||
    DropdownMenuContent,
 | 
			
		||||
    DropdownMenuItem,
 | 
			
		||||
    DropdownMenuSeparator,
 | 
			
		||||
    DropdownMenuTrigger,
 | 
			
		||||
} from '@/components/dropdown-menu/dropdown-menu';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { Ellipsis, Layers2, SquareArrowOutUpRight, Trash2 } from 'lucide-react';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import type { Diagram } from '@/lib/domain';
 | 
			
		||||
import { useStorage } from '@/hooks/use-storage';
 | 
			
		||||
import { cloneDiagram } from '@/lib/clone';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
 | 
			
		||||
interface DiagramRowActionsMenuProps {
 | 
			
		||||
    diagram: Diagram;
 | 
			
		||||
    onOpen: () => void;
 | 
			
		||||
    refetch: () => void;
 | 
			
		||||
    numberOfDiagrams: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const DiagramRowActionsMenu: React.FC<DiagramRowActionsMenuProps> = ({
 | 
			
		||||
    diagram,
 | 
			
		||||
    onOpen,
 | 
			
		||||
    refetch,
 | 
			
		||||
    numberOfDiagrams,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { diagramId } = useChartDB();
 | 
			
		||||
    const { deleteDiagram, addDiagram } = useStorage();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
    const onDelete = useCallback(async () => {
 | 
			
		||||
        deleteDiagram(diagram.id);
 | 
			
		||||
        refetch();
 | 
			
		||||
 | 
			
		||||
        if (diagram.id === diagramId || numberOfDiagrams <= 1) {
 | 
			
		||||
            window.location.href = '/';
 | 
			
		||||
        }
 | 
			
		||||
    }, [deleteDiagram, diagram.id, diagramId, refetch, numberOfDiagrams]);
 | 
			
		||||
 | 
			
		||||
    const onDuplicate = useCallback(async () => {
 | 
			
		||||
        const duplicatedDiagram = cloneDiagram(diagram);
 | 
			
		||||
 | 
			
		||||
        const diagramToAdd = duplicatedDiagram.diagram;
 | 
			
		||||
 | 
			
		||||
        if (!diagramToAdd) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        diagramToAdd.name = `${diagram.name} (Copy)`;
 | 
			
		||||
 | 
			
		||||
        addDiagram({ diagram: diagramToAdd });
 | 
			
		||||
        refetch();
 | 
			
		||||
    }, [addDiagram, refetch, diagram]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <DropdownMenu>
 | 
			
		||||
            <DropdownMenuTrigger asChild>
 | 
			
		||||
                <Button
 | 
			
		||||
                    variant="ghost"
 | 
			
		||||
                    size="icon"
 | 
			
		||||
                    className="size-8 p-0"
 | 
			
		||||
                    onClick={(e) => e.stopPropagation()}
 | 
			
		||||
                >
 | 
			
		||||
                    <Ellipsis className="size-4" />
 | 
			
		||||
                </Button>
 | 
			
		||||
            </DropdownMenuTrigger>
 | 
			
		||||
            <DropdownMenuContent align="end">
 | 
			
		||||
                <DropdownMenuItem
 | 
			
		||||
                    onClick={onOpen}
 | 
			
		||||
                    className="flex justify-between gap-4"
 | 
			
		||||
                >
 | 
			
		||||
                    {t('open_diagram_dialog.diagram_actions.open')}
 | 
			
		||||
                    <SquareArrowOutUpRight className="size-3.5" />
 | 
			
		||||
                </DropdownMenuItem>
 | 
			
		||||
 | 
			
		||||
                <DropdownMenuItem
 | 
			
		||||
                    onClick={onDuplicate}
 | 
			
		||||
                    className="flex justify-between gap-4"
 | 
			
		||||
                >
 | 
			
		||||
                    {t('open_diagram_dialog.diagram_actions.duplicate')}
 | 
			
		||||
                    <Layers2 className="size-3.5" />
 | 
			
		||||
                </DropdownMenuItem>
 | 
			
		||||
 | 
			
		||||
                <DropdownMenuSeparator />
 | 
			
		||||
                <DropdownMenuItem
 | 
			
		||||
                    onClick={onDelete}
 | 
			
		||||
                    className="flex justify-between gap-4 text-red-700"
 | 
			
		||||
                >
 | 
			
		||||
                    {t('open_diagram_dialog.diagram_actions.delete')}
 | 
			
		||||
                    <Trash2 className="size-3.5 text-red-700" />
 | 
			
		||||
                </DropdownMenuItem>
 | 
			
		||||
            </DropdownMenuContent>
 | 
			
		||||
        </DropdownMenu>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -27,6 +27,7 @@ import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
			
		||||
import { useDebounce } from '@/hooks/use-debounce';
 | 
			
		||||
import { DiagramRowActionsMenu } from './diagram-row-actions-menu/diagram-row-actions-menu';
 | 
			
		||||
 | 
			
		||||
export interface OpenDiagramDialogProps extends BaseDialogProps {
 | 
			
		||||
    canClose?: boolean;
 | 
			
		||||
@@ -46,26 +47,27 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
 | 
			
		||||
        string | undefined
 | 
			
		||||
    >();
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setSelectedDiagramId(undefined);
 | 
			
		||||
    }, [dialog.open]);
 | 
			
		||||
    const fetchDiagrams = useCallback(async () => {
 | 
			
		||||
        const diagrams = await listDiagrams({ includeTables: true });
 | 
			
		||||
        setDiagrams(
 | 
			
		||||
            diagrams.sort(
 | 
			
		||||
                (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
 | 
			
		||||
            )
 | 
			
		||||
        );
 | 
			
		||||
    }, [listDiagrams]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        const fetchDiagrams = async () => {
 | 
			
		||||
            const diagrams = await listDiagrams({ includeTables: true });
 | 
			
		||||
            setDiagrams(
 | 
			
		||||
                diagrams.sort(
 | 
			
		||||
                    (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()
 | 
			
		||||
                )
 | 
			
		||||
            );
 | 
			
		||||
        };
 | 
			
		||||
        if (!dialog.open) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        setSelectedDiagramId(undefined);
 | 
			
		||||
        fetchDiagrams();
 | 
			
		||||
    }, [listDiagrams, setDiagrams, dialog.open]);
 | 
			
		||||
    }, [dialog.open, fetchDiagrams]);
 | 
			
		||||
 | 
			
		||||
    const openDiagram = useCallback(
 | 
			
		||||
        (diagramId: string) => {
 | 
			
		||||
            if (diagramId) {
 | 
			
		||||
                updateConfig({ defaultDiagramId: diagramId });
 | 
			
		||||
                updateConfig({ config: { defaultDiagramId: diagramId } });
 | 
			
		||||
                navigate(`/diagrams/${diagramId}`);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
@@ -166,6 +168,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
 | 
			
		||||
                                            'open_diagram_dialog.table_columns.tables_count'
 | 
			
		||||
                                        )}
 | 
			
		||||
                                    </TableHead>
 | 
			
		||||
                                    <TableHead />
 | 
			
		||||
                                </TableRow>
 | 
			
		||||
                            </TableHeader>
 | 
			
		||||
                            <TableBody>
 | 
			
		||||
@@ -221,6 +224,19 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
 | 
			
		||||
                                        <TableCell className="text-center">
 | 
			
		||||
                                            {diagram.tables?.length}
 | 
			
		||||
                                        </TableCell>
 | 
			
		||||
                                        <TableCell className="items-center p-0 pr-1 text-right">
 | 
			
		||||
                                            <DiagramRowActionsMenu
 | 
			
		||||
                                                diagram={diagram}
 | 
			
		||||
                                                onOpen={() => {
 | 
			
		||||
                                                    openDiagram(diagram.id);
 | 
			
		||||
                                                    closeOpenDiagramDialog();
 | 
			
		||||
                                                }}
 | 
			
		||||
                                                numberOfDiagrams={
 | 
			
		||||
                                                    diagrams.length
 | 
			
		||||
                                                }
 | 
			
		||||
                                                refetch={fetchDiagrams}
 | 
			
		||||
                                            />
 | 
			
		||||
                                        </TableCell>
 | 
			
		||||
                                    </TableRow>
 | 
			
		||||
                                ))}
 | 
			
		||||
                            </TableBody>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import React, { useCallback, useEffect, useMemo } from 'react';
 | 
			
		||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import {
 | 
			
		||||
    Dialog,
 | 
			
		||||
@@ -17,11 +17,23 @@ import type { DBSchema } from '@/lib/domain/db-schema';
 | 
			
		||||
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
 | 
			
		||||
import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { Separator } from '@/components/separator/separator';
 | 
			
		||||
import { Group, SquarePlus } from 'lucide-react';
 | 
			
		||||
import {
 | 
			
		||||
    Tooltip,
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
    TooltipTrigger,
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { defaultSchemas } from '@/lib/data/default-schemas';
 | 
			
		||||
import { Label } from '@/components/label/label';
 | 
			
		||||
 | 
			
		||||
export interface TableSchemaDialogProps extends BaseDialogProps {
 | 
			
		||||
    table?: DBTable;
 | 
			
		||||
    schemas: DBSchema[];
 | 
			
		||||
    onConfirm: (schema: string) => void;
 | 
			
		||||
    onConfirm: ({ schema }: { schema: DBSchema }) => void;
 | 
			
		||||
    allowSchemaCreation?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
			
		||||
@@ -29,27 +41,73 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
			
		||||
    table,
 | 
			
		||||
    schemas,
 | 
			
		||||
    onConfirm,
 | 
			
		||||
    allowSchemaCreation = false,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const [selectedSchema, setSelectedSchema] = React.useState<string>(
 | 
			
		||||
    const { databaseType } = useChartDB();
 | 
			
		||||
    const [selectedSchemaId, setSelectedSchemaId] = useState<string>(
 | 
			
		||||
        table?.schema
 | 
			
		||||
            ? schemaNameToSchemaId(table.schema)
 | 
			
		||||
            : (schemas?.[0]?.id ?? '')
 | 
			
		||||
    );
 | 
			
		||||
    const allowSchemaSelection = useMemo(
 | 
			
		||||
        () => schemas && schemas.length > 0,
 | 
			
		||||
        [schemas]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const defaultSchemaName = useMemo(
 | 
			
		||||
        () => defaultSchemas?.[databaseType],
 | 
			
		||||
        [databaseType]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const [isCreatingNew, setIsCreatingNew] =
 | 
			
		||||
        useState<boolean>(!allowSchemaSelection);
 | 
			
		||||
    const [newSchemaName, setNewSchemaName] = useState<string>(
 | 
			
		||||
        allowSchemaCreation && !allowSchemaSelection
 | 
			
		||||
            ? (defaultSchemaName ?? '')
 | 
			
		||||
            : ''
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!dialog.open) return;
 | 
			
		||||
        setSelectedSchema(
 | 
			
		||||
        setSelectedSchemaId(
 | 
			
		||||
            table?.schema
 | 
			
		||||
                ? schemaNameToSchemaId(table.schema)
 | 
			
		||||
                : (schemas?.[0]?.id ?? '')
 | 
			
		||||
        );
 | 
			
		||||
    }, [dialog.open, schemas, table?.schema]);
 | 
			
		||||
        setIsCreatingNew(!allowSchemaSelection);
 | 
			
		||||
        setNewSchemaName(
 | 
			
		||||
            allowSchemaCreation && !allowSchemaSelection
 | 
			
		||||
                ? (defaultSchemaName ?? '')
 | 
			
		||||
                : ''
 | 
			
		||||
        );
 | 
			
		||||
    }, [
 | 
			
		||||
        defaultSchemaName,
 | 
			
		||||
        dialog.open,
 | 
			
		||||
        schemas,
 | 
			
		||||
        table?.schema,
 | 
			
		||||
        allowSchemaSelection,
 | 
			
		||||
        allowSchemaCreation,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const { closeTableSchemaDialog } = useDialog();
 | 
			
		||||
 | 
			
		||||
    const handleConfirm = useCallback(() => {
 | 
			
		||||
        onConfirm(selectedSchema);
 | 
			
		||||
    }, [onConfirm, selectedSchema]);
 | 
			
		||||
        if (isCreatingNew && newSchemaName.trim()) {
 | 
			
		||||
            const newSchema: DBSchema = {
 | 
			
		||||
                id: schemaNameToSchemaId(newSchemaName.trim()),
 | 
			
		||||
                name: newSchemaName.trim(),
 | 
			
		||||
                tableCount: 0,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            onConfirm({ schema: newSchema });
 | 
			
		||||
        } else {
 | 
			
		||||
            const schema = schemas.find((s) => s.id === selectedSchemaId);
 | 
			
		||||
            if (!schema) return;
 | 
			
		||||
 | 
			
		||||
            onConfirm({ schema });
 | 
			
		||||
        }
 | 
			
		||||
    }, [onConfirm, selectedSchemaId, schemas, isCreatingNew, newSchemaName]);
 | 
			
		||||
 | 
			
		||||
    const schemaOptions: SelectBoxOption[] = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
@@ -60,6 +118,25 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
			
		||||
        [schemas]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const renderSwitchCreateOrSelectButton = useCallback(
 | 
			
		||||
        () => (
 | 
			
		||||
            <Button
 | 
			
		||||
                variant="outline"
 | 
			
		||||
                className="w-full justify-start"
 | 
			
		||||
                onClick={() => setIsCreatingNew(!isCreatingNew)}
 | 
			
		||||
                disabled={!allowSchemaSelection || !allowSchemaCreation}
 | 
			
		||||
            >
 | 
			
		||||
                {!isCreatingNew ? (
 | 
			
		||||
                    <SquarePlus className="mr-2 size-4 " />
 | 
			
		||||
                ) : (
 | 
			
		||||
                    <Group className="mr-2 size-4 " />
 | 
			
		||||
                )}
 | 
			
		||||
                {isCreatingNew ? 'Select existing schema' : 'Create new schema'}
 | 
			
		||||
            </Button>
 | 
			
		||||
        ),
 | 
			
		||||
        [isCreatingNew, allowSchemaSelection, allowSchemaCreation]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Dialog
 | 
			
		||||
            {...dialog}
 | 
			
		||||
@@ -67,48 +144,106 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
 | 
			
		||||
                if (!open) {
 | 
			
		||||
                    closeTableSchemaDialog();
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                setTimeout(() => (document.body.style.pointerEvents = ''), 500);
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent className="flex flex-col" showClose>
 | 
			
		||||
                <DialogHeader>
 | 
			
		||||
                    <DialogTitle>
 | 
			
		||||
                        {table
 | 
			
		||||
                            ? t('update_table_schema_dialog.title')
 | 
			
		||||
                            : t('new_table_schema_dialog.title')}
 | 
			
		||||
                        {!allowSchemaSelection && allowSchemaCreation
 | 
			
		||||
                            ? t('create_table_schema_dialog.title')
 | 
			
		||||
                            : table
 | 
			
		||||
                              ? t('update_table_schema_dialog.title')
 | 
			
		||||
                              : t('new_table_schema_dialog.title')}
 | 
			
		||||
                    </DialogTitle>
 | 
			
		||||
                    <DialogDescription>
 | 
			
		||||
                        {table
 | 
			
		||||
                            ? t('update_table_schema_dialog.description', {
 | 
			
		||||
                                  tableName: table.name,
 | 
			
		||||
                              })
 | 
			
		||||
                            : t('new_table_schema_dialog.description')}
 | 
			
		||||
                        {!allowSchemaSelection && allowSchemaCreation
 | 
			
		||||
                            ? t('create_table_schema_dialog.description')
 | 
			
		||||
                            : table
 | 
			
		||||
                              ? t('update_table_schema_dialog.description', {
 | 
			
		||||
                                    tableName: table.name,
 | 
			
		||||
                                })
 | 
			
		||||
                              : t('new_table_schema_dialog.description')}
 | 
			
		||||
                    </DialogDescription>
 | 
			
		||||
                </DialogHeader>
 | 
			
		||||
                <div className="grid gap-4 py-1">
 | 
			
		||||
                    <div className="grid w-full items-center gap-4">
 | 
			
		||||
                        <SelectBox
 | 
			
		||||
                            options={schemaOptions}
 | 
			
		||||
                            multiple={false}
 | 
			
		||||
                            value={selectedSchema}
 | 
			
		||||
                            onChange={(value) =>
 | 
			
		||||
                                setSelectedSchema(value as string)
 | 
			
		||||
                            }
 | 
			
		||||
                        />
 | 
			
		||||
                        {!isCreatingNew ? (
 | 
			
		||||
                            <SelectBox
 | 
			
		||||
                                options={schemaOptions}
 | 
			
		||||
                                multiple={false}
 | 
			
		||||
                                value={selectedSchemaId}
 | 
			
		||||
                                onChange={(value) =>
 | 
			
		||||
                                    setSelectedSchemaId(value as string)
 | 
			
		||||
                                }
 | 
			
		||||
                            />
 | 
			
		||||
                        ) : (
 | 
			
		||||
                            <div className="flex flex-col gap-2">
 | 
			
		||||
                                {allowSchemaCreation &&
 | 
			
		||||
                                !allowSchemaSelection ? (
 | 
			
		||||
                                    <Label htmlFor="new-schema-name">
 | 
			
		||||
                                        Schema Name
 | 
			
		||||
                                    </Label>
 | 
			
		||||
                                ) : null}
 | 
			
		||||
                                <Input
 | 
			
		||||
                                    id="new-schema-name"
 | 
			
		||||
                                    value={newSchemaName}
 | 
			
		||||
                                    onChange={(e) =>
 | 
			
		||||
                                        setNewSchemaName(e.target.value)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    placeholder={`Enter schema name.${defaultSchemaName ? ` e.g. ${defaultSchemaName}.` : ''}`}
 | 
			
		||||
                                    autoFocus
 | 
			
		||||
                                />
 | 
			
		||||
                            </div>
 | 
			
		||||
                        )}
 | 
			
		||||
 | 
			
		||||
                        {allowSchemaCreation && allowSchemaSelection ? (
 | 
			
		||||
                            <>
 | 
			
		||||
                                <div className="relative">
 | 
			
		||||
                                    <Separator className="my-2" />
 | 
			
		||||
                                    <span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-background px-2 text-xs text-muted-foreground">
 | 
			
		||||
                                        or
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                {allowSchemaSelection ? (
 | 
			
		||||
                                    renderSwitchCreateOrSelectButton()
 | 
			
		||||
                                ) : (
 | 
			
		||||
                                    <Tooltip>
 | 
			
		||||
                                        <TooltipTrigger asChild>
 | 
			
		||||
                                            <span>
 | 
			
		||||
                                                {renderSwitchCreateOrSelectButton()}
 | 
			
		||||
                                            </span>
 | 
			
		||||
                                        </TooltipTrigger>
 | 
			
		||||
                                        <TooltipContent>
 | 
			
		||||
                                            <p>No existing schemas available</p>
 | 
			
		||||
                                        </TooltipContent>
 | 
			
		||||
                                    </Tooltip>
 | 
			
		||||
                                )}
 | 
			
		||||
                            </>
 | 
			
		||||
                        ) : null}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <DialogFooter className="flex gap-1 md:justify-between">
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
                        <Button variant="secondary">
 | 
			
		||||
                            {table
 | 
			
		||||
                                ? t('update_table_schema_dialog.cancel')
 | 
			
		||||
                                : t('new_table_schema_dialog.cancel')}
 | 
			
		||||
                            {isCreatingNew
 | 
			
		||||
                                ? t('create_table_schema_dialog.cancel')
 | 
			
		||||
                                : table
 | 
			
		||||
                                  ? t('update_table_schema_dialog.cancel')
 | 
			
		||||
                                  : t('new_table_schema_dialog.cancel')}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </DialogClose>
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
                        <Button onClick={handleConfirm}>
 | 
			
		||||
                            {table
 | 
			
		||||
                                ? t('update_table_schema_dialog.confirm')
 | 
			
		||||
                                : t('new_table_schema_dialog.confirm')}
 | 
			
		||||
                        <Button
 | 
			
		||||
                            onClick={handleConfirm}
 | 
			
		||||
                            disabled={isCreatingNew && !newSchemaName.trim()}
 | 
			
		||||
                        >
 | 
			
		||||
                            {isCreatingNew
 | 
			
		||||
                                ? t('create_table_schema_dialog.create')
 | 
			
		||||
                                : table
 | 
			
		||||
                                  ? t('update_table_schema_dialog.confirm')
 | 
			
		||||
                                  : t('new_table_schema_dialog.confirm')}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </DialogClose>
 | 
			
		||||
                </DialogFooter>
 | 
			
		||||
 
 | 
			
		||||
@@ -83,6 +83,7 @@
 | 
			
		||||
    }
 | 
			
		||||
    body {
 | 
			
		||||
        @apply bg-background text-foreground;
 | 
			
		||||
        overscroll-behavior-x: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .text-editable {
 | 
			
		||||
@@ -154,3 +155,29 @@
 | 
			
		||||
        background-size: 650%;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Edit button emphasis animation */
 | 
			
		||||
@keyframes dbml_edit-button-emphasis {
 | 
			
		||||
    0% {
 | 
			
		||||
        transform: scale(1);
 | 
			
		||||
        box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
 | 
			
		||||
        background-color: rgba(59, 130, 246, 0);
 | 
			
		||||
    }
 | 
			
		||||
    50% {
 | 
			
		||||
        transform: scale(1.1);
 | 
			
		||||
        box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
 | 
			
		||||
        background-color: rgba(59, 130, 246, 0.1);
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
        transform: scale(1);
 | 
			
		||||
        box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
 | 
			
		||||
        background-color: rgba(59, 130, 246, 0);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dbml-edit-button-emphasis {
 | 
			
		||||
    animation: dbml_edit-button-emphasis 0.6s ease-in-out;
 | 
			
		||||
    animation-iteration-count: 1;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    z-index: 10;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										142
									
								
								src/hooks/use-focus-on.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,142 @@
 | 
			
		||||
import { useCallback } from 'react';
 | 
			
		||||
import { useReactFlow } from '@xyflow/react';
 | 
			
		||||
import { useLayout } from '@/hooks/use-layout';
 | 
			
		||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
 | 
			
		||||
 | 
			
		||||
interface FocusOptions {
 | 
			
		||||
    select?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const useFocusOn = () => {
 | 
			
		||||
    const { fitView, setNodes, setEdges } = useReactFlow();
 | 
			
		||||
    const { hideSidePanel } = useLayout();
 | 
			
		||||
    const { isMd: isDesktop } = useBreakpoint('md');
 | 
			
		||||
 | 
			
		||||
    const focusOnArea = useCallback(
 | 
			
		||||
        (areaId: string, options: FocusOptions = {}) => {
 | 
			
		||||
            const { select = true } = options;
 | 
			
		||||
 | 
			
		||||
            if (select) {
 | 
			
		||||
                setNodes((nodes) =>
 | 
			
		||||
                    nodes.map((node) =>
 | 
			
		||||
                        node.id === areaId
 | 
			
		||||
                            ? {
 | 
			
		||||
                                  ...node,
 | 
			
		||||
                                  selected: true,
 | 
			
		||||
                              }
 | 
			
		||||
                            : {
 | 
			
		||||
                                  ...node,
 | 
			
		||||
                                  selected: false,
 | 
			
		||||
                              }
 | 
			
		||||
                    )
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            fitView({
 | 
			
		||||
                duration: 500,
 | 
			
		||||
                maxZoom: 1,
 | 
			
		||||
                minZoom: 1,
 | 
			
		||||
                nodes: [
 | 
			
		||||
                    {
 | 
			
		||||
                        id: areaId,
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (!isDesktop) {
 | 
			
		||||
                hideSidePanel();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [fitView, setNodes, hideSidePanel, isDesktop]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const focusOnTable = useCallback(
 | 
			
		||||
        (tableId: string, options: FocusOptions = {}) => {
 | 
			
		||||
            const { select = true } = options;
 | 
			
		||||
 | 
			
		||||
            if (select) {
 | 
			
		||||
                setNodes((nodes) =>
 | 
			
		||||
                    nodes.map((node) =>
 | 
			
		||||
                        node.id === tableId
 | 
			
		||||
                            ? {
 | 
			
		||||
                                  ...node,
 | 
			
		||||
                                  selected: true,
 | 
			
		||||
                              }
 | 
			
		||||
                            : {
 | 
			
		||||
                                  ...node,
 | 
			
		||||
                                  selected: false,
 | 
			
		||||
                              }
 | 
			
		||||
                    )
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            fitView({
 | 
			
		||||
                duration: 500,
 | 
			
		||||
                maxZoom: 1,
 | 
			
		||||
                minZoom: 1,
 | 
			
		||||
                nodes: [
 | 
			
		||||
                    {
 | 
			
		||||
                        id: tableId,
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (!isDesktop) {
 | 
			
		||||
                hideSidePanel();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [fitView, setNodes, hideSidePanel, isDesktop]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const focusOnRelationship = useCallback(
 | 
			
		||||
        (
 | 
			
		||||
            relationshipId: string,
 | 
			
		||||
            sourceTableId: string,
 | 
			
		||||
            targetTableId: string,
 | 
			
		||||
            options: FocusOptions = {}
 | 
			
		||||
        ) => {
 | 
			
		||||
            const { select = true } = options;
 | 
			
		||||
 | 
			
		||||
            if (select) {
 | 
			
		||||
                setEdges((edges) =>
 | 
			
		||||
                    edges.map((edge) =>
 | 
			
		||||
                        edge.id === relationshipId
 | 
			
		||||
                            ? {
 | 
			
		||||
                                  ...edge,
 | 
			
		||||
                                  selected: true,
 | 
			
		||||
                              }
 | 
			
		||||
                            : {
 | 
			
		||||
                                  ...edge,
 | 
			
		||||
                                  selected: false,
 | 
			
		||||
                              }
 | 
			
		||||
                    )
 | 
			
		||||
                );
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            fitView({
 | 
			
		||||
                duration: 500,
 | 
			
		||||
                maxZoom: 1,
 | 
			
		||||
                minZoom: 1,
 | 
			
		||||
                nodes: [
 | 
			
		||||
                    {
 | 
			
		||||
                        id: sourceTableId,
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        id: targetTableId,
 | 
			
		||||
                    },
 | 
			
		||||
                ],
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (!isDesktop) {
 | 
			
		||||
                hideSidePanel();
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [fitView, setEdges, hideSidePanel, isDesktop]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        focusOnArea,
 | 
			
		||||
        focusOnTable,
 | 
			
		||||
        focusOnRelationship,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										320
									
								
								src/hooks/use-update-table-field.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,320 @@
 | 
			
		||||
import { useCallback, useMemo, useState, useEffect } from 'react';
 | 
			
		||||
import { useChartDB } from './use-chartdb';
 | 
			
		||||
import { useDebounce } from './use-debounce-v2';
 | 
			
		||||
import type { DBField, DBTable } from '@/lib/domain';
 | 
			
		||||
import type {
 | 
			
		||||
    SelectBoxOption,
 | 
			
		||||
    SelectBoxProps,
 | 
			
		||||
} from '@/components/select-box/select-box';
 | 
			
		||||
import {
 | 
			
		||||
    dataTypeDataToDataType,
 | 
			
		||||
    sortedDataTypeMap,
 | 
			
		||||
} from '@/lib/data/data-types/data-types';
 | 
			
		||||
import { generateDBFieldSuffix } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DataTypeData } from '@/lib/data/data-types/data-types';
 | 
			
		||||
 | 
			
		||||
const generateFieldRegexPatterns = (
 | 
			
		||||
    dataType: DataTypeData
 | 
			
		||||
): {
 | 
			
		||||
    regex?: string;
 | 
			
		||||
    extractRegex?: RegExp;
 | 
			
		||||
} => {
 | 
			
		||||
    if (!dataType.fieldAttributes) {
 | 
			
		||||
        return { regex: undefined, extractRegex: undefined };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const typeName = dataType.name;
 | 
			
		||||
    const fieldAttributes = dataType.fieldAttributes;
 | 
			
		||||
 | 
			
		||||
    if (fieldAttributes.hasCharMaxLength) {
 | 
			
		||||
        if (fieldAttributes.hasCharMaxLengthOption) {
 | 
			
		||||
            return {
 | 
			
		||||
                regex: `^${typeName}\\((\\d+|[mM][aA][xX])\\)$`,
 | 
			
		||||
                extractRegex: /\((\d+|max)\)/i,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        return {
 | 
			
		||||
            regex: `^${typeName}\\(\\d+\\)$`,
 | 
			
		||||
            extractRegex: /\((\d+)\)/,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (fieldAttributes.precision && fieldAttributes.scale) {
 | 
			
		||||
        return {
 | 
			
		||||
            regex: `^${typeName}\\s*\\(\\s*\\d+\\s*(?:,\\s*\\d+\\s*)?\\)$`,
 | 
			
		||||
            extractRegex: new RegExp(
 | 
			
		||||
                `${typeName}\\s*\\(\\s*(\\d+)\\s*(?:,\\s*(\\d+)\\s*)?\\)`
 | 
			
		||||
            ),
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (fieldAttributes.precision) {
 | 
			
		||||
        return {
 | 
			
		||||
            regex: `^${typeName}\\s*\\(\\s*\\d+\\s*\\)$`,
 | 
			
		||||
            extractRegex: /\((\d+)\)/,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { regex: undefined, extractRegex: undefined };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useUpdateTableField = (
 | 
			
		||||
    table: DBTable,
 | 
			
		||||
    field: DBField,
 | 
			
		||||
    customUpdateField?: (attrs: Partial<DBField>) => void
 | 
			
		||||
) => {
 | 
			
		||||
    const {
 | 
			
		||||
        databaseType,
 | 
			
		||||
        customTypes,
 | 
			
		||||
        updateField: chartDBUpdateField,
 | 
			
		||||
        removeField: chartDBRemoveField,
 | 
			
		||||
    } = useChartDB();
 | 
			
		||||
 | 
			
		||||
    // Local state for responsive UI
 | 
			
		||||
    const [localFieldName, setLocalFieldName] = useState(field.name);
 | 
			
		||||
    const [localNullable, setLocalNullable] = useState(field.nullable);
 | 
			
		||||
    const [localPrimaryKey, setLocalPrimaryKey] = useState(field.primaryKey);
 | 
			
		||||
 | 
			
		||||
    // Update local state when field properties change externally
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setLocalFieldName(field.name);
 | 
			
		||||
        setLocalNullable(field.nullable);
 | 
			
		||||
        setLocalPrimaryKey(field.primaryKey);
 | 
			
		||||
    }, [field.name, field.nullable, field.primaryKey]);
 | 
			
		||||
 | 
			
		||||
    // Use custom updateField if provided, otherwise use the chartDB one
 | 
			
		||||
    const updateField = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
            customUpdateField
 | 
			
		||||
                ? (
 | 
			
		||||
                      _tableId: string,
 | 
			
		||||
                      _fieldId: string,
 | 
			
		||||
                      attrs: Partial<DBField>
 | 
			
		||||
                  ) => customUpdateField(attrs)
 | 
			
		||||
                : chartDBUpdateField,
 | 
			
		||||
        [customUpdateField, chartDBUpdateField]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Calculate primary key fields for validation
 | 
			
		||||
    const primaryKeyFields = useMemo(() => {
 | 
			
		||||
        return table.fields.filter((f) => f.primaryKey);
 | 
			
		||||
    }, [table.fields]);
 | 
			
		||||
 | 
			
		||||
    const primaryKeyCount = useMemo(
 | 
			
		||||
        () => primaryKeyFields.length,
 | 
			
		||||
        [primaryKeyFields.length]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Generate data type options for select box
 | 
			
		||||
    const dataFieldOptions = useMemo(() => {
 | 
			
		||||
        const standardTypes: SelectBoxOption[] = sortedDataTypeMap[
 | 
			
		||||
            databaseType
 | 
			
		||||
        ].map((type) => {
 | 
			
		||||
            const regexPatterns = generateFieldRegexPatterns(type);
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                label: type.name,
 | 
			
		||||
                value: type.id,
 | 
			
		||||
                regex: regexPatterns.regex,
 | 
			
		||||
                extractRegex: regexPatterns.extractRegex,
 | 
			
		||||
                group: customTypes?.length ? 'Standard Types' : undefined,
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (!customTypes?.length) {
 | 
			
		||||
            return standardTypes;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Add custom types as options
 | 
			
		||||
        const customTypeOptions: SelectBoxOption[] = customTypes.map(
 | 
			
		||||
            (type) => ({
 | 
			
		||||
                label: type.name,
 | 
			
		||||
                value: type.name,
 | 
			
		||||
                description:
 | 
			
		||||
                    type.kind === 'enum' ? `${type.values?.join(' | ')}` : '',
 | 
			
		||||
                group: 'Custom Types',
 | 
			
		||||
            })
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return [...standardTypes, ...customTypeOptions];
 | 
			
		||||
    }, [databaseType, customTypes]);
 | 
			
		||||
 | 
			
		||||
    // Handle data type change
 | 
			
		||||
    const handleDataTypeChange = useCallback<
 | 
			
		||||
        NonNullable<SelectBoxProps['onChange']>
 | 
			
		||||
    >(
 | 
			
		||||
        (value, regexMatches) => {
 | 
			
		||||
            const dataType = sortedDataTypeMap[databaseType].find(
 | 
			
		||||
                (v) => v.id === value
 | 
			
		||||
            ) ?? {
 | 
			
		||||
                id: value as string,
 | 
			
		||||
                name: value as string,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let characterMaximumLength: string | undefined = undefined;
 | 
			
		||||
            let precision: number | undefined = undefined;
 | 
			
		||||
            let scale: number | undefined = undefined;
 | 
			
		||||
 | 
			
		||||
            if (regexMatches?.length) {
 | 
			
		||||
                if (dataType?.fieldAttributes?.hasCharMaxLength) {
 | 
			
		||||
                    characterMaximumLength = regexMatches[1]?.toLowerCase();
 | 
			
		||||
                } else if (
 | 
			
		||||
                    dataType?.fieldAttributes?.precision &&
 | 
			
		||||
                    dataType?.fieldAttributes?.scale
 | 
			
		||||
                ) {
 | 
			
		||||
                    precision = parseInt(regexMatches[1]);
 | 
			
		||||
                    scale = regexMatches[2]
 | 
			
		||||
                        ? parseInt(regexMatches[2])
 | 
			
		||||
                        : undefined;
 | 
			
		||||
                } else if (dataType?.fieldAttributes?.precision) {
 | 
			
		||||
                    precision = parseInt(regexMatches[1]);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                if (
 | 
			
		||||
                    dataType?.fieldAttributes?.hasCharMaxLength &&
 | 
			
		||||
                    field.characterMaximumLength
 | 
			
		||||
                ) {
 | 
			
		||||
                    characterMaximumLength = field.characterMaximumLength;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (dataType?.fieldAttributes?.precision && field.precision) {
 | 
			
		||||
                    precision = field.precision;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (dataType?.fieldAttributes?.scale && field.scale) {
 | 
			
		||||
                    scale = field.scale;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            updateField(table.id, field.id, {
 | 
			
		||||
                characterMaximumLength,
 | 
			
		||||
                precision,
 | 
			
		||||
                scale,
 | 
			
		||||
                increment: undefined,
 | 
			
		||||
                default: undefined,
 | 
			
		||||
                type: dataTypeDataToDataType(
 | 
			
		||||
                    dataType ?? {
 | 
			
		||||
                        id: value as string,
 | 
			
		||||
                        name: value as string,
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
            updateField,
 | 
			
		||||
            databaseType,
 | 
			
		||||
            field.characterMaximumLength,
 | 
			
		||||
            field.precision,
 | 
			
		||||
            field.scale,
 | 
			
		||||
            field.id,
 | 
			
		||||
            table.id,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Debounced update for field name
 | 
			
		||||
    const debouncedNameUpdate = useDebounce(
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (value: string) => {
 | 
			
		||||
                if (value.trim() !== field.name) {
 | 
			
		||||
                    updateField(table.id, field.id, { name: value });
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            [updateField, table.id, field.id, field.name]
 | 
			
		||||
        ),
 | 
			
		||||
        300 // 300ms debounce for text input
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Debounced update for nullable toggle
 | 
			
		||||
    const debouncedNullableUpdate = useDebounce(
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (value: boolean) => {
 | 
			
		||||
                updateField(table.id, field.id, { nullable: value });
 | 
			
		||||
            },
 | 
			
		||||
            [updateField, table.id, field.id]
 | 
			
		||||
        ),
 | 
			
		||||
        100 // 100ms debounce for toggle
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Debounced update for primary key toggle
 | 
			
		||||
    const debouncedPrimaryKeyUpdate = useDebounce(
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (value: boolean, primaryKeyCount: number) => {
 | 
			
		||||
                if (value) {
 | 
			
		||||
                    // When setting as primary key
 | 
			
		||||
                    const updates: Partial<DBField> = {
 | 
			
		||||
                        primaryKey: true,
 | 
			
		||||
                    };
 | 
			
		||||
                    // Only auto-set unique if this will be the only primary key
 | 
			
		||||
                    if (primaryKeyCount === 0) {
 | 
			
		||||
                        updates.unique = true;
 | 
			
		||||
                    }
 | 
			
		||||
                    updateField(table.id, field.id, updates);
 | 
			
		||||
                } else {
 | 
			
		||||
                    // When removing primary key
 | 
			
		||||
                    updateField(table.id, field.id, {
 | 
			
		||||
                        primaryKey: false,
 | 
			
		||||
                    });
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            [updateField, table.id, field.id]
 | 
			
		||||
        ),
 | 
			
		||||
        100 // 100ms debounce for toggle
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Handle primary key toggle with optimistic update
 | 
			
		||||
    const handlePrimaryKeyToggle = useCallback(
 | 
			
		||||
        (value: boolean) => {
 | 
			
		||||
            setLocalPrimaryKey(value);
 | 
			
		||||
            debouncedPrimaryKeyUpdate(value, primaryKeyCount);
 | 
			
		||||
        },
 | 
			
		||||
        [primaryKeyCount, debouncedPrimaryKeyUpdate]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Handle nullable toggle with optimistic update
 | 
			
		||||
    const handleNullableToggle = useCallback(
 | 
			
		||||
        (value: boolean) => {
 | 
			
		||||
            setLocalNullable(value);
 | 
			
		||||
            debouncedNullableUpdate(value);
 | 
			
		||||
        },
 | 
			
		||||
        [debouncedNullableUpdate]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Handle name change with optimistic update
 | 
			
		||||
    const handleNameChange = useCallback(
 | 
			
		||||
        (value: string) => {
 | 
			
		||||
            setLocalFieldName(value);
 | 
			
		||||
            debouncedNameUpdate(value);
 | 
			
		||||
        },
 | 
			
		||||
        [debouncedNameUpdate]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Utility function to generate field suffix for display
 | 
			
		||||
    const generateFieldSuffix = useCallback(
 | 
			
		||||
        (typeId?: string) => {
 | 
			
		||||
            return generateDBFieldSuffix(field, {
 | 
			
		||||
                databaseType,
 | 
			
		||||
                forceExtended: true,
 | 
			
		||||
                typeId,
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [field, databaseType]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const removeField = useCallback(() => {
 | 
			
		||||
        chartDBRemoveField(table.id, field.id);
 | 
			
		||||
    }, [chartDBRemoveField, table.id, field.id]);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        dataFieldOptions,
 | 
			
		||||
        handleDataTypeChange,
 | 
			
		||||
        handlePrimaryKeyToggle,
 | 
			
		||||
        handleNullableToggle,
 | 
			
		||||
        handleNameChange,
 | 
			
		||||
        generateFieldSuffix,
 | 
			
		||||
        primaryKeyCount,
 | 
			
		||||
        fieldName: localFieldName,
 | 
			
		||||
        nullable: localNullable,
 | 
			
		||||
        primaryKey: localPrimaryKey,
 | 
			
		||||
        removeField,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										42
									
								
								src/hooks/use-update-table.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,42 @@
 | 
			
		||||
import { useCallback, useState, useEffect } from 'react';
 | 
			
		||||
import { useChartDB } from './use-chartdb';
 | 
			
		||||
import { useDebounce } from './use-debounce-v2';
 | 
			
		||||
import type { DBTable } from '@/lib/domain';
 | 
			
		||||
 | 
			
		||||
// Hook for updating table properties with debouncing for performance
 | 
			
		||||
export const useUpdateTable = (table: DBTable) => {
 | 
			
		||||
    const { updateTable: chartDBUpdateTable } = useChartDB();
 | 
			
		||||
    const [localTableName, setLocalTableName] = useState(table.name);
 | 
			
		||||
 | 
			
		||||
    // Debounced update function
 | 
			
		||||
    const debouncedUpdate = useDebounce(
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (value: string) => {
 | 
			
		||||
                if (value.trim() && value.trim() !== table.name) {
 | 
			
		||||
                    chartDBUpdateTable(table.id, { name: value.trim() });
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            [chartDBUpdateTable, table.id, table.name]
 | 
			
		||||
        ),
 | 
			
		||||
        1000 // 1000ms debounce
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Update local state immediately for responsive UI
 | 
			
		||||
    const handleTableNameChange = useCallback(
 | 
			
		||||
        (value: string) => {
 | 
			
		||||
            setLocalTableName(value);
 | 
			
		||||
            debouncedUpdate(value);
 | 
			
		||||
        },
 | 
			
		||||
        [debouncedUpdate]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Update local state when table name changes externally
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setLocalTableName(table.name);
 | 
			
		||||
    }, [table.name]);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        tableName: localTableName,
 | 
			
		||||
        handleTableNameChange,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||