mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-11-03 21:43:23 +00:00 
			
		
		
		
	Compare commits
	
		
			72 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					467ff697c9 | ||
| 
						 | 
					d6919f3033 | ||
| 
						 | 
					56382a9fdc | ||
| 
						 | 
					e06eb2a48e | ||
| 
						 | 
					543b716c77 | ||
| 
						 | 
					b55d631146 | ||
| 
						 | 
					ef118929ad | ||
| 
						 | 
					68f48190c9 | ||
| 
						 | 
					bba265ad43 | ||
| 
						 | 
					cbc4e85a14 | ||
| 
						 | 
					26a0a5b550 | ||
| 
						 | 
					b935b7f251 | ||
| 
						 | 
					a1c0cf102a | ||
| 
						 | 
					ab89bad6d5 | ||
| 
						 | 
					deb218423f | ||
| 
						 | 
					48342471ac | ||
| 
						 | 
					47bb87a88f | ||
| 
						 | 
					a96c2e1078 | ||
| 
						 | 
					26d95eed25 | ||
| 
						 | 
					be65328f24 | ||
| 
						 | 
					85fd14fa02 | ||
| 
						 | 
					9c485b3b01 | ||
| 
						 | 
					e993f1549c | ||
| 
						 | 
					0db67ea42a | ||
| 
						 | 
					b9e621bd68 | ||
| 
						 | 
					93d59f8887 | ||
| 
						 | 
					190e4f4ffa | ||
| 
						 | 
					dc404c9d7e | ||
| 
						 | 
					dd4324d64f | ||
| 
						 | 
					1878083056 | ||
| 
						 | 
					7b6271962a | ||
| 
						 | 
					2edc8dfde8 | ||
| 
						 | 
					004d530880 | ||
| 
						 | 
					fd2cc9fcfc | ||
| 
						 | 
					4c93326bb6 | ||
| 
						 | 
					ef3d7a8b67 | ||
| 
						 | 
					3b3be086b1 | ||
| 
						 | 
					b424518212 | ||
| 
						 | 
					99a8201398 | ||
| 
						 | 
					eb9b41e4f6 | ||
| 
						 | 
					fef6d3f499 | ||
| 
						 | 
					14f11c27a7 | ||
| 
						 | 
					2118bce0f0 | ||
| 
						 | 
					88be6c1fd4 | ||
| 
						 | 
					0dcc9b9568 | ||
| 
						 | 
					ff3269ec05 | ||
| 
						 | 
					659dc2e3e7 | ||
| 
						 | 
					c36cd33180 | ||
| 
						 | 
					58231c9139 | ||
| 
						 | 
					1643e7bdeb | ||
| 
						 | 
					42d4cbac8c | ||
| 
						 | 
					7452ca6965 | ||
| 
						 | 
					27aede7794 | ||
| 
						 | 
					e9e2736cb2 | ||
| 
						 | 
					74c1730425 | ||
| 
						 | 
					94bed7fcce | ||
| 
						 | 
					8abf2a7bfc | ||
| 
						 | 
					ee659eaa03 | ||
| 
						 | 
					7c5db0848e | ||
| 
						 | 
					4b43f720e9 | ||
| 
						 | 
					766b5164b8 | ||
| 
						 | 
					7868ca9f42 | ||
| 
						 | 
					0411742864 | ||
| 
						 | 
					9831ac5a10 | ||
| 
						 | 
					91c6fb9249 | ||
| 
						 | 
					c155013668 | ||
| 
						 | 
					1b0f293c87 | ||
| 
						 | 
					df2dc03aa0 | ||
| 
						 | 
					205d431c89 | ||
| 
						 | 
					0abe18cdf9 | ||
| 
						 | 
					a151f56b5d | ||
| 
						 | 
					2b6b733261 | 
							
								
								
									
										101
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										101
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,5 +1,106 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## [1.8.1](https://github.com/chartdb/chartdb/compare/v1.8.0...v1.8.1) (2025-03-02)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* **add-docs:** add link to ChartDB documentation ([#597](https://github.com/chartdb/chartdb/issues/597)) ([b55d631](https://github.com/chartdb/chartdb/commit/b55d631146ff3a1f7d63c800d44b5d3d3a223c76))
 | 
			
		||||
* components config ([#591](https://github.com/chartdb/chartdb/issues/591)) ([cbc4e85](https://github.com/chartdb/chartdb/commit/cbc4e85a14e24a43f9ff470518f8fe2845046bdb))
 | 
			
		||||
* **docker config:** Environment Variable Handling and Configuration Logic ([#605](https://github.com/chartdb/chartdb/issues/605)) ([d6919f3](https://github.com/chartdb/chartdb/commit/d6919f30336cc846fe6e6505b5a5278aa14dcce6))
 | 
			
		||||
* **empty-state:** show diff buttons on import-dbml when triggered by empty ([#574](https://github.com/chartdb/chartdb/issues/574)) ([4834247](https://github.com/chartdb/chartdb/commit/48342471ac231922f2ca4455b74a9879127a54f1))
 | 
			
		||||
* **i18n:** add [FR] translation ([#579](https://github.com/chartdb/chartdb/issues/579)) ([ab89bad](https://github.com/chartdb/chartdb/commit/ab89bad6d544ba4c339a3360eeec7d29e5579511))
 | 
			
		||||
* **img-export:** add ChartDB watermark to exported image ([#588](https://github.com/chartdb/chartdb/issues/588)) ([b935b7f](https://github.com/chartdb/chartdb/commit/b935b7f25111d5f72b7f8d7c552a4ea5974f791e))
 | 
			
		||||
* **import-mssql:** fix import/export scripts to handle data correctly ([#598](https://github.com/chartdb/chartdb/issues/598)) ([e06eb2a](https://github.com/chartdb/chartdb/commit/e06eb2a48e6bd3bcf352f4bcf128214c7da4c1b1))
 | 
			
		||||
* **menu-backup:** update export to be backup ([#590](https://github.com/chartdb/chartdb/issues/590)) ([26a0a5b](https://github.com/chartdb/chartdb/commit/26a0a5b550ef5e47e89b00d0232dc98936f63f23))
 | 
			
		||||
* open create new diagram when there is no diagram ([#594](https://github.com/chartdb/chartdb/issues/594)) ([ef11892](https://github.com/chartdb/chartdb/commit/ef118929ad5d5cbfae0290061bd8ea30bd262496))
 | 
			
		||||
* **open diagram:** in case there is no diagram, opens the dialog ([#593](https://github.com/chartdb/chartdb/issues/593)) ([68f4819](https://github.com/chartdb/chartdb/commit/68f48190c93f155398cca15dd7af2a025de2d45f))
 | 
			
		||||
* **side-panel:** simplify how to add field and index ([#573](https://github.com/chartdb/chartdb/issues/573)) ([a1c0cf1](https://github.com/chartdb/chartdb/commit/a1c0cf102add4fb235e913e75078139b3961341b))
 | 
			
		||||
* **sql_server_export:** use sql server export ([#600](https://github.com/chartdb/chartdb/issues/600)) ([56382a9](https://github.com/chartdb/chartdb/commit/56382a9fdc5e3044f8811873dd8a79f590771896))
 | 
			
		||||
* **sqlite-import:** import nuallable columns correctly + add json type ([#571](https://github.com/chartdb/chartdb/issues/571)) ([deb2184](https://github.com/chartdb/chartdb/commit/deb218423f77f0c0945a93005696456f62b00ce3))
 | 
			
		||||
 | 
			
		||||
## [1.8.0](https://github.com/chartdb/chartdb/compare/v1.7.0...v1.8.0) (2025-02-13)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* **dbml-import:** add error highlighting for dbml imports ([#556](https://github.com/chartdb/chartdb/issues/556)) ([190e4f4](https://github.com/chartdb/chartdb/commit/190e4f4ffa834fa621f264dc608ca3f3b393a331))
 | 
			
		||||
* **docker image:** add support for custom inference servers ([#543](https://github.com/chartdb/chartdb/issues/543)) ([1878083](https://github.com/chartdb/chartdb/commit/1878083056ea4db7a05cdeeb38a4f7b9f5f95bd1))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* **canvas:** add right-click option to create relationships ([#568](https://github.com/chartdb/chartdb/issues/568)) ([e993f15](https://github.com/chartdb/chartdb/commit/e993f1549c4c86bb9e7e36062db803ba6613b3b3))
 | 
			
		||||
* **canvas:** locate table from canvas ([#560](https://github.com/chartdb/chartdb/issues/560)) ([dc404c9](https://github.com/chartdb/chartdb/commit/dc404c9d7ee272c93aac69646bac859829a5234e))
 | 
			
		||||
* **docker:** add option to hide popups ([#580](https://github.com/chartdb/chartdb/issues/580)) ([a96c2e1](https://github.com/chartdb/chartdb/commit/a96c2e107838d2dc13b586923fd9dbe06598cdd8))
 | 
			
		||||
* **export-sql:** show create script for only filtered schemas ([#570](https://github.com/chartdb/chartdb/issues/570)) ([85fd14f](https://github.com/chartdb/chartdb/commit/85fd14fa02bb2879c36bba53369dbf2e7fa578d4))
 | 
			
		||||
* **i18n:** fix Ukrainian ([#554](https://github.com/chartdb/chartdb/issues/554)) ([7b62719](https://github.com/chartdb/chartdb/commit/7b6271962a99bfe5ffbd0176e714c76368ef5c41))
 | 
			
		||||
* **import dbml:** add import for indexes ([#566](https://github.com/chartdb/chartdb/issues/566)) ([0db67ea](https://github.com/chartdb/chartdb/commit/0db67ea42a5f9585ca1d246db7a7ff0239bec0ba))
 | 
			
		||||
* **import-query:** improve the cleanup for messy json input ([#562](https://github.com/chartdb/chartdb/issues/562)) ([93d59f8](https://github.com/chartdb/chartdb/commit/93d59f8887765098d040a3184aaee32112f67267))
 | 
			
		||||
* **index unique:** extract unique toggle for faster editing ([#559](https://github.com/chartdb/chartdb/issues/559)) ([dd4324d](https://github.com/chartdb/chartdb/commit/dd4324d64f7638ada5c022a2ab38bd8e6986af25))
 | 
			
		||||
* **mssql-import:** improve script readability by adding edition comment ([#572](https://github.com/chartdb/chartdb/issues/572)) ([be65328](https://github.com/chartdb/chartdb/commit/be65328f24b0361638b9e2edb39eaa9906e77f67))
 | 
			
		||||
* **realtionships section:** add the schema to source/target tables ([#561](https://github.com/chartdb/chartdb/issues/561)) ([b9e621b](https://github.com/chartdb/chartdb/commit/b9e621bd680730a0ffbf1054d735bfa418711cae))
 | 
			
		||||
* **sqlserver-import:** open ssms guide when max chars ([#565](https://github.com/chartdb/chartdb/issues/565)) ([9c485b3](https://github.com/chartdb/chartdb/commit/9c485b3b01a131bf551c7e95916b0c416f6aa0b5))
 | 
			
		||||
* **table actions:** fix size of table actions ([#578](https://github.com/chartdb/chartdb/issues/578)) ([26d95ee](https://github.com/chartdb/chartdb/commit/26d95eed25d86452d9168a9d93a301ba50d934e3))
 | 
			
		||||
 | 
			
		||||
## [1.7.0](https://github.com/chartdb/chartdb/compare/v1.6.1...v1.7.0) (2025-02-03)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* **dbml-editor:** add dbml editor in side pannel ([#534](https://github.com/chartdb/chartdb/issues/534)) ([88be6c1](https://github.com/chartdb/chartdb/commit/88be6c1fd4a7e1f20937e8204c14d8fc1c2665b4))
 | 
			
		||||
* **import-dbml:** add import dbml functionality ([#549](https://github.com/chartdb/chartdb/issues/549)) ([b424518](https://github.com/chartdb/chartdb/commit/b424518212290a870fdb7c420a303f65f5901429))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* **canvas edit:** add option to edit names in canvas ([#536](https://github.com/chartdb/chartdb/issues/536)) ([0dcc9b9](https://github.com/chartdb/chartdb/commit/0dcc9b9568cfe749d44d2e93cb365ba3d3a1e71c))
 | 
			
		||||
* **dbml-editor:** add shortcuts to dbml and filter: [#534](https://github.com/chartdb/chartdb/issues/534) ([#535](https://github.com/chartdb/chartdb/issues/535)) ([3b3be08](https://github.com/chartdb/chartdb/commit/3b3be086b1e8d5acf999f8504580d9e2f956f7da))
 | 
			
		||||
* **dbml:** add error handling ([#545](https://github.com/chartdb/chartdb/issues/545)) ([fef6d3f](https://github.com/chartdb/chartdb/commit/fef6d3f4996130a3769d1f25b4b1f2090293a1bf))
 | 
			
		||||
* **empty-state:** fix dark-mode for empty-state ([#547](https://github.com/chartdb/chartdb/issues/547)) ([99a8201](https://github.com/chartdb/chartdb/commit/99a820139861546a012d7b562ddbb9b77698151a))
 | 
			
		||||
* **examples:** fix employee example dbml ([#544](https://github.com/chartdb/chartdb/issues/544)) ([2118bce](https://github.com/chartdb/chartdb/commit/2118bce0f00d55eb19d22b9fa2d4964ba2533a09))
 | 
			
		||||
* **i18n:** translation/Ukrainian ([#529](https://github.com/chartdb/chartdb/issues/529)) ([ff3269e](https://github.com/chartdb/chartdb/commit/ff3269ec0510bbae4bc114e65a1ea86a656e8785))
 | 
			
		||||
* **open-diagram:** add arrow keys navigation in open diagram dialog ([#537](https://github.com/chartdb/chartdb/issues/537)) ([14f11c2](https://github.com/chartdb/chartdb/commit/14f11c27a7ad5b990131c8495148cabf12835082))
 | 
			
		||||
* **performance:** fix bundle size ([#551](https://github.com/chartdb/chartdb/issues/551)) ([4c93326](https://github.com/chartdb/chartdb/commit/4c93326bb6e3eaa143373c500a0c641e95a53fb9))
 | 
			
		||||
* **performance:** reduce bundle size ([#553](https://github.com/chartdb/chartdb/issues/553)) ([004d530](https://github.com/chartdb/chartdb/commit/004d530880a50dea6e9786eb9ae63cf592a4d852))
 | 
			
		||||
* **performance:** resolve error on startup ([#552](https://github.com/chartdb/chartdb/issues/552)) ([fd2cc9f](https://github.com/chartdb/chartdb/commit/fd2cc9fcfc8f4a9f0bc79def47d89114159392fb))
 | 
			
		||||
* **psql-import:** remove typo for import command (psql) ([#546](https://github.com/chartdb/chartdb/issues/546)) ([eb9b41e](https://github.com/chartdb/chartdb/commit/eb9b41e4f656bec1451c45763f4ea5b547aeec5c))
 | 
			
		||||
* **scroll:** fix scroll area ([#550](https://github.com/chartdb/chartdb/issues/550)) ([ef3d7a8](https://github.com/chartdb/chartdb/commit/ef3d7a8b67431e923b75bf8287b86bbc8abe723b))
 | 
			
		||||
 | 
			
		||||
## [1.6.1](https://github.com/chartdb/chartdb/compare/v1.6.0...v1.6.1) (2025-01-26)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* change empty state image ([#531](https://github.com/chartdb/chartdb/issues/531)) ([42d4cba](https://github.com/chartdb/chartdb/commit/42d4cbac8ce352e0e4e155d7003bfb85296b897f))
 | 
			
		||||
* **chat-type:** remove typo of char datatype in examples ([#530](https://github.com/chartdb/chartdb/issues/530)) ([58231c9](https://github.com/chartdb/chartdb/commit/58231c91393de30ebff817f0ebc57a5c5579f106))
 | 
			
		||||
* **empty_state:** customize empty state ([#533](https://github.com/chartdb/chartdb/issues/533)) ([1643e7b](https://github.com/chartdb/chartdb/commit/1643e7bdeb1bbaf081ab064e871d102c87243c0a))
 | 
			
		||||
* **Image Export:** importing css rules error while download image ([#524](https://github.com/chartdb/chartdb/issues/524)) ([e9e2736](https://github.com/chartdb/chartdb/commit/e9e2736cb2203702d53df9afc30b8e989a8c9953))
 | 
			
		||||
* **shortcuts:** add zoom all shortcut ([#528](https://github.com/chartdb/chartdb/issues/528)) ([7452ca6](https://github.com/chartdb/chartdb/commit/7452ca6965b0332a93b686c397ddf51013e42506))
 | 
			
		||||
* **filter-tables:** show clean filter if no-results ([#532](https://github.com/chartdb/chartdb/issues/532)) ([c36cd33](https://github.com/chartdb/chartdb/commit/c36cd33180badaa9b7f9e27c765f19cb03a50ccd))
 | 
			
		||||
 | 
			
		||||
## [1.6.0](https://github.com/chartdb/chartdb/compare/v1.5.1...v1.6.0) (2025-01-02)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* **view-menu:** add toggle for mini map visibility ([#496](https://github.com/chartdb/chartdb/issues/496)) ([#505](https://github.com/chartdb/chartdb/issues/505)) ([8abf2a7](https://github.com/chartdb/chartdb/commit/8abf2a7bfcc36d39e60ac133b0e5e569de1bbc72))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* add loadDiagramFromData logic to chartdb provider ([#513](https://github.com/chartdb/chartdb/issues/513)) ([ee659ea](https://github.com/chartdb/chartdb/commit/ee659eaa038a94ee13801801e84152df4d79683d))
 | 
			
		||||
* **dependency:** upgrade react query to v7 - clean console warnings ([#504](https://github.com/chartdb/chartdb/issues/504)) ([7c5db08](https://github.com/chartdb/chartdb/commit/7c5db0848e49dfdb7e7120f77003d1e37f8d71b0))
 | 
			
		||||
* **i18n:** translation/Arabic ([#509](https://github.com/chartdb/chartdb/issues/509)) ([4b43f72](https://github.com/chartdb/chartdb/commit/4b43f720e90e49d5461e68d188e3865000f52497))
 | 
			
		||||
 | 
			
		||||
## [1.5.1](https://github.com/chartdb/chartdb/compare/v1.5.0...v1.5.1) (2024-12-15)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* **export:** fix SQL server field.nullable type to boolean ([#486](https://github.com/chartdb/chartdb/issues/486)) ([a151f56](https://github.com/chartdb/chartdb/commit/a151f56b5d950e0b5cc54363684ada95889024b3))
 | 
			
		||||
* **readme:** Update README.md - add CockroachDB ([#482](https://github.com/chartdb/chartdb/issues/482)) ([2b6b733](https://github.com/chartdb/chartdb/commit/2b6b73326155f18d6d56779c0657a3506e2d2cde))
 | 
			
		||||
 | 
			
		||||
## [1.5.0](https://github.com/chartdb/chartdb/compare/v1.4.0...v1.5.0) (2024-12-11)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -1,6 +1,9 @@
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
 | 
			
		||||
@@ -10,9 +13,13 @@ RUN npm ci
 | 
			
		||||
 | 
			
		||||
COPY . .
 | 
			
		||||
 | 
			
		||||
RUN echo "VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}" > .env && \
 | 
			
		||||
    echo "VITE_OPENAI_API_ENDPOINT=${VITE_OPENAI_API_ENDPOINT}" >> .env && \
 | 
			
		||||
    echo "VITE_LLM_MODEL_NAME=${VITE_LLM_MODEL_NAME}" >> .env && \
 | 
			
		||||
    echo "VITE_HIDE_BUCKLE_DOT_DEV=${VITE_HIDE_BUCKLE_DOT_DEV}" >> .env 
 | 
			
		||||
 | 
			
		||||
RUN npm run build
 | 
			
		||||
 | 
			
		||||
# Use a lightweight web server to serve the production build
 | 
			
		||||
FROM nginx:stable-alpine AS production
 | 
			
		||||
 | 
			
		||||
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
 | 
			
		||||
@@ -20,7 +27,6 @@ COPY ./default.conf.template /etc/nginx/conf.d/default.conf.template
 | 
			
		||||
COPY entrypoint.sh /entrypoint.sh
 | 
			
		||||
RUN chmod +x /entrypoint.sh
 | 
			
		||||
 | 
			
		||||
# Expose the default port for the Nginx web server
 | 
			
		||||
EXPOSE 80
 | 
			
		||||
 | 
			
		||||
ENTRYPOINT ["/entrypoint.sh"]
 | 
			
		||||
ENTRYPOINT ["/entrypoint.sh"]
 | 
			
		||||
							
								
								
									
										26
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								README.md
									
									
									
									
									
								
							@@ -68,6 +68,7 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif
 | 
			
		||||
-   ✅ SQL Server
 | 
			
		||||
-   ✅ MariaDB
 | 
			
		||||
-   ✅ SQLite
 | 
			
		||||
-   ✅ CockroachDB
 | 
			
		||||
-   ✅ ClickHouse
 | 
			
		||||
 | 
			
		||||
## Getting Started
 | 
			
		||||
@@ -106,8 +107,33 @@ docker build -t chartdb .
 | 
			
		||||
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 chartdb
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Using Custom Inference Server
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
# Build
 | 
			
		||||
docker build \
 | 
			
		||||
  --build-arg VITE_OPENAI_API_ENDPOINT=<YOUR_ENDPOINT> \
 | 
			
		||||
  --build-arg VITE_LLM_MODEL_NAME=<YOUR_MODEL_NAME> \
 | 
			
		||||
  -t chartdb .
 | 
			
		||||
 | 
			
		||||
# Run
 | 
			
		||||
docker run \
 | 
			
		||||
  -e OPENAI_API_ENDPOINT=<YOUR_ENDPOINT> \
 | 
			
		||||
  -e LLM_MODEL_NAME=<YOUR_MODEL_NAME> \
 | 
			
		||||
  -p 8080:80 chartdb
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
> **Note:** You must configure either Option 1 (OpenAI API key) OR Option 2 (Custom endpoint and model name) for AI capabilities to work. Do not mix the two options.
 | 
			
		||||
 | 
			
		||||
Open your browser and navigate to `http://localhost:8080`.
 | 
			
		||||
 | 
			
		||||
Example configuration for a local vLLM server:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
VITE_OPENAI_API_ENDPOINT=http://localhost:8000/v1
 | 
			
		||||
VITE_LLM_MODEL_NAME=Qwen/Qwen2.5-32B-Instruct-AWQ
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Try it on our website
 | 
			
		||||
 | 
			
		||||
1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,20 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "https://ui.shadcn.com/schema.json",
 | 
			
		||||
  "style": "new-york",
 | 
			
		||||
  "rsc": false,
 | 
			
		||||
  "tsx": true,
 | 
			
		||||
  "tailwind": {
 | 
			
		||||
    "config": "tailwind.config.js",
 | 
			
		||||
    "css": "src/globals.css",
 | 
			
		||||
    "baseColor": "slate",
 | 
			
		||||
    "cssVariables": true,
 | 
			
		||||
    "prefix": ""
 | 
			
		||||
  },
 | 
			
		||||
  "aliases": {
 | 
			
		||||
    "components": "src/components",
 | 
			
		||||
    "utils": "@/lib/utils"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
    "$schema": "https://ui.shadcn.com/schema.json",
 | 
			
		||||
    "style": "new-york",
 | 
			
		||||
    "rsc": false,
 | 
			
		||||
    "tsx": true,
 | 
			
		||||
    "tailwind": {
 | 
			
		||||
        "config": "tailwind.config.js",
 | 
			
		||||
        "css": "src/globals.css",
 | 
			
		||||
        "baseColor": "slate",
 | 
			
		||||
        "cssVariables": true,
 | 
			
		||||
        "prefix": ""
 | 
			
		||||
    },
 | 
			
		||||
    "aliases": {
 | 
			
		||||
        "components": "src/components",
 | 
			
		||||
        "utils": "src/lib/utils",
 | 
			
		||||
        "ui": "src/components/ui",
 | 
			
		||||
        "lib": "src/lib",
 | 
			
		||||
        "hooks": "src/hooks"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,12 @@ server {
 | 
			
		||||
 | 
			
		||||
    location /config.js {
 | 
			
		||||
        default_type application/javascript;
 | 
			
		||||
        return 200 "window.env = { OPENAI_API_KEY: \"$OPENAI_API_KEY\" };";
 | 
			
		||||
        return 200 "window.env = { 
 | 
			
		||||
            OPENAI_API_KEY: \"$OPENAI_API_KEY\",
 | 
			
		||||
            OPENAI_API_ENDPOINT: \"$OPENAI_API_ENDPOINT\",
 | 
			
		||||
            LLM_MODEL_NAME: \"$LLM_MODEL_NAME\",
 | 
			
		||||
            HIDE_BUCKLE_DOT_DEV: \"$HIDE_BUCKLE_DOT_DEV\"
 | 
			
		||||
        };";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    error_page   500 502 503 504  /50x.html;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
 | 
			
		||||
# Replace placeholders in nginx.conf
 | 
			
		||||
envsubst '${OPENAI_API_KEY}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
 | 
			
		||||
envsubst '${OPENAI_API_KEY} ${OPENAI_API_ENDPOINT} ${LLM_MODEL_NAME} ${HIDE_BUCKLE_DOT_DEV}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
 | 
			
		||||
 | 
			
		||||
# Start Nginx
 | 
			
		||||
nginx -g "daemon off;"
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ const compat = new FlatCompat({
 | 
			
		||||
 | 
			
		||||
export default [
 | 
			
		||||
    {
 | 
			
		||||
        ignores: ['**/dist', '**/.eslintrc.cjs', 'tailwind.config.js'],
 | 
			
		||||
        ignores: ['**/dist', '**/.eslintrc.cjs', '**/tailwind.config.js'],
 | 
			
		||||
        // files: ['**/*.ts', '**/*.tsx'],
 | 
			
		||||
    },
 | 
			
		||||
    ...fixupConfigRules(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4761
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4761
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "chartdb",
 | 
			
		||||
    "private": true,
 | 
			
		||||
    "version": "1.5.0",
 | 
			
		||||
    "version": "1.8.1",
 | 
			
		||||
    "type": "module",
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "dev": "vite",
 | 
			
		||||
@@ -13,6 +13,7 @@
 | 
			
		||||
    },
 | 
			
		||||
    "dependencies": {
 | 
			
		||||
        "@ai-sdk/openai": "^0.0.51",
 | 
			
		||||
        "@dbml/core": "^3.9.5",
 | 
			
		||||
        "@dnd-kit/sortable": "^8.0.0",
 | 
			
		||||
        "@monaco-editor/react": "^4.6.0",
 | 
			
		||||
        "@radix-ui/react-accordion": "^1.2.0",
 | 
			
		||||
@@ -28,10 +29,10 @@
 | 
			
		||||
        "@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.1.0",
 | 
			
		||||
        "@radix-ui/react-scroll-area": "1.2.0",
 | 
			
		||||
        "@radix-ui/react-select": "^2.1.1",
 | 
			
		||||
        "@radix-ui/react-separator": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-slot": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-slot": "^1.1.1",
 | 
			
		||||
        "@radix-ui/react-tabs": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-toast": "^1.2.1",
 | 
			
		||||
        "@radix-ui/react-toggle": "^1.1.0",
 | 
			
		||||
@@ -60,7 +61,7 @@
 | 
			
		||||
        "react-i18next": "^15.0.1",
 | 
			
		||||
        "react-resizable-panels": "^2.0.22",
 | 
			
		||||
        "react-responsive": "^10.0.0",
 | 
			
		||||
        "react-router-dom": "^6.26.0",
 | 
			
		||||
        "react-router-dom": "^7.1.1",
 | 
			
		||||
        "react-use": "^17.5.1",
 | 
			
		||||
        "tailwind-merge": "^2.4.0",
 | 
			
		||||
        "tailwindcss-animate": "^1.0.7",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								public/buckle-animated.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/buckle-animated.gif
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 404 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/buckle.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/buckle.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 28 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 6.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/empty_state_dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/empty_state_dark.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 6.1 KiB  | 
@@ -12,6 +12,14 @@ import { DarkTheme } from './themes/dark';
 | 
			
		||||
import { LightTheme } from './themes/light';
 | 
			
		||||
import './config.ts';
 | 
			
		||||
 | 
			
		||||
export const Editor = lazy(() =>
 | 
			
		||||
    import('./code-editor').then((module) => ({
 | 
			
		||||
        default: module.Editor,
 | 
			
		||||
    }))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
type EditorType = typeof Editor;
 | 
			
		||||
 | 
			
		||||
export interface CodeSnippetProps {
 | 
			
		||||
    className?: string;
 | 
			
		||||
    code: string;
 | 
			
		||||
@@ -19,14 +27,9 @@ export interface CodeSnippetProps {
 | 
			
		||||
    loading?: boolean;
 | 
			
		||||
    autoScroll?: boolean;
 | 
			
		||||
    isComplete?: boolean;
 | 
			
		||||
    editorProps?: React.ComponentProps<EditorType>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Editor = lazy(() =>
 | 
			
		||||
    import('./code-editor').then((module) => ({
 | 
			
		||||
        default: module.Editor,
 | 
			
		||||
    }))
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
    ({
 | 
			
		||||
        className,
 | 
			
		||||
@@ -35,6 +38,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
        language = 'sql',
 | 
			
		||||
        autoScroll = false,
 | 
			
		||||
        isComplete = true,
 | 
			
		||||
        editorProps,
 | 
			
		||||
    }) => {
 | 
			
		||||
        const { t } = useTranslation();
 | 
			
		||||
        const monaco = useMonaco();
 | 
			
		||||
@@ -144,27 +148,32 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
                            language={language}
 | 
			
		||||
                            loading={<Spinner />}
 | 
			
		||||
                            theme={effectiveTheme}
 | 
			
		||||
                            {...editorProps}
 | 
			
		||||
                            options={{
 | 
			
		||||
                                minimap: {
 | 
			
		||||
                                    enabled: false,
 | 
			
		||||
                                },
 | 
			
		||||
                                readOnly: true,
 | 
			
		||||
                                automaticLayout: true,
 | 
			
		||||
                                scrollbar: {
 | 
			
		||||
                                    vertical: 'hidden',
 | 
			
		||||
                                    horizontal: 'hidden',
 | 
			
		||||
                                    alwaysConsumeMouseWheel: false,
 | 
			
		||||
                                },
 | 
			
		||||
                                scrollBeyondLastLine: false,
 | 
			
		||||
                                renderValidationDecorations: 'off',
 | 
			
		||||
                                lineDecorationsWidth: 0,
 | 
			
		||||
                                overviewRulerBorder: false,
 | 
			
		||||
                                overviewRulerLanes: 0,
 | 
			
		||||
                                hideCursorInOverviewRuler: true,
 | 
			
		||||
                                contextmenu: false,
 | 
			
		||||
                                ...editorProps?.options,
 | 
			
		||||
                                guides: {
 | 
			
		||||
                                    indentation: false,
 | 
			
		||||
                                    ...editorProps?.options?.guides,
 | 
			
		||||
                                },
 | 
			
		||||
                                scrollbar: {
 | 
			
		||||
                                    vertical: 'hidden',
 | 
			
		||||
                                    horizontal: 'hidden',
 | 
			
		||||
                                    alwaysConsumeMouseWheel: false,
 | 
			
		||||
                                    ...editorProps?.options?.scrollbar,
 | 
			
		||||
                                },
 | 
			
		||||
                                minimap: {
 | 
			
		||||
                                    enabled: false,
 | 
			
		||||
                                    ...editorProps?.options?.minimap,
 | 
			
		||||
                                },
 | 
			
		||||
                                contextmenu: false,
 | 
			
		||||
                            }}
 | 
			
		||||
                        />
 | 
			
		||||
                        {!isComplete ? (
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										54
									
								
								src/components/code-snippet/languages/dbml-language.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/components/code-snippet/languages/dbml-language.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
import type { Monaco } from '@monaco-editor/react';
 | 
			
		||||
import { dataTypes } from '@/lib/data/data-types/data-types';
 | 
			
		||||
 | 
			
		||||
export const setupDBMLLanguage = (monaco: Monaco) => {
 | 
			
		||||
    monaco.languages.register({ id: 'dbml' });
 | 
			
		||||
 | 
			
		||||
    // Define themes for DBML
 | 
			
		||||
    monaco.editor.defineTheme('dbml-dark', {
 | 
			
		||||
        base: 'vs-dark',
 | 
			
		||||
        inherit: true,
 | 
			
		||||
        rules: [
 | 
			
		||||
            { token: 'keyword', foreground: '569CD6' }, // Table, Ref keywords
 | 
			
		||||
            { token: 'string', foreground: 'CE9178' }, // Strings
 | 
			
		||||
            { token: 'annotation', foreground: '9CDCFE' }, // [annotations]
 | 
			
		||||
            { token: 'delimiter', foreground: 'D4D4D4' }, // Braces {}
 | 
			
		||||
            { token: 'operator', foreground: 'D4D4D4' }, // Operators
 | 
			
		||||
            { token: 'datatype', foreground: '4EC9B0' }, // Data types
 | 
			
		||||
        ],
 | 
			
		||||
        colors: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    monaco.editor.defineTheme('dbml-light', {
 | 
			
		||||
        base: 'vs',
 | 
			
		||||
        inherit: true,
 | 
			
		||||
        rules: [
 | 
			
		||||
            { token: 'keyword', foreground: '0000FF' }, // Table, Ref keywords
 | 
			
		||||
            { token: 'string', foreground: 'A31515' }, // Strings
 | 
			
		||||
            { token: 'annotation', foreground: '001080' }, // [annotations]
 | 
			
		||||
            { token: 'delimiter', foreground: '000000' }, // Braces {}
 | 
			
		||||
            { token: 'operator', foreground: '000000' }, // Operators
 | 
			
		||||
            { token: 'type', foreground: '267F99' }, // Data types
 | 
			
		||||
        ],
 | 
			
		||||
        colors: {},
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const dataTypesNames = dataTypes.map((dt) => dt.name);
 | 
			
		||||
    const datatypePattern = dataTypesNames.join('|');
 | 
			
		||||
 | 
			
		||||
    monaco.languages.setMonarchTokensProvider('dbml', {
 | 
			
		||||
        keywords: ['Table', 'Ref', 'Indexes'],
 | 
			
		||||
        datatypes: dataTypesNames,
 | 
			
		||||
        tokenizer: {
 | 
			
		||||
            root: [
 | 
			
		||||
                [/\b(Table|Ref|Indexes)\b/, 'keyword'],
 | 
			
		||||
                [/\[.*?\]/, 'annotation'],
 | 
			
		||||
                [/".*?"/, 'string'],
 | 
			
		||||
                [/'.*?'/, 'string'],
 | 
			
		||||
                [/[{}]/, 'delimiter'],
 | 
			
		||||
                [/[<>]/, 'operator'],
 | 
			
		||||
                [new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
 | 
			
		||||
            ],
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
@@ -1,30 +1,66 @@
 | 
			
		||||
import React, { forwardRef } from 'react';
 | 
			
		||||
import EmptyStateImage from '@/assets/empty_state.png';
 | 
			
		||||
import EmptyStateImageDark from '@/assets/empty_state_dark.png';
 | 
			
		||||
import { Label } from '@/components/label/label';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import { useTheme } from '@/hooks/use-theme';
 | 
			
		||||
 | 
			
		||||
export interface EmptyStateProps {
 | 
			
		||||
    title: string;
 | 
			
		||||
    description: string;
 | 
			
		||||
    imageClassName?: string;
 | 
			
		||||
    titleClassName?: string;
 | 
			
		||||
    descriptionClassName?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const EmptyState = forwardRef<
 | 
			
		||||
    HTMLDivElement,
 | 
			
		||||
    React.HTMLAttributes<HTMLDivElement> & EmptyStateProps
 | 
			
		||||
>(({ title, description, className }, ref) => (
 | 
			
		||||
    <div
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        className={cn(
 | 
			
		||||
            'flex flex-1 flex-col items-center justify-center space-y-1',
 | 
			
		||||
            className
 | 
			
		||||
        )}
 | 
			
		||||
    >
 | 
			
		||||
        <img src={EmptyStateImage} alt="Empty state" className="w-32" />
 | 
			
		||||
        <Label className="text-base">{title}</Label>
 | 
			
		||||
        <Label className="text-sm font-normal text-muted-foreground">
 | 
			
		||||
            {description}
 | 
			
		||||
        </Label>
 | 
			
		||||
    </div>
 | 
			
		||||
));
 | 
			
		||||
>(
 | 
			
		||||
    (
 | 
			
		||||
        {
 | 
			
		||||
            title,
 | 
			
		||||
            description,
 | 
			
		||||
            className,
 | 
			
		||||
            titleClassName,
 | 
			
		||||
            descriptionClassName,
 | 
			
		||||
            imageClassName,
 | 
			
		||||
        },
 | 
			
		||||
        ref
 | 
			
		||||
    ) => {
 | 
			
		||||
        const { effectiveTheme } = useTheme();
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div
 | 
			
		||||
                ref={ref}
 | 
			
		||||
                className={cn(
 | 
			
		||||
                    'flex flex-1 flex-col items-center justify-center space-y-1',
 | 
			
		||||
                    className
 | 
			
		||||
                )}
 | 
			
		||||
            >
 | 
			
		||||
                <img
 | 
			
		||||
                    src={
 | 
			
		||||
                        effectiveTheme === 'dark'
 | 
			
		||||
                            ? EmptyStateImageDark
 | 
			
		||||
                            : EmptyStateImage
 | 
			
		||||
                    }
 | 
			
		||||
                    alt="Empty state"
 | 
			
		||||
                    className={cn('mb-2 w-20', imageClassName)}
 | 
			
		||||
                />
 | 
			
		||||
                <Label className={cn('text-base', titleClassName)}>
 | 
			
		||||
                    {title}
 | 
			
		||||
                </Label>
 | 
			
		||||
                <Label
 | 
			
		||||
                    className={cn(
 | 
			
		||||
                        'text-sm font-normal text-muted-foreground',
 | 
			
		||||
                        descriptionClassName
 | 
			
		||||
                    )}
 | 
			
		||||
                >
 | 
			
		||||
                    {description}
 | 
			
		||||
                </Label>
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
EmptyState.displayName = 'EmptyState';
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								src/context/alert-context/alert-context.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/context/alert-context/alert-context.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
import { createContext, useContext } from 'react';
 | 
			
		||||
import { emptyFn } from '@/lib/utils';
 | 
			
		||||
import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
 | 
			
		||||
 | 
			
		||||
export interface AlertContext {
 | 
			
		||||
    showAlert: (params: BaseAlertDialogProps) => void;
 | 
			
		||||
    closeAlert: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const alertContext = createContext<AlertContext>({
 | 
			
		||||
    closeAlert: emptyFn,
 | 
			
		||||
    showAlert: emptyFn,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const useAlert = () => useContext(alertContext);
 | 
			
		||||
							
								
								
									
										36
									
								
								src/context/alert-context/alert-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								src/context/alert-context/alert-provider.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,36 @@
 | 
			
		||||
import React, { useCallback, useState } from 'react';
 | 
			
		||||
import type { AlertContext } from './alert-context';
 | 
			
		||||
import { alertContext } from './alert-context';
 | 
			
		||||
import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
 | 
			
		||||
import { BaseAlertDialog } from '@/dialogs/base-alert-dialog/base-alert-dialog';
 | 
			
		||||
 | 
			
		||||
export const AlertProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
}) => {
 | 
			
		||||
    const [showAlert, setShowAlert] = useState(false);
 | 
			
		||||
    const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({
 | 
			
		||||
        title: '',
 | 
			
		||||
    });
 | 
			
		||||
    const showAlertHandler: AlertContext['showAlert'] = useCallback(
 | 
			
		||||
        (params) => {
 | 
			
		||||
            setAlertParams(params);
 | 
			
		||||
            setShowAlert(true);
 | 
			
		||||
        },
 | 
			
		||||
        [setShowAlert, setAlertParams]
 | 
			
		||||
    );
 | 
			
		||||
    const closeAlertHandler = useCallback(() => {
 | 
			
		||||
        setShowAlert(false);
 | 
			
		||||
    }, [setShowAlert]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <alertContext.Provider
 | 
			
		||||
            value={{
 | 
			
		||||
                showAlert: showAlertHandler,
 | 
			
		||||
                closeAlert: closeAlertHandler,
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
            <BaseAlertDialog dialog={{ open: showAlert }} {...alertParams} />
 | 
			
		||||
        </alertContext.Provider>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										22
									
								
								src/context/canvas-context/canvas-context.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/context/canvas-context/canvas-context.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
import { createContext } from 'react';
 | 
			
		||||
import { emptyFn } from '@/lib/utils';
 | 
			
		||||
import type { Graph } from '@/lib/graph';
 | 
			
		||||
import { createGraph } from '@/lib/graph';
 | 
			
		||||
 | 
			
		||||
export interface CanvasContext {
 | 
			
		||||
    reorderTables: (options?: { updateHistory?: boolean }) => void;
 | 
			
		||||
    fitView: (options?: {
 | 
			
		||||
        duration?: number;
 | 
			
		||||
        padding?: number;
 | 
			
		||||
        maxZoom?: number;
 | 
			
		||||
    }) => void;
 | 
			
		||||
    setOverlapGraph: (graph: Graph<string>) => void;
 | 
			
		||||
    overlapGraph: Graph<string>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const canvasContext = createContext<CanvasContext>({
 | 
			
		||||
    reorderTables: emptyFn,
 | 
			
		||||
    fitView: emptyFn,
 | 
			
		||||
    setOverlapGraph: emptyFn,
 | 
			
		||||
    overlapGraph: createGraph(),
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										85
									
								
								src/context/canvas-context/canvas-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/context/canvas-context/canvas-provider.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
import React, { type ReactNode, useCallback, useState } from 'react';
 | 
			
		||||
import { canvasContext } from './canvas-context';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import {
 | 
			
		||||
    adjustTablePositions,
 | 
			
		||||
    shouldShowTablesBySchemaFilter,
 | 
			
		||||
} from '@/lib/domain/db-table';
 | 
			
		||||
import { useReactFlow } from '@xyflow/react';
 | 
			
		||||
import { findOverlappingTables } from '@/pages/editor-page/canvas/canvas-utils';
 | 
			
		||||
import type { Graph } from '@/lib/graph';
 | 
			
		||||
import { createGraph } from '@/lib/graph';
 | 
			
		||||
 | 
			
		||||
interface CanvasProviderProps {
 | 
			
		||||
    children: ReactNode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CanvasProvider = ({ children }: CanvasProviderProps) => {
 | 
			
		||||
    const { tables, relationships, updateTablesState, filteredSchemas } =
 | 
			
		||||
        useChartDB();
 | 
			
		||||
    const { fitView } = useReactFlow();
 | 
			
		||||
    const [overlapGraph, setOverlapGraph] =
 | 
			
		||||
        useState<Graph<string>>(createGraph());
 | 
			
		||||
 | 
			
		||||
    const reorderTables = useCallback(
 | 
			
		||||
        (
 | 
			
		||||
            options: { updateHistory?: boolean } = {
 | 
			
		||||
                updateHistory: true,
 | 
			
		||||
            }
 | 
			
		||||
        ) => {
 | 
			
		||||
            const newTables = adjustTablePositions({
 | 
			
		||||
                relationships,
 | 
			
		||||
                tables: tables.filter((table) =>
 | 
			
		||||
                    shouldShowTablesBySchemaFilter(table, filteredSchemas)
 | 
			
		||||
                ),
 | 
			
		||||
                mode: 'all', // Use 'all' mode for manual reordering
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const updatedOverlapGraph = findOverlappingTables({
 | 
			
		||||
                tables: newTables,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            updateTablesState(
 | 
			
		||||
                (currentTables) =>
 | 
			
		||||
                    currentTables.map((table) => {
 | 
			
		||||
                        const newTable = newTables.find(
 | 
			
		||||
                            (t) => t.id === table.id
 | 
			
		||||
                        );
 | 
			
		||||
                        return {
 | 
			
		||||
                            id: table.id,
 | 
			
		||||
                            x: newTable?.x ?? table.x,
 | 
			
		||||
                            y: newTable?.y ?? table.y,
 | 
			
		||||
                        };
 | 
			
		||||
                    }),
 | 
			
		||||
                {
 | 
			
		||||
                    updateHistory: options.updateHistory ?? true,
 | 
			
		||||
                    forceOverride: false,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            setOverlapGraph(updatedOverlapGraph);
 | 
			
		||||
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                fitView({
 | 
			
		||||
                    duration: 500,
 | 
			
		||||
                    padding: 0.2,
 | 
			
		||||
                    maxZoom: 0.8,
 | 
			
		||||
                });
 | 
			
		||||
            }, 500);
 | 
			
		||||
        },
 | 
			
		||||
        [filteredSchemas, relationships, tables, updateTablesState, fitView]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <canvasContext.Provider
 | 
			
		||||
            value={{
 | 
			
		||||
                reorderTables,
 | 
			
		||||
                fitView,
 | 
			
		||||
                setOverlapGraph,
 | 
			
		||||
                overlapGraph,
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
        </canvasContext.Provider>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -84,6 +84,7 @@ export interface ChartDBContext {
 | 
			
		||||
        options?: { updateHistory: boolean }
 | 
			
		||||
    ) => Promise<void>;
 | 
			
		||||
    loadDiagram: (diagramId: string) => Promise<Diagram | undefined>;
 | 
			
		||||
    loadDiagramFromData: (diagram: Diagram) => void;
 | 
			
		||||
    updateDiagramUpdatedAt: () => Promise<void>;
 | 
			
		||||
    clearDiagramData: () => Promise<void>;
 | 
			
		||||
    deleteDiagram: () => Promise<void>;
 | 
			
		||||
@@ -246,6 +247,7 @@ export const chartDBContext = createContext<ChartDBContext>({
 | 
			
		||||
    updateDiagramName: emptyFn,
 | 
			
		||||
    updateDiagramUpdatedAt: emptyFn,
 | 
			
		||||
    loadDiagram: emptyFn,
 | 
			
		||||
    loadDiagramFromData: emptyFn,
 | 
			
		||||
    clearDiagramData: emptyFn,
 | 
			
		||||
    deleteDiagram: emptyFn,
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1336,15 +1336,9 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
 | 
			
		||||
        async (diagramId: string) => {
 | 
			
		||||
            const diagram = await db.getDiagram(diagramId, {
 | 
			
		||||
                includeRelationships: true,
 | 
			
		||||
                includeTables: true,
 | 
			
		||||
                includeDependencies: true,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diagram) {
 | 
			
		||||
    const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            async (diagram) => {
 | 
			
		||||
                setDiagramId(diagram.id);
 | 
			
		||||
                setDiagramName(diagram.name);
 | 
			
		||||
                setDatabaseType(diagram.databaseType);
 | 
			
		||||
@@ -1356,23 +1350,36 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                setDiagramUpdatedAt(diagram.updatedAt);
 | 
			
		||||
 | 
			
		||||
                events.emit({ action: 'load_diagram', data: { diagram } });
 | 
			
		||||
            },
 | 
			
		||||
            [
 | 
			
		||||
                setDiagramId,
 | 
			
		||||
                setDiagramName,
 | 
			
		||||
                setDatabaseType,
 | 
			
		||||
                setDatabaseEdition,
 | 
			
		||||
                setTables,
 | 
			
		||||
                setRelationships,
 | 
			
		||||
                setDependencies,
 | 
			
		||||
                setDiagramCreatedAt,
 | 
			
		||||
                setDiagramUpdatedAt,
 | 
			
		||||
                events,
 | 
			
		||||
            ]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
 | 
			
		||||
        async (diagramId: string) => {
 | 
			
		||||
            const diagram = await db.getDiagram(diagramId, {
 | 
			
		||||
                includeRelationships: true,
 | 
			
		||||
                includeTables: true,
 | 
			
		||||
                includeDependencies: true,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diagram) {
 | 
			
		||||
                loadDiagramFromData(diagram);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return diagram;
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
            db,
 | 
			
		||||
            setDiagramId,
 | 
			
		||||
            setDiagramName,
 | 
			
		||||
            setDatabaseType,
 | 
			
		||||
            setDatabaseEdition,
 | 
			
		||||
            setTables,
 | 
			
		||||
            setRelationships,
 | 
			
		||||
            setDependencies,
 | 
			
		||||
            setDiagramCreatedAt,
 | 
			
		||||
            setDiagramUpdatedAt,
 | 
			
		||||
            events,
 | 
			
		||||
        ]
 | 
			
		||||
        [db, loadDiagramFromData]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
@@ -1393,6 +1400,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                updateDiagramId,
 | 
			
		||||
                updateDiagramName,
 | 
			
		||||
                loadDiagram,
 | 
			
		||||
                loadDiagramFromData,
 | 
			
		||||
                updateDatabaseType,
 | 
			
		||||
                updateDatabaseEdition,
 | 
			
		||||
                clearDiagramData,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,14 @@
 | 
			
		||||
import { createContext } from 'react';
 | 
			
		||||
import { emptyFn } from '@/lib/utils';
 | 
			
		||||
import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
 | 
			
		||||
import type { TableSchemaDialogProps } from '@/dialogs/table-schema-dialog/table-schema-dialog';
 | 
			
		||||
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
 | 
			
		||||
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
 | 
			
		||||
import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog';
 | 
			
		||||
import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
 | 
			
		||||
import type { 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';
 | 
			
		||||
 | 
			
		||||
export interface DialogContext {
 | 
			
		||||
    // Create diagram dialog
 | 
			
		||||
@@ -14,19 +16,19 @@ export interface DialogContext {
 | 
			
		||||
    closeCreateDiagramDialog: () => void;
 | 
			
		||||
 | 
			
		||||
    // Open diagram dialog
 | 
			
		||||
    openOpenDiagramDialog: () => void;
 | 
			
		||||
    openOpenDiagramDialog: (
 | 
			
		||||
        params?: Omit<OpenDiagramDialogProps, 'dialog'>
 | 
			
		||||
    ) => void;
 | 
			
		||||
    closeOpenDiagramDialog: () => void;
 | 
			
		||||
 | 
			
		||||
    // Export SQL dialog
 | 
			
		||||
    openExportSQLDialog: (params: Omit<ExportSQLDialogProps, 'dialog'>) => void;
 | 
			
		||||
    closeExportSQLDialog: () => void;
 | 
			
		||||
 | 
			
		||||
    // Alert dialog
 | 
			
		||||
    showAlert: (params: BaseAlertDialogProps) => void;
 | 
			
		||||
    closeAlert: () => void;
 | 
			
		||||
 | 
			
		||||
    // Create relationship dialog
 | 
			
		||||
    openCreateRelationshipDialog: () => void;
 | 
			
		||||
    openCreateRelationshipDialog: (
 | 
			
		||||
        params?: Omit<CreateRelationshipDialogProps, 'dialog'>
 | 
			
		||||
    ) => void;
 | 
			
		||||
    closeCreateRelationshipDialog: () => void;
 | 
			
		||||
 | 
			
		||||
    // Import database dialog
 | 
			
		||||
@@ -45,6 +47,10 @@ export interface DialogContext {
 | 
			
		||||
    openStarUsDialog: () => void;
 | 
			
		||||
    closeStarUsDialog: () => void;
 | 
			
		||||
 | 
			
		||||
    // Buckle dialog
 | 
			
		||||
    openBuckleDialog: () => void;
 | 
			
		||||
    closeBuckleDialog: () => void;
 | 
			
		||||
 | 
			
		||||
    // Export image dialog
 | 
			
		||||
    openExportImageDialog: (
 | 
			
		||||
        params: Omit<ExportImageDialogProps, 'dialog'>
 | 
			
		||||
@@ -62,6 +68,12 @@ 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>({
 | 
			
		||||
@@ -71,8 +83,6 @@ export const dialogContext = createContext<DialogContext>({
 | 
			
		||||
    closeOpenDiagramDialog: emptyFn,
 | 
			
		||||
    openExportSQLDialog: emptyFn,
 | 
			
		||||
    closeExportSQLDialog: emptyFn,
 | 
			
		||||
    closeAlert: emptyFn,
 | 
			
		||||
    showAlert: emptyFn,
 | 
			
		||||
    closeCreateRelationshipDialog: emptyFn,
 | 
			
		||||
    openCreateRelationshipDialog: emptyFn,
 | 
			
		||||
    openImportDatabaseDialog: emptyFn,
 | 
			
		||||
@@ -87,4 +97,8 @@ export const dialogContext = createContext<DialogContext>({
 | 
			
		||||
    closeExportDiagramDialog: emptyFn,
 | 
			
		||||
    openImportDiagramDialog: emptyFn,
 | 
			
		||||
    closeImportDiagramDialog: emptyFn,
 | 
			
		||||
    openBuckleDialog: emptyFn,
 | 
			
		||||
    closeBuckleDialog: emptyFn,
 | 
			
		||||
    openImportDBMLDialog: emptyFn,
 | 
			
		||||
    closeImportDBMLDialog: emptyFn,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,12 @@ import React, { useCallback, useState } from 'react';
 | 
			
		||||
import type { DialogContext } from './dialog-context';
 | 
			
		||||
import { dialogContext } from './dialog-context';
 | 
			
		||||
import { CreateDiagramDialog } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
 | 
			
		||||
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
 | 
			
		||||
import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
 | 
			
		||||
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
 | 
			
		||||
import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
 | 
			
		||||
import { BaseAlertDialog } from '@/dialogs/base-alert-dialog/base-alert-dialog';
 | 
			
		||||
import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
 | 
			
		||||
import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
 | 
			
		||||
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
 | 
			
		||||
import { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog';
 | 
			
		||||
@@ -19,16 +19,42 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
 | 
			
		||||
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
 | 
			
		||||
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
 | 
			
		||||
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
 | 
			
		||||
import { BuckleDialog } from '@/dialogs/buckle-dialog/buckle-dialog';
 | 
			
		||||
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
 | 
			
		||||
import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
 | 
			
		||||
 | 
			
		||||
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
}) => {
 | 
			
		||||
    const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false);
 | 
			
		||||
    const [openOpenDiagramDialog, setOpenOpenDiagramDialog] = useState(false);
 | 
			
		||||
    const [openDiagramDialogParams, setOpenDiagramDialogParams] =
 | 
			
		||||
        useState<Omit<OpenDiagramDialogProps, 'dialog'>>();
 | 
			
		||||
 | 
			
		||||
    const openOpenDiagramDialogHandler: DialogContext['openOpenDiagramDialog'] =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (props) => {
 | 
			
		||||
                setOpenDiagramDialogParams(props);
 | 
			
		||||
                setOpenOpenDiagramDialog(true);
 | 
			
		||||
            },
 | 
			
		||||
            [setOpenOpenDiagramDialog]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] =
 | 
			
		||||
        useState(false);
 | 
			
		||||
    const [createRelationshipDialogParams, setCreateRelationshipDialogParams] =
 | 
			
		||||
        useState<Omit<CreateRelationshipDialogProps, 'dialog'>>();
 | 
			
		||||
    const openCreateRelationshipDialogHandler: DialogContext['openCreateRelationshipDialog'] =
 | 
			
		||||
        useCallback(
 | 
			
		||||
            (params) => {
 | 
			
		||||
                setCreateRelationshipDialogParams(params);
 | 
			
		||||
                setOpenCreateRelationshipDialog(true);
 | 
			
		||||
            },
 | 
			
		||||
            [setOpenCreateRelationshipDialog]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const [openStarUsDialog, setOpenStarUsDialog] = useState(false);
 | 
			
		||||
    const [openBuckleDialog, setOpenBuckleDialog] = useState(false);
 | 
			
		||||
 | 
			
		||||
    // Export image dialog
 | 
			
		||||
    const [openExportImageDialog, setOpenExportImageDialog] = useState(false);
 | 
			
		||||
@@ -88,7 +114,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            [setOpenTableSchemaDialog]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    // Export image dialog
 | 
			
		||||
    // Export diagram dialog
 | 
			
		||||
    const [openExportDiagramDialog, setOpenExportDiagramDialog] =
 | 
			
		||||
        useState(false);
 | 
			
		||||
 | 
			
		||||
@@ -96,35 +122,22 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    const [openImportDiagramDialog, setOpenImportDiagramDialog] =
 | 
			
		||||
        useState(false);
 | 
			
		||||
 | 
			
		||||
    // Alert dialog
 | 
			
		||||
    const [showAlert, setShowAlert] = useState(false);
 | 
			
		||||
    const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({
 | 
			
		||||
        title: '',
 | 
			
		||||
    });
 | 
			
		||||
    const showAlertHandler: DialogContext['showAlert'] = useCallback(
 | 
			
		||||
        (params) => {
 | 
			
		||||
            setAlertParams(params);
 | 
			
		||||
            setShowAlert(true);
 | 
			
		||||
        },
 | 
			
		||||
        [setShowAlert, setAlertParams]
 | 
			
		||||
    );
 | 
			
		||||
    const closeAlertHandler = useCallback(() => {
 | 
			
		||||
        setShowAlert(false);
 | 
			
		||||
    }, [setShowAlert]);
 | 
			
		||||
    // Import DBML dialog
 | 
			
		||||
    const [openImportDBMLDialog, setOpenImportDBMLDialog] = useState(false);
 | 
			
		||||
    const [importDBMLDialogParams, setImportDBMLDialogParams] =
 | 
			
		||||
        useState<Omit<ImportDBMLDialogProps, 'dialog'>>();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <dialogContext.Provider
 | 
			
		||||
            value={{
 | 
			
		||||
                openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
 | 
			
		||||
                closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
 | 
			
		||||
                openOpenDiagramDialog: () => setOpenOpenDiagramDialog(true),
 | 
			
		||||
                openOpenDiagramDialog: openOpenDiagramDialogHandler,
 | 
			
		||||
                closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
 | 
			
		||||
                openExportSQLDialog: openExportSQLDialogHandler,
 | 
			
		||||
                closeExportSQLDialog: () => setOpenExportSQLDialog(false),
 | 
			
		||||
                showAlert: showAlertHandler,
 | 
			
		||||
                closeAlert: closeAlertHandler,
 | 
			
		||||
                openCreateRelationshipDialog: () =>
 | 
			
		||||
                    setOpenCreateRelationshipDialog(true),
 | 
			
		||||
                openCreateRelationshipDialog:
 | 
			
		||||
                    openCreateRelationshipDialogHandler,
 | 
			
		||||
                closeCreateRelationshipDialog: () =>
 | 
			
		||||
                    setOpenCreateRelationshipDialog(false),
 | 
			
		||||
                openImportDatabaseDialog: openImportDatabaseDialogHandler,
 | 
			
		||||
@@ -134,6 +147,8 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                closeTableSchemaDialog: () => setOpenTableSchemaDialog(false),
 | 
			
		||||
                openStarUsDialog: () => setOpenStarUsDialog(true),
 | 
			
		||||
                closeStarUsDialog: () => setOpenStarUsDialog(false),
 | 
			
		||||
                closeBuckleDialog: () => setOpenBuckleDialog(false),
 | 
			
		||||
                openBuckleDialog: () => setOpenBuckleDialog(true),
 | 
			
		||||
                closeExportImageDialog: () => setOpenExportImageDialog(false),
 | 
			
		||||
                openExportImageDialog: openExportImageDialogHandler,
 | 
			
		||||
                openExportDiagramDialog: () => setOpenExportDiagramDialog(true),
 | 
			
		||||
@@ -142,18 +157,26 @@ 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 }} />
 | 
			
		||||
            <OpenDiagramDialog dialog={{ open: openOpenDiagramDialog }} />
 | 
			
		||||
            <OpenDiagramDialog
 | 
			
		||||
                dialog={{ open: openOpenDiagramDialog }}
 | 
			
		||||
                {...openDiagramDialogParams}
 | 
			
		||||
            />
 | 
			
		||||
            <ExportSQLDialog
 | 
			
		||||
                dialog={{ open: openExportSQLDialog }}
 | 
			
		||||
                {...exportSQLDialogParams}
 | 
			
		||||
            />
 | 
			
		||||
            <BaseAlertDialog dialog={{ open: showAlert }} {...alertParams} />
 | 
			
		||||
            <CreateRelationshipDialog
 | 
			
		||||
                dialog={{ open: openCreateRelationshipDialog }}
 | 
			
		||||
                {...createRelationshipDialogParams}
 | 
			
		||||
            />
 | 
			
		||||
            <ImportDatabaseDialog
 | 
			
		||||
                dialog={{ open: openImportDatabaseDialog }}
 | 
			
		||||
@@ -170,6 +193,11 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            />
 | 
			
		||||
            <ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
 | 
			
		||||
            <ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
 | 
			
		||||
            <BuckleDialog dialog={{ open: openBuckleDialog }} />
 | 
			
		||||
            <ImportDBMLDialog
 | 
			
		||||
                dialog={{ open: openImportDBMLDialog }}
 | 
			
		||||
                {...importDBMLDialogParams}
 | 
			
		||||
            />
 | 
			
		||||
        </dialogContext.Provider>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import React, { useCallback, useMemo } from 'react';
 | 
			
		||||
import React, { useCallback, useMemo, useEffect, useState } from 'react';
 | 
			
		||||
import type { ExportImageContext, ImageType } from './export-image-context';
 | 
			
		||||
import { exportImageContext } from './export-image-context';
 | 
			
		||||
import { toJpeg, toPng, toSvg } from 'html-to-image';
 | 
			
		||||
@@ -6,6 +6,8 @@ import { useReactFlow } from '@xyflow/react';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
@@ -14,6 +16,24 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    const { setNodes, getViewport } = useReactFlow();
 | 
			
		||||
    const { effectiveTheme } = useTheme();
 | 
			
		||||
    const { diagramName } = useChartDB();
 | 
			
		||||
    const [logoBase64, setLogoBase64] = useState<string>('');
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        // Convert logo to base64 on component mount
 | 
			
		||||
        const img = new Image();
 | 
			
		||||
        img.src = effectiveTheme === 'light' ? logoLight : logoDark;
 | 
			
		||||
        img.onload = () => {
 | 
			
		||||
            const canvas = document.createElement('canvas');
 | 
			
		||||
            canvas.width = img.width;
 | 
			
		||||
            canvas.height = img.height;
 | 
			
		||||
            const ctx = canvas.getContext('2d');
 | 
			
		||||
            if (ctx) {
 | 
			
		||||
                ctx.drawImage(img, 0, 0);
 | 
			
		||||
                const base64 = canvas.toDataURL('image/png');
 | 
			
		||||
                setLogoBase64(base64);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }, [effectiveTheme]);
 | 
			
		||||
 | 
			
		||||
    const downloadImage = useCallback(
 | 
			
		||||
        (dataUrl: string, type: ImageType) => {
 | 
			
		||||
@@ -128,16 +148,22 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                    'http://www.w3.org/2000/svg',
 | 
			
		||||
                    'rect'
 | 
			
		||||
                );
 | 
			
		||||
                const padding = 2000;
 | 
			
		||||
                backgroundRect.setAttribute('x', String(-viewport.x - padding));
 | 
			
		||||
                backgroundRect.setAttribute('y', String(-viewport.y - padding));
 | 
			
		||||
                const bgPadding = 2000;
 | 
			
		||||
                backgroundRect.setAttribute(
 | 
			
		||||
                    'x',
 | 
			
		||||
                    String(-viewport.x - bgPadding)
 | 
			
		||||
                );
 | 
			
		||||
                backgroundRect.setAttribute(
 | 
			
		||||
                    'y',
 | 
			
		||||
                    String(-viewport.y - bgPadding)
 | 
			
		||||
                );
 | 
			
		||||
                backgroundRect.setAttribute(
 | 
			
		||||
                    'width',
 | 
			
		||||
                    String(reactFlowBounds.width + 2 * padding)
 | 
			
		||||
                    String(reactFlowBounds.width + 2 * bgPadding)
 | 
			
		||||
                );
 | 
			
		||||
                backgroundRect.setAttribute(
 | 
			
		||||
                    'height',
 | 
			
		||||
                    String(reactFlowBounds.height + 2 * padding)
 | 
			
		||||
                    String(reactFlowBounds.height + 2 * bgPadding)
 | 
			
		||||
                );
 | 
			
		||||
                backgroundRect.setAttribute('fill', 'url(#background-pattern)');
 | 
			
		||||
                tempSvg.appendChild(backgroundRect);
 | 
			
		||||
@@ -148,27 +174,110 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                try {
 | 
			
		||||
                    const dataUrl = await imageCreateFn(viewportElement, {
 | 
			
		||||
                        ...(type === 'jpeg' || type === 'png'
 | 
			
		||||
                            ? {
 | 
			
		||||
                                  backgroundColor:
 | 
			
		||||
                                      effectiveTheme === 'light'
 | 
			
		||||
                                          ? '#ffffff'
 | 
			
		||||
                                          : '#141414',
 | 
			
		||||
                              }
 | 
			
		||||
                            : {}),
 | 
			
		||||
                        width: reactFlowBounds.width,
 | 
			
		||||
                        height: reactFlowBounds.height,
 | 
			
		||||
                        style: {
 | 
			
		||||
                            width: `${reactFlowBounds.width}px`,
 | 
			
		||||
                            height: `${reactFlowBounds.height}px`,
 | 
			
		||||
                            transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
 | 
			
		||||
                        },
 | 
			
		||||
                        quality: 1,
 | 
			
		||||
                        pixelRatio: scale,
 | 
			
		||||
                    });
 | 
			
		||||
                    // Handle SVG export differently
 | 
			
		||||
                    if (type === 'svg') {
 | 
			
		||||
                        const dataUrl = await imageCreateFn(viewportElement, {
 | 
			
		||||
                            width: reactFlowBounds.width,
 | 
			
		||||
                            height: reactFlowBounds.height,
 | 
			
		||||
                            style: {
 | 
			
		||||
                                width: `${reactFlowBounds.width}px`,
 | 
			
		||||
                                height: `${reactFlowBounds.height}px`,
 | 
			
		||||
                                transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
 | 
			
		||||
                            },
 | 
			
		||||
                            quality: 1,
 | 
			
		||||
                            pixelRatio: scale,
 | 
			
		||||
                            skipFonts: true,
 | 
			
		||||
                        });
 | 
			
		||||
                        downloadImage(dataUrl, type);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    downloadImage(dataUrl, type);
 | 
			
		||||
                    // For PNG and JPEG, continue with the watermark process
 | 
			
		||||
                    const initialDataUrl = await imageCreateFn(
 | 
			
		||||
                        viewportElement,
 | 
			
		||||
                        {
 | 
			
		||||
                            backgroundColor:
 | 
			
		||||
                                effectiveTheme === 'light'
 | 
			
		||||
                                    ? '#ffffff'
 | 
			
		||||
                                    : '#141414',
 | 
			
		||||
                            width: reactFlowBounds.width,
 | 
			
		||||
                            height: reactFlowBounds.height,
 | 
			
		||||
                            style: {
 | 
			
		||||
                                width: `${reactFlowBounds.width}px`,
 | 
			
		||||
                                height: `${reactFlowBounds.height}px`,
 | 
			
		||||
                                transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
 | 
			
		||||
                            },
 | 
			
		||||
                            quality: 1,
 | 
			
		||||
                            pixelRatio: scale,
 | 
			
		||||
                            skipFonts: true,
 | 
			
		||||
                        }
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    // Create a canvas to combine the diagram and watermark
 | 
			
		||||
                    const canvas = document.createElement('canvas');
 | 
			
		||||
                    const ctx = canvas.getContext('2d');
 | 
			
		||||
 | 
			
		||||
                    if (!ctx) {
 | 
			
		||||
                        downloadImage(initialDataUrl, type);
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    // Set canvas size to match the export size
 | 
			
		||||
                    canvas.width = reactFlowBounds.width * scale;
 | 
			
		||||
                    canvas.height = reactFlowBounds.height * scale;
 | 
			
		||||
 | 
			
		||||
                    // Load the exported diagram
 | 
			
		||||
                    const diagramImage = new Image();
 | 
			
		||||
                    diagramImage.src = initialDataUrl;
 | 
			
		||||
 | 
			
		||||
                    await new Promise((resolve) => {
 | 
			
		||||
                        diagramImage.onload = async () => {
 | 
			
		||||
                            // Draw the diagram
 | 
			
		||||
                            ctx.drawImage(diagramImage, 0, 0);
 | 
			
		||||
 | 
			
		||||
                            // Calculate logo size
 | 
			
		||||
                            const logoHeight = Math.max(
 | 
			
		||||
                                24,
 | 
			
		||||
                                Math.floor(canvas.width * 0.024)
 | 
			
		||||
                            );
 | 
			
		||||
                            const padding = Math.max(
 | 
			
		||||
                                12,
 | 
			
		||||
                                Math.floor(logoHeight * 0.5)
 | 
			
		||||
                            );
 | 
			
		||||
 | 
			
		||||
                            // Load and draw the logo
 | 
			
		||||
                            const logoImage = new Image();
 | 
			
		||||
                            logoImage.src = logoBase64;
 | 
			
		||||
 | 
			
		||||
                            await new Promise((resolve) => {
 | 
			
		||||
                                logoImage.onload = () => {
 | 
			
		||||
                                    // Calculate logo width while maintaining aspect ratio
 | 
			
		||||
                                    const logoWidth =
 | 
			
		||||
                                        (logoImage.width / logoImage.height) *
 | 
			
		||||
                                        logoHeight;
 | 
			
		||||
 | 
			
		||||
                                    // Draw logo in bottom-left corner
 | 
			
		||||
                                    ctx.globalAlpha = 0.9;
 | 
			
		||||
                                    ctx.drawImage(
 | 
			
		||||
                                        logoImage,
 | 
			
		||||
                                        padding,
 | 
			
		||||
                                        canvas.height - logoHeight - padding,
 | 
			
		||||
                                        logoWidth,
 | 
			
		||||
                                        logoHeight
 | 
			
		||||
                                    );
 | 
			
		||||
                                    ctx.globalAlpha = 1;
 | 
			
		||||
                                    resolve(null);
 | 
			
		||||
                                };
 | 
			
		||||
                            });
 | 
			
		||||
 | 
			
		||||
                            // Convert canvas to data URL
 | 
			
		||||
                            const finalDataUrl = canvas.toDataURL(
 | 
			
		||||
                                type === 'png' ? 'image/png' : 'image/jpeg'
 | 
			
		||||
                            );
 | 
			
		||||
                            downloadImage(finalDataUrl, type);
 | 
			
		||||
                            resolve(null);
 | 
			
		||||
                        };
 | 
			
		||||
                    });
 | 
			
		||||
                } finally {
 | 
			
		||||
                    viewportElement.removeChild(tempSvg);
 | 
			
		||||
                    hideLoader();
 | 
			
		||||
@@ -183,6 +292,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            setNodes,
 | 
			
		||||
            showLoader,
 | 
			
		||||
            effectiveTheme,
 | 
			
		||||
            logoBase64,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import { useHistory } from '@/hooks/use-history';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useLayout } from '@/hooks/use-layout';
 | 
			
		||||
import { useReactFlow } from '@xyflow/react';
 | 
			
		||||
 | 
			
		||||
export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
@@ -17,6 +18,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    const { openOpenDiagramDialog } = useDialog();
 | 
			
		||||
    const { updateDiagramUpdatedAt } = useChartDB();
 | 
			
		||||
    const { toggleSidePanel } = useLayout();
 | 
			
		||||
    const { fitView } = useReactFlow();
 | 
			
		||||
 | 
			
		||||
    useHotkeys(
 | 
			
		||||
        keyboardShortcutsForOS[KeyboardShortcutAction.REDO].keyCombination,
 | 
			
		||||
@@ -37,7 +39,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    useHotkeys(
 | 
			
		||||
        keyboardShortcutsForOS[KeyboardShortcutAction.OPEN_DIAGRAM]
 | 
			
		||||
            .keyCombination,
 | 
			
		||||
        openOpenDiagramDialog,
 | 
			
		||||
        () => openOpenDiagramDialog(),
 | 
			
		||||
        {
 | 
			
		||||
            preventDefault: true,
 | 
			
		||||
        },
 | 
			
		||||
@@ -61,6 +63,20 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        },
 | 
			
		||||
        [toggleSidePanel]
 | 
			
		||||
    );
 | 
			
		||||
    useHotkeys(
 | 
			
		||||
        keyboardShortcutsForOS[KeyboardShortcutAction.SHOW_ALL].keyCombination,
 | 
			
		||||
        () => {
 | 
			
		||||
            fitView({
 | 
			
		||||
                duration: 500,
 | 
			
		||||
                padding: 0.1,
 | 
			
		||||
                maxZoom: 0.8,
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            preventDefault: true,
 | 
			
		||||
        },
 | 
			
		||||
        [fitView]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <keyboardShortcutsContext.Provider value={{}}>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ export enum KeyboardShortcutAction {
 | 
			
		||||
    OPEN_DIAGRAM = 'open_diagram',
 | 
			
		||||
    SAVE_DIAGRAM = 'save_diagram',
 | 
			
		||||
    TOGGLE_SIDE_PANEL = 'toggle_side_panel',
 | 
			
		||||
    SHOW_ALL = 'show_all',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface KeyboardShortcut {
 | 
			
		||||
@@ -55,6 +56,13 @@ export const keyboardShortcuts: Record<
 | 
			
		||||
        keyCombinationMac: 'meta+b',
 | 
			
		||||
        keyCombinationWin: 'ctrl+b',
 | 
			
		||||
    },
 | 
			
		||||
    [KeyboardShortcutAction.SHOW_ALL]: {
 | 
			
		||||
        action: KeyboardShortcutAction.SHOW_ALL,
 | 
			
		||||
        keyCombinationLabelMac: '⌘0',
 | 
			
		||||
        keyCombinationLabelWin: 'Ctrl+0',
 | 
			
		||||
        keyCombinationMac: 'meta+0',
 | 
			
		||||
        keyCombinationWin: 'ctrl+0',
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface KeyboardShortcutForOS {
 | 
			
		||||
 
 | 
			
		||||
@@ -30,8 +30,17 @@ export interface LocalConfigContext {
 | 
			
		||||
    starUsDialogLastOpen: number;
 | 
			
		||||
    setStarUsDialogLastOpen: (lastOpen: number) => void;
 | 
			
		||||
 | 
			
		||||
    buckleWaitlistOpened: boolean;
 | 
			
		||||
    setBuckleWaitlistOpened: (githubRepoOpened: boolean) => void;
 | 
			
		||||
 | 
			
		||||
    buckleDialogLastOpen: number;
 | 
			
		||||
    setBuckleDialogLastOpen: (lastOpen: number) => void;
 | 
			
		||||
 | 
			
		||||
    showDependenciesOnCanvas: boolean;
 | 
			
		||||
    setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
 | 
			
		||||
 | 
			
		||||
    showMiniMapOnCanvas: boolean;
 | 
			
		||||
    setShowMiniMapOnCanvas: (showMiniMapOnCanvas: boolean) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const LocalConfigContext = createContext<LocalConfigContext>({
 | 
			
		||||
@@ -56,6 +65,15 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
 | 
			
		||||
    starUsDialogLastOpen: 0,
 | 
			
		||||
    setStarUsDialogLastOpen: emptyFn,
 | 
			
		||||
 | 
			
		||||
    buckleWaitlistOpened: false,
 | 
			
		||||
    setBuckleWaitlistOpened: emptyFn,
 | 
			
		||||
 | 
			
		||||
    buckleDialogLastOpen: 0,
 | 
			
		||||
    setBuckleDialogLastOpen: emptyFn,
 | 
			
		||||
 | 
			
		||||
    showDependenciesOnCanvas: false,
 | 
			
		||||
    setShowDependenciesOnCanvas: emptyFn,
 | 
			
		||||
 | 
			
		||||
    showMiniMapOnCanvas: false,
 | 
			
		||||
    setShowMiniMapOnCanvas: emptyFn,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,10 @@ const showCardinalityKey = 'show_cardinality';
 | 
			
		||||
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
 | 
			
		||||
const githubRepoOpenedKey = 'github_repo_opened';
 | 
			
		||||
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
 | 
			
		||||
const buckleWaitlistOpenedKey = 'buckle_waitlist_opened';
 | 
			
		||||
const buckleDialogLastOpenKey = 'buckle_dialog_last_open';
 | 
			
		||||
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
 | 
			
		||||
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
 | 
			
		||||
 | 
			
		||||
export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
@@ -48,12 +51,28 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const [buckleWaitlistOpened, setBuckleWaitlistOpened] =
 | 
			
		||||
        React.useState<boolean>(
 | 
			
		||||
            (localStorage.getItem(buckleWaitlistOpenedKey) || 'false') ===
 | 
			
		||||
                'true'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const [buckleDialogLastOpen, setBuckleDialogLastOpen] =
 | 
			
		||||
        React.useState<number>(
 | 
			
		||||
            parseInt(localStorage.getItem(buckleDialogLastOpenKey) || '0')
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
 | 
			
		||||
        React.useState<boolean>(
 | 
			
		||||
            (localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
 | 
			
		||||
                'true'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const [showMiniMapOnCanvas, setShowMiniMapOnCanvas] =
 | 
			
		||||
        React.useState<boolean>(
 | 
			
		||||
            (localStorage.getItem(showMiniMapOnCanvasKey) || 'true') === 'true'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        localStorage.setItem(
 | 
			
		||||
            starUsDialogLastOpenKey,
 | 
			
		||||
@@ -65,6 +84,20 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
 | 
			
		||||
    }, [githubRepoOpened]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        localStorage.setItem(
 | 
			
		||||
            buckleDialogLastOpenKey,
 | 
			
		||||
            buckleDialogLastOpen.toString()
 | 
			
		||||
        );
 | 
			
		||||
    }, [buckleDialogLastOpen]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        localStorage.setItem(
 | 
			
		||||
            buckleWaitlistOpenedKey,
 | 
			
		||||
            buckleWaitlistOpened.toString()
 | 
			
		||||
        );
 | 
			
		||||
    }, [buckleWaitlistOpened]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        localStorage.setItem(
 | 
			
		||||
            hideMultiSchemaNotificationKey,
 | 
			
		||||
@@ -95,6 +128,13 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        );
 | 
			
		||||
    }, [showDependenciesOnCanvas]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        localStorage.setItem(
 | 
			
		||||
            showMiniMapOnCanvasKey,
 | 
			
		||||
            showMiniMapOnCanvas.toString()
 | 
			
		||||
        );
 | 
			
		||||
    }, [showMiniMapOnCanvas]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <LocalConfigContext.Provider
 | 
			
		||||
            value={{
 | 
			
		||||
@@ -114,6 +154,12 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                setStarUsDialogLastOpen,
 | 
			
		||||
                showDependenciesOnCanvas,
 | 
			
		||||
                setShowDependenciesOnCanvas,
 | 
			
		||||
                setBuckleDialogLastOpen,
 | 
			
		||||
                buckleDialogLastOpen,
 | 
			
		||||
                buckleWaitlistOpened,
 | 
			
		||||
                setBuckleWaitlistOpened,
 | 
			
		||||
                showMiniMapOnCanvas,
 | 
			
		||||
                setShowMiniMapOnCanvas,
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
 
 | 
			
		||||
@@ -134,6 +134,20 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        config: '++id, defaultDiagramId',
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    db.version(9).upgrade((tx) =>
 | 
			
		||||
        tx
 | 
			
		||||
            .table<DBTable & { diagramId: string }>('db_tables')
 | 
			
		||||
            .toCollection()
 | 
			
		||||
            .modify((table) => {
 | 
			
		||||
                for (const field of table.fields) {
 | 
			
		||||
                    if (typeof field.nullable === 'string') {
 | 
			
		||||
                        field.nullable =
 | 
			
		||||
                            (field.nullable as string).toLowerCase() === 'true';
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    db.on('ready', async () => {
 | 
			
		||||
        const config = await getConfig();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ import {
 | 
			
		||||
    AlertDialogTitle,
 | 
			
		||||
} from '@/components/alert-dialog/alert-dialog';
 | 
			
		||||
import type { AlertDialogProps } from '@radix-ui/react-alert-dialog';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import { useAlert } from '@/context/alert-context/alert-context';
 | 
			
		||||
 | 
			
		||||
export interface BaseAlertDialogProps {
 | 
			
		||||
    title: string;
 | 
			
		||||
@@ -33,7 +33,7 @@ export const BaseAlertDialog: React.FC<BaseAlertDialogProps> = ({
 | 
			
		||||
    content,
 | 
			
		||||
    onClose,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { closeAlert } = useDialog();
 | 
			
		||||
    const { closeAlert } = useAlert();
 | 
			
		||||
 | 
			
		||||
    const closeAlertHandler = useCallback(() => {
 | 
			
		||||
        onClose?.();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										80
									
								
								src/dialogs/buckle-dialog/buckle-dialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/dialogs/buckle-dialog/buckle-dialog.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
import React, { useCallback, useEffect } from 'react';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import {
 | 
			
		||||
    Dialog,
 | 
			
		||||
    DialogClose,
 | 
			
		||||
    DialogContent,
 | 
			
		||||
    DialogDescription,
 | 
			
		||||
    DialogFooter,
 | 
			
		||||
    DialogHeader,
 | 
			
		||||
    DialogTitle,
 | 
			
		||||
} from '@/components/dialog/dialog';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
			
		||||
import { useLocalConfig } from '@/hooks/use-local-config';
 | 
			
		||||
import { useTheme } from '@/hooks/use-theme';
 | 
			
		||||
 | 
			
		||||
export interface BuckleDialogProps extends BaseDialogProps {}
 | 
			
		||||
 | 
			
		||||
export const BuckleDialog: React.FC<BuckleDialogProps> = ({ dialog }) => {
 | 
			
		||||
    const { setBuckleWaitlistOpened } = useLocalConfig();
 | 
			
		||||
    const { effectiveTheme } = useTheme();
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!dialog.open) return;
 | 
			
		||||
    }, [dialog.open]);
 | 
			
		||||
    const { closeBuckleDialog } = useDialog();
 | 
			
		||||
 | 
			
		||||
    const handleConfirm = useCallback(() => {
 | 
			
		||||
        setBuckleWaitlistOpened(true);
 | 
			
		||||
        window.open('https://waitlist.buckle.dev', '_blank');
 | 
			
		||||
    }, [setBuckleWaitlistOpened]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Dialog
 | 
			
		||||
            {...dialog}
 | 
			
		||||
            onOpenChange={(open) => {
 | 
			
		||||
                if (!open) {
 | 
			
		||||
                    closeBuckleDialog();
 | 
			
		||||
                }
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent
 | 
			
		||||
                className="flex flex-col"
 | 
			
		||||
                showClose={false}
 | 
			
		||||
                onInteractOutside={(e) => {
 | 
			
		||||
                    e.preventDefault();
 | 
			
		||||
                }}
 | 
			
		||||
            >
 | 
			
		||||
                <DialogHeader>
 | 
			
		||||
                    <DialogTitle className="hidden" />
 | 
			
		||||
                    <DialogDescription className="hidden" />
 | 
			
		||||
                </DialogHeader>
 | 
			
		||||
                <div className="flex w-full flex-col items-center">
 | 
			
		||||
                    <img
 | 
			
		||||
                        src={
 | 
			
		||||
                            effectiveTheme === 'light'
 | 
			
		||||
                                ? '/buckle-animated.gif'
 | 
			
		||||
                                : '/buckle.png'
 | 
			
		||||
                        }
 | 
			
		||||
                        className="h-16"
 | 
			
		||||
                    />
 | 
			
		||||
                    <div className="mt-6 text-center text-base">
 | 
			
		||||
                        We've been working on something big -{' '}
 | 
			
		||||
                        <span className="font-semibold">Ready to explore?</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <DialogFooter className="flex gap-1 md:justify-between">
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
                        <Button variant="secondary">Not now</Button>
 | 
			
		||||
                    </DialogClose>
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
                        <Button onClick={handleConfirm}>
 | 
			
		||||
                            Try ChartDB v2.0!
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </DialogClose>
 | 
			
		||||
                </DialogFooter>
 | 
			
		||||
            </DialogContent>
 | 
			
		||||
        </Dialog>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -85,6 +85,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
    const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
 | 
			
		||||
    const [isCheckingJson, setIsCheckingJson] = useState(false);
 | 
			
		||||
 | 
			
		||||
    const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        const loadScripts = async () => {
 | 
			
		||||
            const { importMetadataScripts } = await import(
 | 
			
		||||
@@ -127,6 +129,11 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
        (e: React.ChangeEvent<HTMLTextAreaElement>) => {
 | 
			
		||||
            const inputValue = e.target.value;
 | 
			
		||||
            setScriptResult(inputValue);
 | 
			
		||||
 | 
			
		||||
            // Automatically open SSMS info when input length is exactly 65535
 | 
			
		||||
            if (inputValue.length === 65535) {
 | 
			
		||||
                setShowSSMSInfoDialog(true);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [setScriptResult]
 | 
			
		||||
    );
 | 
			
		||||
@@ -245,7 +252,10 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
                                {t('new_diagram_dialog.import_database.step_1')}
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {databaseType === DatabaseType.SQL_SERVER && (
 | 
			
		||||
                                <SSMSInfo />
 | 
			
		||||
                                <SSMSInfo
 | 
			
		||||
                                    open={showSSMSInfoDialog}
 | 
			
		||||
                                    setOpen={setShowSSMSInfoDialog}
 | 
			
		||||
                                />
 | 
			
		||||
                            )}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {databaseTypeToClientsMap[databaseType].length > 0 ? (
 | 
			
		||||
@@ -369,6 +379,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
        showCheckJsonButton,
 | 
			
		||||
        isCheckingJson,
 | 
			
		||||
        handleCheckJson,
 | 
			
		||||
        showSSMSInfoDialog,
 | 
			
		||||
        setShowSSMSInfoDialog,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const renderFooter = useCallback(() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,32 +4,55 @@ import {
 | 
			
		||||
    HoverCardTrigger,
 | 
			
		||||
} from '@/components/hover-card/hover-card';
 | 
			
		||||
import { Label } from '@/components/label/label';
 | 
			
		||||
import { Info } from 'lucide-react';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Info, X } from 'lucide-react';
 | 
			
		||||
import React, { useCallback, useEffect, useMemo } from 'react';
 | 
			
		||||
import SSMSInstructions from '@/assets/ssms-instructions.png';
 | 
			
		||||
import { ZoomableImage } from '@/components/zoomable-image/zoomable-image';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
 | 
			
		||||
export interface SSMSInfoProps {}
 | 
			
		||||
export interface SSMSInfoProps {
 | 
			
		||||
    open?: boolean;
 | 
			
		||||
    setOpen?: (open: boolean) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SSMSInfo = React.forwardRef<
 | 
			
		||||
    React.ElementRef<typeof HoverCardTrigger>,
 | 
			
		||||
    SSMSInfoProps
 | 
			
		||||
>((props, ref) => {
 | 
			
		||||
>(({ open: controlledOpen, setOpen: setControlledOpen }, ref) => {
 | 
			
		||||
    const [open, setOpen] = React.useState(false);
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (controlledOpen) {
 | 
			
		||||
            setOpen(true);
 | 
			
		||||
        }
 | 
			
		||||
    }, [controlledOpen]);
 | 
			
		||||
 | 
			
		||||
    const closeHandler = useCallback(() => {
 | 
			
		||||
        setOpen(false);
 | 
			
		||||
        setControlledOpen?.(false);
 | 
			
		||||
    }, [setControlledOpen]);
 | 
			
		||||
 | 
			
		||||
    const isOpen = useMemo(
 | 
			
		||||
        () => open || controlledOpen,
 | 
			
		||||
        [open, controlledOpen]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <HoverCard
 | 
			
		||||
            open={open}
 | 
			
		||||
            open={isOpen}
 | 
			
		||||
            onOpenChange={(isOpen) => {
 | 
			
		||||
                if (controlledOpen) {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
                setOpen(isOpen);
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <HoverCardTrigger ref={ref} {...props} asChild>
 | 
			
		||||
            <HoverCardTrigger ref={ref} asChild>
 | 
			
		||||
                <div
 | 
			
		||||
                    className="flex flex-row items-center gap-1 text-pink-600"
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                        setOpen(!open);
 | 
			
		||||
                        setOpen?.(!open);
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    <Info size={14} />
 | 
			
		||||
@@ -41,13 +64,21 @@ export const SSMSInfo = React.forwardRef<
 | 
			
		||||
                </div>
 | 
			
		||||
            </HoverCardTrigger>
 | 
			
		||||
            <HoverCardContent className="w-80">
 | 
			
		||||
                <div className="flex">
 | 
			
		||||
                    <div className="space-y-1">
 | 
			
		||||
                <div className="flex flex-col">
 | 
			
		||||
                    <div className="flex items-start justify-between">
 | 
			
		||||
                        <h4 className="text-sm font-semibold">
 | 
			
		||||
                            {t(
 | 
			
		||||
                                'new_diagram_dialog.import_database.ssms_instructions.title'
 | 
			
		||||
                            )}
 | 
			
		||||
                        </h4>
 | 
			
		||||
                        <button
 | 
			
		||||
                            onClick={closeHandler}
 | 
			
		||||
                            className="text-muted-foreground hover:text-foreground"
 | 
			
		||||
                        >
 | 
			
		||||
                            <X size={16} />
 | 
			
		||||
                        </button>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="space-y-1">
 | 
			
		||||
                        <p className="text-xs text-muted-foreground">
 | 
			
		||||
                            <span className="font-semibold">1. </span>
 | 
			
		||||
                            {t(
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
    const [databaseType, setDatabaseType] = useState<DatabaseType>(
 | 
			
		||||
        DatabaseType.GENERIC
 | 
			
		||||
    );
 | 
			
		||||
    const { closeCreateDiagramDialog } = useDialog();
 | 
			
		||||
    const { closeCreateDiagramDialog, openImportDBMLDialog } = useDialog();
 | 
			
		||||
    const { updateConfig } = useConfig();
 | 
			
		||||
    const [scriptResult, setScriptResult] = useState('');
 | 
			
		||||
    const [databaseEdition, setDatabaseEdition] = useState<
 | 
			
		||||
@@ -104,6 +104,10 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
        await updateConfig({ defaultDiagramId: diagram.id });
 | 
			
		||||
        closeCreateDiagramDialog();
 | 
			
		||||
        navigate(`/diagrams/${diagram.id}`);
 | 
			
		||||
        setTimeout(
 | 
			
		||||
            () => openImportDBMLDialog({ withCreateEmptyDiagram: true }),
 | 
			
		||||
            700
 | 
			
		||||
        );
 | 
			
		||||
    }, [
 | 
			
		||||
        databaseType,
 | 
			
		||||
        addDiagram,
 | 
			
		||||
@@ -112,6 +116,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
        navigate,
 | 
			
		||||
        updateConfig,
 | 
			
		||||
        diagramNumber,
 | 
			
		||||
        openImportDBMLDialog,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
 
 | 
			
		||||
@@ -22,13 +22,17 @@ import { areFieldTypesCompatible } from '@/lib/data/data-types/data-types';
 | 
			
		||||
const ErrorMessageRelationshipFieldsNotSameType =
 | 
			
		||||
    'Relationships can only be created between fields of the same type';
 | 
			
		||||
 | 
			
		||||
export interface CreateRelationshipDialogProps extends BaseDialogProps {}
 | 
			
		||||
export interface CreateRelationshipDialogProps extends BaseDialogProps {
 | 
			
		||||
    sourceTableId?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const CreateRelationshipDialog: React.FC<
 | 
			
		||||
    CreateRelationshipDialogProps
 | 
			
		||||
> = ({ dialog }) => {
 | 
			
		||||
> = ({ dialog, sourceTableId: preSelectedSourceTableId }) => {
 | 
			
		||||
    const { closeCreateRelationshipDialog } = useDialog();
 | 
			
		||||
    const [primaryTableId, setPrimaryTableId] = useState<string | undefined>();
 | 
			
		||||
    const [primaryTableId, setPrimaryTableId] = useState<string | undefined>(
 | 
			
		||||
        preSelectedSourceTableId
 | 
			
		||||
    );
 | 
			
		||||
    const [primaryFieldId, setPrimaryFieldId] = useState<string | undefined>();
 | 
			
		||||
    const [referencedTableId, setReferencedTableId] = useState<
 | 
			
		||||
        string | undefined
 | 
			
		||||
@@ -43,6 +47,9 @@ export const CreateRelationshipDialog: React.FC<
 | 
			
		||||
    const [canCreateRelationship, setCanCreateRelationship] = useState(false);
 | 
			
		||||
    const { fitView, setEdges } = useReactFlow();
 | 
			
		||||
    const { databaseType } = useChartDB();
 | 
			
		||||
    const [primaryFieldSelectOpen, setPrimaryFieldSelectOpen] = useState(false);
 | 
			
		||||
    const [referencedTableSelectOpen, setReferencedTableSelectOpen] =
 | 
			
		||||
        useState(false);
 | 
			
		||||
 | 
			
		||||
    const tableOptions = useMemo(() => {
 | 
			
		||||
        return tables.map(
 | 
			
		||||
@@ -89,8 +96,23 @@ export const CreateRelationshipDialog: React.FC<
 | 
			
		||||
        setReferencedTableId(undefined);
 | 
			
		||||
        setReferencedFieldId(undefined);
 | 
			
		||||
        setErrorMessage('');
 | 
			
		||||
        setPrimaryFieldSelectOpen(false);
 | 
			
		||||
        setReferencedTableSelectOpen(false);
 | 
			
		||||
    }, [dialog.open]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (preSelectedSourceTableId) {
 | 
			
		||||
            const table = getTable(preSelectedSourceTableId);
 | 
			
		||||
            if (table) {
 | 
			
		||||
                setPrimaryTableId(preSelectedSourceTableId);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                setPrimaryFieldSelectOpen(true);
 | 
			
		||||
            }, 100);
 | 
			
		||||
        }
 | 
			
		||||
    }, [preSelectedSourceTableId, getTable]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setCanCreateRelationship(false);
 | 
			
		||||
        setErrorMessage('');
 | 
			
		||||
@@ -223,8 +245,14 @@ export const CreateRelationshipDialog: React.FC<
 | 
			
		||||
                                    )}
 | 
			
		||||
                                    value={primaryTableId}
 | 
			
		||||
                                    onChange={(value) => {
 | 
			
		||||
                                        setPrimaryTableId(value as string);
 | 
			
		||||
                                        setPrimaryFieldId(undefined);
 | 
			
		||||
                                        const newTableId = value as string;
 | 
			
		||||
                                        setPrimaryTableId(newTableId);
 | 
			
		||||
                                        if (
 | 
			
		||||
                                            newTableId !==
 | 
			
		||||
                                            preSelectedSourceTableId
 | 
			
		||||
                                        ) {
 | 
			
		||||
                                            setPrimaryFieldId(undefined);
 | 
			
		||||
                                        }
 | 
			
		||||
                                    }}
 | 
			
		||||
                                    emptyPlaceholder={t(
 | 
			
		||||
                                        'create_relationship_dialog.no_tables_found'
 | 
			
		||||
@@ -253,6 +281,8 @@ export const CreateRelationshipDialog: React.FC<
 | 
			
		||||
                                            'create_relationship_dialog.primary_field_placeholder'
 | 
			
		||||
                                        )}
 | 
			
		||||
                                        value={primaryFieldId}
 | 
			
		||||
                                        open={primaryFieldSelectOpen}
 | 
			
		||||
                                        onOpenChange={setPrimaryFieldSelectOpen}
 | 
			
		||||
                                        onChange={(value) =>
 | 
			
		||||
                                            setPrimaryFieldId(value as string)
 | 
			
		||||
                                        }
 | 
			
		||||
@@ -283,6 +313,8 @@ export const CreateRelationshipDialog: React.FC<
 | 
			
		||||
                                        'create_relationship_dialog.referenced_table_placeholder'
 | 
			
		||||
                                    )}
 | 
			
		||||
                                    value={referencedTableId}
 | 
			
		||||
                                    open={referencedTableSelectOpen}
 | 
			
		||||
                                    onOpenChange={setReferencedTableSelectOpen}
 | 
			
		||||
                                    onChange={(value) => {
 | 
			
		||||
                                        setReferencedTableId(value as string);
 | 
			
		||||
                                        setReferencedFieldId(undefined);
 | 
			
		||||
 
 | 
			
		||||
@@ -15,11 +15,10 @@ import { SelectBox } from '@/components/select-box/select-box';
 | 
			
		||||
import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { diagramToJSONOutput } from '@/lib/export-import-utils';
 | 
			
		||||
import { Spinner } from '@/components/spinner/spinner';
 | 
			
		||||
import { waitFor } from '@/lib/utils';
 | 
			
		||||
import { AlertCircle } from 'lucide-react';
 | 
			
		||||
import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert';
 | 
			
		||||
import { useExportDiagram } from '@/hooks/use-export-diagram';
 | 
			
		||||
 | 
			
		||||
export interface ExportDiagramDialogProps extends BaseDialogProps {}
 | 
			
		||||
 | 
			
		||||
@@ -27,44 +26,27 @@ export const ExportDiagramDialog: React.FC<ExportDiagramDialogProps> = ({
 | 
			
		||||
    dialog,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { diagramName, currentDiagram } = useChartDB();
 | 
			
		||||
    const [isLoading, setIsLoading] = useState(false);
 | 
			
		||||
    const { currentDiagram } = useChartDB();
 | 
			
		||||
    const { closeExportDiagramDialog } = useDialog();
 | 
			
		||||
    const [error, setError] = useState(false);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!dialog.open) return;
 | 
			
		||||
        setIsLoading(false);
 | 
			
		||||
        setError(false);
 | 
			
		||||
    }, [dialog.open]);
 | 
			
		||||
 | 
			
		||||
    const downloadOutput = useCallback(
 | 
			
		||||
        (dataUrl: string) => {
 | 
			
		||||
            const a = document.createElement('a');
 | 
			
		||||
            a.setAttribute('download', `ChartDB(${diagramName}).json`);
 | 
			
		||||
            a.setAttribute('href', dataUrl);
 | 
			
		||||
            a.click();
 | 
			
		||||
        },
 | 
			
		||||
        [diagramName]
 | 
			
		||||
    );
 | 
			
		||||
    const { exportDiagram, isExporting: isLoading } = useExportDiagram();
 | 
			
		||||
 | 
			
		||||
    const handleExport = useCallback(async () => {
 | 
			
		||||
        setIsLoading(true);
 | 
			
		||||
        await waitFor(1000);
 | 
			
		||||
        try {
 | 
			
		||||
            const json = diagramToJSONOutput(currentDiagram);
 | 
			
		||||
            const blob = new Blob([json], { type: 'application/json' });
 | 
			
		||||
            const dataUrl = URL.createObjectURL(blob);
 | 
			
		||||
            downloadOutput(dataUrl);
 | 
			
		||||
            setIsLoading(false);
 | 
			
		||||
            await exportDiagram({ diagram: currentDiagram });
 | 
			
		||||
            closeExportDiagramDialog();
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            setError(true);
 | 
			
		||||
            setIsLoading(false);
 | 
			
		||||
 | 
			
		||||
            throw e;
 | 
			
		||||
        }
 | 
			
		||||
    }, [downloadOutput, currentDiagram, closeExportDiagramDialog]);
 | 
			
		||||
    }, [exportDiagram, currentDiagram, closeExportDiagramDialog]);
 | 
			
		||||
 | 
			
		||||
    const outputTypeOptions: SelectBoxOption[] = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
 
 | 
			
		||||
@@ -20,10 +20,12 @@ import {
 | 
			
		||||
} from '@/lib/data/export-metadata/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';
 | 
			
		||||
 | 
			
		||||
export interface ExportSQLDialogProps extends BaseDialogProps {
 | 
			
		||||
    targetDatabaseType: DatabaseType;
 | 
			
		||||
@@ -34,7 +36,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
 | 
			
		||||
    targetDatabaseType,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { closeExportSQLDialog } = useDialog();
 | 
			
		||||
    const { currentDiagram } = useChartDB();
 | 
			
		||||
    const { currentDiagram, filteredSchemas } = useChartDB();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const [script, setScript] = React.useState<string>();
 | 
			
		||||
    const [error, setError] = React.useState<boolean>(false);
 | 
			
		||||
@@ -43,17 +45,58 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
 | 
			
		||||
    const abortControllerRef = useRef<AbortController | null>(null);
 | 
			
		||||
 | 
			
		||||
    const exportSQLScript = useCallback(async () => {
 | 
			
		||||
        const filteredDiagram: Diagram = {
 | 
			
		||||
            ...currentDiagram,
 | 
			
		||||
            tables: currentDiagram.tables?.filter((table) =>
 | 
			
		||||
                shouldShowTablesBySchemaFilter(table, filteredSchemas)
 | 
			
		||||
            ),
 | 
			
		||||
            relationships: currentDiagram.relationships?.filter((rel) => {
 | 
			
		||||
                const sourceTable = currentDiagram.tables?.find(
 | 
			
		||||
                    (t) => t.id === rel.sourceTableId
 | 
			
		||||
                );
 | 
			
		||||
                const targetTable = currentDiagram.tables?.find(
 | 
			
		||||
                    (t) => t.id === rel.targetTableId
 | 
			
		||||
                );
 | 
			
		||||
                return (
 | 
			
		||||
                    sourceTable &&
 | 
			
		||||
                    targetTable &&
 | 
			
		||||
                    shouldShowTablesBySchemaFilter(
 | 
			
		||||
                        sourceTable,
 | 
			
		||||
                        filteredSchemas
 | 
			
		||||
                    ) &&
 | 
			
		||||
                    shouldShowTablesBySchemaFilter(targetTable, filteredSchemas)
 | 
			
		||||
                );
 | 
			
		||||
            }),
 | 
			
		||||
            dependencies: currentDiagram.dependencies?.filter((dep) => {
 | 
			
		||||
                const table = currentDiagram.tables?.find(
 | 
			
		||||
                    (t) => t.id === dep.tableId
 | 
			
		||||
                );
 | 
			
		||||
                const dependentTable = currentDiagram.tables?.find(
 | 
			
		||||
                    (t) => t.id === dep.dependentTableId
 | 
			
		||||
                );
 | 
			
		||||
                return (
 | 
			
		||||
                    table &&
 | 
			
		||||
                    dependentTable &&
 | 
			
		||||
                    shouldShowTablesBySchemaFilter(table, filteredSchemas) &&
 | 
			
		||||
                    shouldShowTablesBySchemaFilter(
 | 
			
		||||
                        dependentTable,
 | 
			
		||||
                        filteredSchemas
 | 
			
		||||
                    )
 | 
			
		||||
                );
 | 
			
		||||
            }),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (targetDatabaseType === DatabaseType.GENERIC) {
 | 
			
		||||
            return Promise.resolve(exportBaseSQL(currentDiagram));
 | 
			
		||||
            return Promise.resolve(exportBaseSQL(filteredDiagram));
 | 
			
		||||
        } else {
 | 
			
		||||
            return exportSQL(currentDiagram, targetDatabaseType, {
 | 
			
		||||
            return exportSQL(filteredDiagram, targetDatabaseType, {
 | 
			
		||||
                stream: true,
 | 
			
		||||
                onResultStream: (text) =>
 | 
			
		||||
                    setScript((prev) => (prev ? prev + text : text)),
 | 
			
		||||
                signal: abortControllerRef.current?.signal,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }, [targetDatabaseType, currentDiagram]);
 | 
			
		||||
    }, [targetDatabaseType, currentDiagram, filteredSchemas]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!dialog.open) {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ 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';
 | 
			
		||||
 | 
			
		||||
export interface ImportDatabaseDialogProps extends BaseDialogProps {
 | 
			
		||||
    databaseType: DatabaseType;
 | 
			
		||||
@@ -21,7 +22,8 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
 | 
			
		||||
    dialog,
 | 
			
		||||
    databaseType,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { closeImportDatabaseDialog, showAlert } = useDialog();
 | 
			
		||||
    const { closeImportDatabaseDialog } = useDialog();
 | 
			
		||||
    const { showAlert } = useAlert();
 | 
			
		||||
    const {
 | 
			
		||||
        tables,
 | 
			
		||||
        relationships,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										410
									
								
								src/dialogs/import-dbml-dialog/import-dbml-dialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										410
									
								
								src/dialogs/import-dbml-dialog/import-dbml-dialog.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,410 @@
 | 
			
		||||
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>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -22,15 +22,19 @@ import { useConfig } from '@/hooks/use-config';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import { useStorage } from '@/hooks/use-storage';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import React, { useCallback, useEffect, useState } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
			
		||||
import { useDebounce } from '@/hooks/use-debounce';
 | 
			
		||||
 | 
			
		||||
export interface OpenDiagramDialogProps extends BaseDialogProps {}
 | 
			
		||||
export interface OpenDiagramDialogProps extends BaseDialogProps {
 | 
			
		||||
    canClose?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
 | 
			
		||||
    dialog,
 | 
			
		||||
    canClose = true,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { closeOpenDiagramDialog } = useDialog();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
@@ -58,24 +62,77 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
 | 
			
		||||
        fetchDiagrams();
 | 
			
		||||
    }, [listDiagrams, setDiagrams, dialog.open]);
 | 
			
		||||
 | 
			
		||||
    const openDiagram = (diagramId: string) => {
 | 
			
		||||
        if (diagramId) {
 | 
			
		||||
            updateConfig({ defaultDiagramId: diagramId });
 | 
			
		||||
            navigate(`/diagrams/${diagramId}`);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    const openDiagram = useCallback(
 | 
			
		||||
        (diagramId: string) => {
 | 
			
		||||
            if (diagramId) {
 | 
			
		||||
                updateConfig({ defaultDiagramId: diagramId });
 | 
			
		||||
                navigate(`/diagrams/${diagramId}`);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [updateConfig, navigate]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const handleRowKeyDown = useCallback(
 | 
			
		||||
        (e: React.KeyboardEvent<HTMLTableRowElement>) => {
 | 
			
		||||
            const element = e.target as HTMLElement;
 | 
			
		||||
            const diagramId = element.getAttribute('data-diagram-id');
 | 
			
		||||
            const selectionIndexAttr = element.getAttribute(
 | 
			
		||||
                'data-selection-index'
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (!diagramId || !selectionIndexAttr) return;
 | 
			
		||||
 | 
			
		||||
            const selectionIndex = parseInt(selectionIndexAttr, 10);
 | 
			
		||||
 | 
			
		||||
            switch (e.key) {
 | 
			
		||||
                case 'Enter':
 | 
			
		||||
                case ' ':
 | 
			
		||||
                    e.preventDefault();
 | 
			
		||||
                    openDiagram(diagramId);
 | 
			
		||||
                    closeOpenDiagramDialog();
 | 
			
		||||
                    break;
 | 
			
		||||
                case 'ArrowDown': {
 | 
			
		||||
                    e.preventDefault();
 | 
			
		||||
 | 
			
		||||
                    (
 | 
			
		||||
                        document.querySelector(
 | 
			
		||||
                            `[data-selection-index="${selectionIndex + 1}"]`
 | 
			
		||||
                        ) as HTMLElement
 | 
			
		||||
                    )?.focus();
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
                case 'ArrowUp': {
 | 
			
		||||
                    e.preventDefault();
 | 
			
		||||
 | 
			
		||||
                    (
 | 
			
		||||
                        document.querySelector(
 | 
			
		||||
                            `[data-selection-index="${selectionIndex - 1}"]`
 | 
			
		||||
                        ) as HTMLElement
 | 
			
		||||
                    )?.focus();
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [openDiagram, closeOpenDiagramDialog]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const onFocusHandler = useDebounce(
 | 
			
		||||
        (diagramId: string) => setSelectedDiagramId(diagramId),
 | 
			
		||||
        50
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Dialog
 | 
			
		||||
            {...dialog}
 | 
			
		||||
            onOpenChange={(open) => {
 | 
			
		||||
                if (!open) {
 | 
			
		||||
                if (!open && canClose) {
 | 
			
		||||
                    closeOpenDiagramDialog();
 | 
			
		||||
                }
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent
 | 
			
		||||
                className="flex h-[30rem] max-h-screen w-[90vw] flex-col overflow-y-auto md:w-screen xl:min-w-[55vw]"
 | 
			
		||||
                showClose
 | 
			
		||||
                className="flex h-[30rem] max-h-screen flex-col overflow-y-auto md:min-w-[80vw] xl:min-w-[55vw]"
 | 
			
		||||
                showClose={canClose}
 | 
			
		||||
            >
 | 
			
		||||
                <DialogHeader>
 | 
			
		||||
                    <DialogTitle>{t('open_diagram_dialog.title')}</DialogTitle>
 | 
			
		||||
@@ -112,10 +169,17 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
 | 
			
		||||
                                </TableRow>
 | 
			
		||||
                            </TableHeader>
 | 
			
		||||
                            <TableBody>
 | 
			
		||||
                                {diagrams.map((diagram) => (
 | 
			
		||||
                                {diagrams.map((diagram, index) => (
 | 
			
		||||
                                    <TableRow
 | 
			
		||||
                                        key={diagram.id}
 | 
			
		||||
                                        data-state={`${selectedDiagramId === diagram.id ? 'selected' : ''}`}
 | 
			
		||||
                                        data-diagram-id={diagram.id}
 | 
			
		||||
                                        data-selection-index={index}
 | 
			
		||||
                                        tabIndex={0}
 | 
			
		||||
                                        onFocus={() =>
 | 
			
		||||
                                            onFocusHandler(diagram.id)
 | 
			
		||||
                                        }
 | 
			
		||||
                                        className="focus:bg-accent focus:outline-none"
 | 
			
		||||
                                        onClick={(e) => {
 | 
			
		||||
                                            switch (e.detail) {
 | 
			
		||||
                                                case 1:
 | 
			
		||||
@@ -133,6 +197,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
 | 
			
		||||
                                                    );
 | 
			
		||||
                                            }
 | 
			
		||||
                                        }}
 | 
			
		||||
                                        onKeyDown={handleRowKeyDown}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <TableCell className="table-cell">
 | 
			
		||||
                                            <div className="flex justify-center">
 | 
			
		||||
@@ -164,11 +229,15 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
 | 
			
		||||
                </DialogInternalContent>
 | 
			
		||||
 | 
			
		||||
                <DialogFooter className="flex !justify-between gap-2">
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
                        <Button type="button" variant="secondary">
 | 
			
		||||
                            {t('open_diagram_dialog.cancel')}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </DialogClose>
 | 
			
		||||
                    {canClose ? (
 | 
			
		||||
                        <DialogClose asChild>
 | 
			
		||||
                            <Button type="button" variant="secondary">
 | 
			
		||||
                                {t('open_diagram_dialog.cancel')}
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </DialogClose>
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        <div />
 | 
			
		||||
                    )}
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
                        <Button
 | 
			
		||||
                            type="submit"
 | 
			
		||||
 
 | 
			
		||||
@@ -73,3 +73,68 @@
 | 
			
		||||
        @apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] rounded-md cursor-pointer;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.gradient-background {
 | 
			
		||||
    /* Fallback: Set a background color. */
 | 
			
		||||
    background-color: #f46b24;
 | 
			
		||||
 | 
			
		||||
    /* Create the gradient. */
 | 
			
		||||
    background-image: linear-gradient(
 | 
			
		||||
        45deg,
 | 
			
		||||
        #2e6579 20%,
 | 
			
		||||
        #4fafca 20%,
 | 
			
		||||
        #4fafca 40%,
 | 
			
		||||
        #6dc630 40%,
 | 
			
		||||
        #6dc630 60%,
 | 
			
		||||
        #f9dc3a 60%,
 | 
			
		||||
        #f9dc3a 80%,
 | 
			
		||||
        #f46b24 80%
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    /* Set the background size and repeat properties. */
 | 
			
		||||
    background-size: 100%;
 | 
			
		||||
    background-repeat: repeat;
 | 
			
		||||
 | 
			
		||||
    /* Use the text as a mask for the background. */
 | 
			
		||||
    /* This will show the gradient as a text color rather than element bg. */
 | 
			
		||||
    /* -webkit-background-clip: text;
 | 
			
		||||
    -webkit-text-fill-color: transparent; */
 | 
			
		||||
 | 
			
		||||
    /* Animate the text when loading the element. */
 | 
			
		||||
    /* This animates it on page load and when hovering out. */
 | 
			
		||||
    animation: rainbow-text-simple-animation-rev 0.75s ease forwards;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.gradient-background:hover {
 | 
			
		||||
    animation: rainbow-text-simple-animation 0.5s ease-in forwards;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dbml-error-line {
 | 
			
		||||
    background-color: rgba(255, 0, 0, 0.2) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes rainbow-text-simple-animation-rev {
 | 
			
		||||
    0% {
 | 
			
		||||
        background-size: 650%;
 | 
			
		||||
    }
 | 
			
		||||
    40% {
 | 
			
		||||
        background-size: 650%;
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
        background-size: 100%;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Move the background and make it larger. */
 | 
			
		||||
/* Animation shown when hovering over the text. */
 | 
			
		||||
@keyframes rainbow-text-simple-animation {
 | 
			
		||||
    0% {
 | 
			
		||||
        background-size: 100%;
 | 
			
		||||
    }
 | 
			
		||||
    80% {
 | 
			
		||||
        background-size: 650%;
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
        background-size: 650%;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								src/hooks/use-canvas.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/hooks/use-canvas.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
import { useContext } from 'react';
 | 
			
		||||
import { canvasContext } from '@/context/canvas-context/canvas-context';
 | 
			
		||||
 | 
			
		||||
export const useCanvas = () => useContext(canvasContext);
 | 
			
		||||
							
								
								
									
										21
									
								
								src/hooks/use-debounce.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/hooks/use-debounce.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
import { useCallback, useRef } from 'react';
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
type AnyFunction = (...args: any[]) => any;
 | 
			
		||||
 | 
			
		||||
export const useDebounce = <T extends AnyFunction>(
 | 
			
		||||
    func: T,
 | 
			
		||||
    delay: number
 | 
			
		||||
): ((...args: Parameters<T>) => void) => {
 | 
			
		||||
    const inDebounce = useRef<NodeJS.Timeout>();
 | 
			
		||||
 | 
			
		||||
    const debounce = useCallback(
 | 
			
		||||
        (...args: Parameters<T>) => {
 | 
			
		||||
            clearTimeout(inDebounce.current);
 | 
			
		||||
            inDebounce.current = setTimeout(() => func(...args), delay);
 | 
			
		||||
        },
 | 
			
		||||
        [func, delay]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return debounce;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										40
									
								
								src/hooks/use-export-diagram.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/hooks/use-export-diagram.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
import { useCallback, useState } from 'react';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import { diagramToJSONOutput } from '@/lib/export-import-utils';
 | 
			
		||||
import { waitFor } from '@/lib/utils';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
 | 
			
		||||
export const useExportDiagram = () => {
 | 
			
		||||
    const [isLoading, setIsLoading] = useState(false);
 | 
			
		||||
    const { closeExportDiagramDialog } = useDialog();
 | 
			
		||||
 | 
			
		||||
    const downloadOutput = useCallback((name: string, dataUrl: string) => {
 | 
			
		||||
        const a = document.createElement('a');
 | 
			
		||||
        a.setAttribute('download', `ChartDB(${name}).json`);
 | 
			
		||||
        a.setAttribute('href', dataUrl);
 | 
			
		||||
        a.click();
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const handleExport = useCallback(
 | 
			
		||||
        async ({ diagram }: { diagram: Diagram }) => {
 | 
			
		||||
            setIsLoading(true);
 | 
			
		||||
            await waitFor(1000);
 | 
			
		||||
            try {
 | 
			
		||||
                const json = diagramToJSONOutput(diagram);
 | 
			
		||||
                const blob = new Blob([json], { type: 'application/json' });
 | 
			
		||||
                const dataUrl = URL.createObjectURL(blob);
 | 
			
		||||
                downloadOutput(diagram.name, dataUrl);
 | 
			
		||||
                setIsLoading(false);
 | 
			
		||||
                closeExportDiagramDialog();
 | 
			
		||||
            } finally {
 | 
			
		||||
                setIsLoading(false);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [downloadOutput, closeExportDiagramDialog]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        exportDiagram: handleExport,
 | 
			
		||||
        isExporting: isLoading,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
@@ -22,6 +22,7 @@ import { te, teMetadata } from './locales/te';
 | 
			
		||||
import { bn, bnMetadata } from './locales/bn';
 | 
			
		||||
import { gu, guMetadata } from './locales/gu';
 | 
			
		||||
import { vi, viMetadata } from './locales/vi';
 | 
			
		||||
import { ar, arMetadata } from './locales/ar';
 | 
			
		||||
 | 
			
		||||
export const languages: LanguageMetadata[] = [
 | 
			
		||||
    enMetadata,
 | 
			
		||||
@@ -44,6 +45,7 @@ export const languages: LanguageMetadata[] = [
 | 
			
		||||
    bnMetadata,
 | 
			
		||||
    guMetadata,
 | 
			
		||||
    viMetadata,
 | 
			
		||||
    arMetadata,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const resources = {
 | 
			
		||||
@@ -67,6 +69,7 @@ const resources = {
 | 
			
		||||
    bn,
 | 
			
		||||
    gu,
 | 
			
		||||
    vi,
 | 
			
		||||
    ar,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
i18n.use(LanguageDetector)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										428
									
								
								src/i18n/locales/ar.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										428
									
								
								src/i18n/locales/ar.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,428 @@
 | 
			
		||||
import type { LanguageMetadata, LanguageTranslation } from '../types';
 | 
			
		||||
 | 
			
		||||
export const ar: LanguageTranslation = {
 | 
			
		||||
    translation: {
 | 
			
		||||
        menu: {
 | 
			
		||||
            file: {
 | 
			
		||||
                file: 'ملف',
 | 
			
		||||
                new: 'جديد',
 | 
			
		||||
                open: 'فتح',
 | 
			
		||||
                save: 'حفظ',
 | 
			
		||||
                import: 'استيراد قاعدة بيانات',
 | 
			
		||||
                export_sql: 'SQL تصدير',
 | 
			
		||||
                export_as: 'تصدير كـ',
 | 
			
		||||
                delete_diagram: 'حذف الرسم البياني',
 | 
			
		||||
                exit: 'خروج',
 | 
			
		||||
            },
 | 
			
		||||
            edit: {
 | 
			
		||||
                edit: 'تحرير',
 | 
			
		||||
                undo: 'تراجع',
 | 
			
		||||
                redo: 'إعادة',
 | 
			
		||||
                clear: 'مسح',
 | 
			
		||||
            },
 | 
			
		||||
            view: {
 | 
			
		||||
                view: 'عرض',
 | 
			
		||||
                show_sidebar: 'إظهار الشريط الجانبي',
 | 
			
		||||
                hide_sidebar: 'إخفاء الشريط الجانبي',
 | 
			
		||||
                hide_cardinality: 'إخفاء الكاردينالية',
 | 
			
		||||
                show_cardinality: 'إظهار الكاردينالية',
 | 
			
		||||
                zoom_on_scroll: 'تكبير/تصغير عند التمرير',
 | 
			
		||||
                theme: 'المظهر',
 | 
			
		||||
                show_dependencies: 'إظهار الاعتمادات',
 | 
			
		||||
                hide_dependencies: 'إخفاء الاعتمادات',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'النسخ الاحتياطي',
 | 
			
		||||
                export_diagram: 'تصدير المخطط',
 | 
			
		||||
                restore_diagram: 'استعادة المخطط',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'مساعدة',
 | 
			
		||||
                docs_website: 'الوثائق',
 | 
			
		||||
                visit_website: 'ChartDB قم بزيارة',
 | 
			
		||||
                join_discord: 'Discord انضم إلينا على',
 | 
			
		||||
                schedule_a_call: '!تحدث معنا',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        delete_diagram_alert: {
 | 
			
		||||
            title: 'حذف المخطط',
 | 
			
		||||
            description:
 | 
			
		||||
                '.لا يمكن التراجع عن هذا الإجراء. سيتم حذف الرسم البياني بشكل دائم',
 | 
			
		||||
            cancel: 'إلغاء',
 | 
			
		||||
            delete: 'حذف',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        clear_diagram_alert: {
 | 
			
		||||
            title: 'مسح الرسم البياني',
 | 
			
		||||
            description:
 | 
			
		||||
                '.لا يمكن التراجع عن هذا الاجراء. سيتم حذف جميع البيانات في الرسم البياني بشكل دائم',
 | 
			
		||||
            cancel: 'إلغاء',
 | 
			
		||||
            clear: 'مسح',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        reorder_diagram_alert: {
 | 
			
		||||
            title: 'إعادة ترتيب الرسم البياني',
 | 
			
		||||
            description:
 | 
			
		||||
                'هذا الإجراء سيقوم بإعادة ترتيب الجداول في المخطط بشكل تلقائي. هل تريد المتابعة؟',
 | 
			
		||||
            reorder: 'إعادة ترتيب',
 | 
			
		||||
            cancel: 'إلغاء',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        multiple_schemas_alert: {
 | 
			
		||||
            title: 'مخططات متعددة',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
 | 
			
		||||
            dont_show_again: 'لا تظهره مجدداً',
 | 
			
		||||
            change_schema: 'تغيير',
 | 
			
		||||
            none: 'لا شيء',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        copy_to_clipboard_toast: {
 | 
			
		||||
            unsupported: {
 | 
			
		||||
                title: 'فشل النسخ',
 | 
			
		||||
                description: '.الحافظة غير مدعومة',
 | 
			
		||||
            },
 | 
			
		||||
            failed: {
 | 
			
		||||
                title: 'فشل النسخ',
 | 
			
		||||
                description: 'حدث خطأ أثناء النسخ. حاول مجدداً',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        theme: {
 | 
			
		||||
            system: 'النظام',
 | 
			
		||||
            light: 'فاتح',
 | 
			
		||||
            dark: 'داكن',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        zoom: {
 | 
			
		||||
            on: 'تشغيل',
 | 
			
		||||
            off: 'إيقاف',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        last_saved: 'آخر حفظ',
 | 
			
		||||
        saved: 'تم الحفظ',
 | 
			
		||||
        loading_diagram: '...جارِ تحميل الرسم البياني',
 | 
			
		||||
        deselect_all: 'إلغاء تحديد الكل',
 | 
			
		||||
        select_all: 'تحديد الكل',
 | 
			
		||||
        clear: 'مسح',
 | 
			
		||||
        show_more: 'عرض المزيد',
 | 
			
		||||
        show_less: 'عرض أقل',
 | 
			
		||||
        copy_to_clipboard: 'نسخ إلى الحافظة',
 | 
			
		||||
        copied: '!تم النسخ',
 | 
			
		||||
 | 
			
		||||
        side_panel: {
 | 
			
		||||
            schema: ':المخطط',
 | 
			
		||||
            filter_by_schema: 'تصفية حسب المخطط',
 | 
			
		||||
            search_schema: '...بحث في المخطط',
 | 
			
		||||
            no_schemas_found: '.لم يتم العثور على مخططات',
 | 
			
		||||
            view_all_options: '...عرض جميع الخيارات',
 | 
			
		||||
            tables_section: {
 | 
			
		||||
                tables: 'الجداول',
 | 
			
		||||
                add_table: 'إضافة جدول',
 | 
			
		||||
                filter: 'تصفية',
 | 
			
		||||
                collapse: 'طي الكل',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'الحقول',
 | 
			
		||||
                    nullable: 'يمكن ان يكون فارغاً؟',
 | 
			
		||||
                    primary_key: 'المفتاح الأساسي',
 | 
			
		||||
                    indexes: 'الفهارس',
 | 
			
		||||
                    comments: 'تعليقات',
 | 
			
		||||
                    no_comments: 'لا توجد تعليقات',
 | 
			
		||||
                    add_field: 'إضافة حقل',
 | 
			
		||||
                    add_index: 'إضافة فهرس',
 | 
			
		||||
                    index_select_fields: 'حدد الحقول',
 | 
			
		||||
                    no_types_found: 'لا يوجد أنواع',
 | 
			
		||||
                    field_name: 'الإسم',
 | 
			
		||||
                    field_type: 'النوع',
 | 
			
		||||
                    field_actions: {
 | 
			
		||||
                        title: 'خصائص الحقل',
 | 
			
		||||
                        unique: 'فريد',
 | 
			
		||||
                        comments: 'تعليقات',
 | 
			
		||||
                        no_comments: 'لا يوجد تعليقات',
 | 
			
		||||
                        delete_field: 'حذف الحقل',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'خصائص الفهرس',
 | 
			
		||||
                        name: 'الإسم',
 | 
			
		||||
                        unique: 'فريد',
 | 
			
		||||
                        delete_index: 'حذف الفهرس',
 | 
			
		||||
                    },
 | 
			
		||||
                    table_actions: {
 | 
			
		||||
                        title: 'إجراءات الجدول',
 | 
			
		||||
                        change_schema: 'تغيير المخطط',
 | 
			
		||||
                        add_field: 'إضافة حقل',
 | 
			
		||||
                        add_index: 'إضافة فهرس',
 | 
			
		||||
                        duplicate_table: 'نسخ الجدول',
 | 
			
		||||
                        delete_table: 'حذف الجدول',
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
                empty_state: {
 | 
			
		||||
                    title: 'لا توجد جداول',
 | 
			
		||||
                    description: 'أنشئ جدولاً للبدء',
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            relationships_section: {
 | 
			
		||||
                relationships: 'العلاقات',
 | 
			
		||||
                filter: 'تصفية',
 | 
			
		||||
                add_relationship: 'إضافة علاقة',
 | 
			
		||||
                collapse: 'طي الكل',
 | 
			
		||||
                relationship: {
 | 
			
		||||
                    primary: 'الجدول الأساسي',
 | 
			
		||||
                    foreign: 'الجدول المرتبط',
 | 
			
		||||
                    cardinality: 'الكاردينالية',
 | 
			
		||||
                    delete_relationship: 'حذف',
 | 
			
		||||
                    relationship_actions: {
 | 
			
		||||
                        title: 'إجراءات',
 | 
			
		||||
                        delete_relationship: 'حذف',
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
                empty_state: {
 | 
			
		||||
                    title: 'لا توجد علاقات',
 | 
			
		||||
                    description: 'إنشئ علاقة لربط الجداول',
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            dependencies_section: {
 | 
			
		||||
                dependencies: 'الاعتمادات',
 | 
			
		||||
                filter: 'تصفية',
 | 
			
		||||
                collapse: 'طي الكل',
 | 
			
		||||
                dependency: {
 | 
			
		||||
                    table: 'الجدول',
 | 
			
		||||
                    dependent_table: 'عرض الاعتمادات',
 | 
			
		||||
                    delete_dependency: 'حذف',
 | 
			
		||||
                    dependency_actions: {
 | 
			
		||||
                        title: 'إجراءات',
 | 
			
		||||
                        delete_dependency: 'حذف',
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
                empty_state: {
 | 
			
		||||
                    title: 'لا توجد اعتمادات',
 | 
			
		||||
                    description: 'إنشاء اعتماد للبدء',
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        toolbar: {
 | 
			
		||||
            zoom_in: 'تكبير',
 | 
			
		||||
            zoom_out: 'تصغير',
 | 
			
		||||
            save: 'حفظ',
 | 
			
		||||
            show_all: 'عرض الكل',
 | 
			
		||||
            undo: 'تراجع',
 | 
			
		||||
            redo: 'إعادة',
 | 
			
		||||
            reorder_diagram: 'إعادة ترتيب الرسم البياني',
 | 
			
		||||
            highlight_overlapping_tables: 'تمييز الجداول المتداخلة',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
            database_selection: {
 | 
			
		||||
                title: 'ما هو نوع قاعدة البيانات الخاصة بك؟',
 | 
			
		||||
                description:
 | 
			
		||||
                    'تتمتع كل قاعدة بيانات بمميزاتها وقدراتها الفريدة.',
 | 
			
		||||
                check_examples_long: 'ألقي نظرة على الأمثلة',
 | 
			
		||||
                check_examples_short: 'أمثلة',
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            import_database: {
 | 
			
		||||
                title: 'إسترد قاعدة بياناتك',
 | 
			
		||||
                database_edition: ':إصدار قاعدة البيانات',
 | 
			
		||||
                step_1: ':قم بتشغيل هذا البرنامج النصي في قاعدة بياناتك',
 | 
			
		||||
                step_2: ':إلصق نتيجة البرنامج النصي هنا',
 | 
			
		||||
                script_results_placeholder: '...نتيجة البرنامج النصي هنا',
 | 
			
		||||
                ssms_instructions: {
 | 
			
		||||
                    button_text: 'SSMS تعليمات',
 | 
			
		||||
                    title: 'تعليمات',
 | 
			
		||||
                    step_1: 'SQL SERVER < انتقل إلى الأدوات > الخيارات > نتائح الاستعلام',
 | 
			
		||||
                    step_2: '(اضبطها على 9999999) XML اذا كنت تستخدم "نتائج إلى الشبكة"، قم بتغيير الحد الاقصى للاحرف المستردة للبيانات غير',
 | 
			
		||||
                },
 | 
			
		||||
                instructions_link: 'تحتاج مساعدة؟ شاهد الفيديو',
 | 
			
		||||
                check_script_result: 'تحقق من نتيجة البرنامج النصي',
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            cancel: 'إلغاء',
 | 
			
		||||
            import_from_file: 'استيراد من ملف',
 | 
			
		||||
            back: 'رجوع',
 | 
			
		||||
            empty_diagram: 'مخطط فارغ',
 | 
			
		||||
            continue: 'متابعة',
 | 
			
		||||
            import: 'استيراد',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        open_diagram_dialog: {
 | 
			
		||||
            title: 'فتح مخطط',
 | 
			
		||||
            description: 'اختر مخططًا لفتحه من القائمة ادناه',
 | 
			
		||||
            table_columns: {
 | 
			
		||||
                name: 'الإسم',
 | 
			
		||||
                created_at: 'تاريخ الإنشاء',
 | 
			
		||||
                last_modified: 'آخر تعديل',
 | 
			
		||||
                tables_count: 'الجداول',
 | 
			
		||||
            },
 | 
			
		||||
            cancel: 'إلغاء',
 | 
			
		||||
            open: 'فتح',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        export_sql_dialog: {
 | 
			
		||||
            title: 'SQL تصدير',
 | 
			
		||||
            description:
 | 
			
		||||
                '{{databaseType}} صدّر مخطط الرسم البياني إلى برنامج نصي لـ',
 | 
			
		||||
            close: 'إغلاق',
 | 
			
		||||
            loading: {
 | 
			
		||||
                text: '...{{databaseType}} ل SQL يقوم الذكاء الاصطناعي بإنشاء',
 | 
			
		||||
                description: 'هذا قد يستغرق 30 ثانية',
 | 
			
		||||
            },
 | 
			
		||||
            error: {
 | 
			
		||||
                message:
 | 
			
		||||
                    'النصي. يرجى المحاولة مرة اخرى لاحقاً او <0>اتصل بنا</0> SQL خطأ في إنشاء برنامج',
 | 
			
		||||
                description:
 | 
			
		||||
                    ' الخاصة بك. راجع الدليل <0>هنا</0> OPENAI_TOKEN لا تتردد في استخدام',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_relationship_dialog: {
 | 
			
		||||
            title: 'إنشاء علاقة',
 | 
			
		||||
            primary_table: 'الجدول الأساسي',
 | 
			
		||||
            primary_field: 'الحقل الأساسي',
 | 
			
		||||
            referenced_table: 'الجدول المرتبط',
 | 
			
		||||
            referenced_field: 'الحقل المرتبط',
 | 
			
		||||
            primary_table_placeholder: 'حدد الجدول',
 | 
			
		||||
            primary_field_placeholder: 'حدد الحقل',
 | 
			
		||||
            referenced_table_placeholder: 'حدد الجدول',
 | 
			
		||||
            referenced_field_placeholder: 'حدد الحقل',
 | 
			
		||||
            no_tables_found: 'لم يتم العثور على جداول',
 | 
			
		||||
            no_fields_found: 'لم يتم العثور على حقول',
 | 
			
		||||
            create: 'إنشاء',
 | 
			
		||||
            cancel: 'إلغاء',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        import_database_dialog: {
 | 
			
		||||
            title: 'استيراد إلى المخطط الحالي',
 | 
			
		||||
            override_alert: {
 | 
			
		||||
                title: 'استيراد قاعدة بيانات',
 | 
			
		||||
                content: {
 | 
			
		||||
                    alert: 'سيؤدي استيراد هذا المخطط إلى التأثير على الجداول والعلاقات الحالية.',
 | 
			
		||||
                    new_tables:
 | 
			
		||||
                        'جداول جديدة <bold>{{newTablesNumber}}</bold> سيتم إضافة',
 | 
			
		||||
                    new_relationships:
 | 
			
		||||
                        'علاقات جديدة <bold>{{newRelationshipsNumber}}</bold> سيتم إنشاء',
 | 
			
		||||
                    tables_override:
 | 
			
		||||
                        'جداول <bold>{{tablesOverrideNumber}}</bold> سيتم تعديل',
 | 
			
		||||
                    proceed: 'هل تريد المتابعة؟',
 | 
			
		||||
                },
 | 
			
		||||
                import: 'استيراد',
 | 
			
		||||
                cancel: 'إلغاء',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        export_image_dialog: {
 | 
			
		||||
            title: 'تصدير الصورة',
 | 
			
		||||
            description: ':اختر عامل المقياس للتصدير',
 | 
			
		||||
            scale_1x: '1x عادي',
 | 
			
		||||
            scale_2x: '2x (موصى به)',
 | 
			
		||||
            scale_3x: '3x',
 | 
			
		||||
            scale_4x: '4x',
 | 
			
		||||
            cancel: 'إلغاء',
 | 
			
		||||
            export: 'تصدير',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_table_schema_dialog: {
 | 
			
		||||
            title: 'اختر مخططاً',
 | 
			
		||||
            description:
 | 
			
		||||
                '.يتم حالياً عرض مخططات متعددة. اختر واحداً للجدول الجديد',
 | 
			
		||||
            cancel: 'إلغاء',
 | 
			
		||||
            confirm: 'تأكيد',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        update_table_schema_dialog: {
 | 
			
		||||
            title: 'تغيير المخطط',
 | 
			
		||||
            description: '"{{tableName}}" تحديث مخطط الجدول',
 | 
			
		||||
            cancel: 'إلغاء',
 | 
			
		||||
            confirm: 'تغيير',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: '!ساعدنا على التحسن',
 | 
			
		||||
            description: '؟! إنها مجرد نقرة واحدةGITHUB هل ترغب في تقييمنا على',
 | 
			
		||||
            close: 'ليس الآن',
 | 
			
		||||
            confirm: '!بالتأكيد',
 | 
			
		||||
        },
 | 
			
		||||
        export_diagram_dialog: {
 | 
			
		||||
            title: 'تصدير المخطط',
 | 
			
		||||
            description: ':اختر التنسيق للتصدير',
 | 
			
		||||
            format_json: 'JSON',
 | 
			
		||||
            cancel: 'إلغاء',
 | 
			
		||||
            export: 'تصدير',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'حدث خطأ أثناء التصدير',
 | 
			
		||||
                description:
 | 
			
		||||
                    'chartdb.io@gmail.com حدث خطأ ما. هل تحتاج إلى مساعدة؟',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        import_diagram_dialog: {
 | 
			
		||||
            title: 'استيراد الرسم البياني',
 | 
			
		||||
            description: ':للرسم البياني ادناه JSON قم بلصق',
 | 
			
		||||
            cancel: 'إلغاء',
 | 
			
		||||
            import: 'استيراد',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'حدث خطأ أثناء الاستيراد',
 | 
			
		||||
                description:
 | 
			
		||||
                    'chartdb.io@gmail.com و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'واحد إلى واحد',
 | 
			
		||||
            one_to_many: 'واحد إلى متعدد',
 | 
			
		||||
            many_to_one: 'متعدد إلى واحد',
 | 
			
		||||
            many_to_many: 'متعدد إلى متعدد',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        canvas_context_menu: {
 | 
			
		||||
            new_table: 'جدول جديد',
 | 
			
		||||
            new_relationship: 'علاقة جديدة',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        table_node_context_menu: {
 | 
			
		||||
            edit_table: 'تعديل الجدول',
 | 
			
		||||
            duplicate_table: 'نسخ الجدول',
 | 
			
		||||
            delete_table: 'حذف الجدول',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        snap_to_grid_tooltip: '({{key}} مغنظة الشبكة (اضغط مع الاستمرار على',
 | 
			
		||||
 | 
			
		||||
        tool_tips: {
 | 
			
		||||
            double_click_to_edit: 'انقر مرتين للتعديل',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        language_select: {
 | 
			
		||||
            change_language: 'اللغة',
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const arMetadata: LanguageMetadata = {
 | 
			
		||||
    name: 'Arabic',
 | 
			
		||||
    nativeName: 'العربية',
 | 
			
		||||
    code: 'ar',
 | 
			
		||||
};
 | 
			
		||||
@@ -8,7 +8,7 @@ export const bn: LanguageTranslation = {
 | 
			
		||||
                new: 'নতুন',
 | 
			
		||||
                open: 'খুলুন',
 | 
			
		||||
                save: 'সংরক্ষণ করুন',
 | 
			
		||||
                import_database: 'ডাটাবেস আমদানি করুন',
 | 
			
		||||
                import: 'ডাটাবেস আমদানি করুন',
 | 
			
		||||
                export_sql: 'SQL রপ্তানি করুন',
 | 
			
		||||
                export_as: 'রূপে রপ্তানি করুন',
 | 
			
		||||
                delete_diagram: 'ডায়াগ্রাম মুছুন',
 | 
			
		||||
@@ -30,15 +30,19 @@ export const bn: LanguageTranslation = {
 | 
			
		||||
                theme: 'থিম',
 | 
			
		||||
                show_dependencies: 'নির্ভরতাগুলি দেখান',
 | 
			
		||||
                hide_dependencies: 'নির্ভরতাগুলি লুকান',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'শেয়ার করুন',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'ব্যাকআপ',
 | 
			
		||||
                export_diagram: 'ডায়াগ্রাম রপ্তানি করুন',
 | 
			
		||||
                import_diagram: 'ডায়াগ্রাম আমদানি করুন',
 | 
			
		||||
                restore_diagram: 'ডায়াগ্রাম পুনরুদ্ধার করুন',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'সাহায্য',
 | 
			
		||||
                docs_website: 'ডকুমেন্টেশন',
 | 
			
		||||
                visit_website: 'ChartDB ওয়েবসাইটে যান',
 | 
			
		||||
                join_discord: 'আমাদের Discord-এ যোগ দিন',
 | 
			
		||||
                schedule_a_call: 'আমাদের সাথে কথা বলুন!',
 | 
			
		||||
@@ -102,7 +106,6 @@ export const bn: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: 'সর্বশেষ সংরক্ষণ',
 | 
			
		||||
        saved: 'সংরক্ষিত',
 | 
			
		||||
        diagrams: 'ডায়াগ্রাম',
 | 
			
		||||
        loading_diagram: 'ডায়াগ্রাম লোড হচ্ছে...',
 | 
			
		||||
        deselect_all: 'সব নির্বাচন সরান',
 | 
			
		||||
        select_all: 'সব নির্বাচন করুন',
 | 
			
		||||
@@ -123,6 +126,12 @@ export const bn: LanguageTranslation = {
 | 
			
		||||
                add_table: 'টেবিল যোগ করুন',
 | 
			
		||||
                filter: 'ফিল্টার',
 | 
			
		||||
                collapse: 'সব ভাঁজ করুন',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'ফিল্ড',
 | 
			
		||||
@@ -371,6 +380,20 @@ export const bn: LanguageTranslation = {
 | 
			
		||||
                    'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'এক থেকে এক',
 | 
			
		||||
            one_to_many: 'এক থেকে অনেক',
 | 
			
		||||
@@ -387,6 +410,7 @@ export const bn: LanguageTranslation = {
 | 
			
		||||
            edit_table: 'টেবিল সম্পাদনা করুন',
 | 
			
		||||
            duplicate_table: 'টেবিল নকল করুন',
 | 
			
		||||
            delete_table: 'টেবিল মুছে ফেলুন',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        snap_to_grid_tooltip: 'গ্রিডে স্ন্যাপ করুন (অবস্থান {{key}})',
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const de: LanguageTranslation = {
 | 
			
		||||
                new: 'Neu',
 | 
			
		||||
                open: 'Öffnen',
 | 
			
		||||
                save: 'Speichern',
 | 
			
		||||
                import_database: 'Datenbank importieren',
 | 
			
		||||
                import: 'Datenbank importieren',
 | 
			
		||||
                export_sql: 'SQL exportieren',
 | 
			
		||||
                export_as: 'Exportieren als',
 | 
			
		||||
                delete_diagram: 'Diagramm löschen',
 | 
			
		||||
@@ -30,15 +30,19 @@ export const de: LanguageTranslation = {
 | 
			
		||||
                theme: 'Stil',
 | 
			
		||||
                show_dependencies: 'Abhängigkeiten anzeigen',
 | 
			
		||||
                hide_dependencies: 'Abhängigkeiten ausblenden',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Share',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'Backup',
 | 
			
		||||
                export_diagram: 'Export Diagram',
 | 
			
		||||
                import_diagram: 'Import Diagram',
 | 
			
		||||
                restore_diagram: 'Restore Diagram',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'Hilfe',
 | 
			
		||||
                docs_website: 'Dokumentation',
 | 
			
		||||
                visit_website: 'ChartDB Webseite',
 | 
			
		||||
                join_discord: 'Auf Discord beitreten',
 | 
			
		||||
                schedule_a_call: 'Gespräch vereinbaren',
 | 
			
		||||
@@ -103,7 +107,6 @@ export const de: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: 'Zuletzt gespeichert',
 | 
			
		||||
        saved: 'Gespeichert',
 | 
			
		||||
        diagrams: 'Diagramme',
 | 
			
		||||
        loading_diagram: 'Diagramm wird geladen...',
 | 
			
		||||
        deselect_all: 'Alles abwählen',
 | 
			
		||||
        select_all: 'Alles auswählen',
 | 
			
		||||
@@ -124,6 +127,12 @@ export const de: LanguageTranslation = {
 | 
			
		||||
                add_table: 'Tabelle hinzufügen',
 | 
			
		||||
                filter: 'Filter',
 | 
			
		||||
                collapse: 'Alle einklappen',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'Felder',
 | 
			
		||||
@@ -374,6 +383,20 @@ export const de: LanguageTranslation = {
 | 
			
		||||
                    'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'Ein zu Eins (1:1)',
 | 
			
		||||
            one_to_many: 'Ein zu Viele (1:n)',
 | 
			
		||||
@@ -390,6 +413,7 @@ export const de: LanguageTranslation = {
 | 
			
		||||
            edit_table: 'Tabelle bearbeiten',
 | 
			
		||||
            duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
            delete_table: 'Tabelle löschen',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // TODO: Add translations
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const en = {
 | 
			
		||||
                new: 'New',
 | 
			
		||||
                open: 'Open',
 | 
			
		||||
                save: 'Save',
 | 
			
		||||
                import_database: 'Import Database',
 | 
			
		||||
                import: 'Import',
 | 
			
		||||
                export_sql: 'Export SQL',
 | 
			
		||||
                export_as: 'Export as',
 | 
			
		||||
                delete_diagram: 'Delete Diagram',
 | 
			
		||||
@@ -30,14 +30,17 @@ export const en = {
 | 
			
		||||
                theme: 'Theme',
 | 
			
		||||
                show_dependencies: 'Show Dependencies',
 | 
			
		||||
                hide_dependencies: 'Hide Dependencies',
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Share',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'Backup',
 | 
			
		||||
                export_diagram: 'Export Diagram',
 | 
			
		||||
                import_diagram: 'Import Diagram',
 | 
			
		||||
                restore_diagram: 'Restore Diagram',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'Help',
 | 
			
		||||
                docs_website: 'Docs',
 | 
			
		||||
                visit_website: 'Visit ChartDB',
 | 
			
		||||
                join_discord: 'Join us on Discord',
 | 
			
		||||
                schedule_a_call: 'Talk with us!',
 | 
			
		||||
@@ -101,7 +104,6 @@ export const en = {
 | 
			
		||||
 | 
			
		||||
        last_saved: 'Last saved',
 | 
			
		||||
        saved: 'Saved',
 | 
			
		||||
        diagrams: 'Diagrams',
 | 
			
		||||
        loading_diagram: 'Loading diagram...',
 | 
			
		||||
        deselect_all: 'Deselect All',
 | 
			
		||||
        select_all: 'Select All',
 | 
			
		||||
@@ -122,6 +124,10 @@ export const en = {
 | 
			
		||||
                add_table: 'Add Table',
 | 
			
		||||
                filter: 'Filter',
 | 
			
		||||
                collapse: 'Collapse All',
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'Fields',
 | 
			
		||||
@@ -360,7 +366,7 @@ export const en = {
 | 
			
		||||
 | 
			
		||||
        import_diagram_dialog: {
 | 
			
		||||
            title: 'Import Diagram',
 | 
			
		||||
            description: 'Paste the diagram JSON below:',
 | 
			
		||||
            description: 'Import a diagram from a JSON file.',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            error: {
 | 
			
		||||
@@ -369,6 +375,20 @@ export const en = {
 | 
			
		||||
                    'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error importing DBML',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'One to One',
 | 
			
		||||
            one_to_many: 'One to Many',
 | 
			
		||||
@@ -385,6 +405,7 @@ export const en = {
 | 
			
		||||
            edit_table: 'Edit Table',
 | 
			
		||||
            duplicate_table: 'Duplicate Table',
 | 
			
		||||
            delete_table: 'Delete Table',
 | 
			
		||||
            add_relationship: 'Add Relationship',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const es: LanguageTranslation = {
 | 
			
		||||
                new: 'Nuevo',
 | 
			
		||||
                open: 'Abrir',
 | 
			
		||||
                save: 'Guardar',
 | 
			
		||||
                import_database: 'Importar Base de Datos',
 | 
			
		||||
                import: 'Importar Base de Datos',
 | 
			
		||||
                export_sql: 'Exportar SQL',
 | 
			
		||||
                export_as: 'Exportar como',
 | 
			
		||||
                delete_diagram: 'Eliminar Diagrama',
 | 
			
		||||
@@ -30,15 +30,18 @@ export const es: LanguageTranslation = {
 | 
			
		||||
                theme: 'Tema',
 | 
			
		||||
                show_dependencies: 'Mostrar dependencias',
 | 
			
		||||
                hide_dependencies: 'Ocultar dependencias',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Share',
 | 
			
		||||
                export_diagram: 'Export Diagram',
 | 
			
		||||
                import_diagram: 'Import Diagram',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'Respaldo',
 | 
			
		||||
                export_diagram: 'Exportar Diagrama',
 | 
			
		||||
                restore_diagram: 'Restaurar Diagrama',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'Ayuda',
 | 
			
		||||
                docs_website: 'Documentación',
 | 
			
		||||
                visit_website: 'Visitar ChartDB',
 | 
			
		||||
                join_discord: 'Únete a nosotros en Discord',
 | 
			
		||||
                schedule_a_call: '¡Habla con nosotros!',
 | 
			
		||||
@@ -93,7 +96,6 @@ export const es: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: 'Último guardado',
 | 
			
		||||
        saved: 'Guardado',
 | 
			
		||||
        diagrams: 'Diagramas',
 | 
			
		||||
        loading_diagram: 'Cargando diagrama...',
 | 
			
		||||
        deselect_all: 'Deseleccionar todo',
 | 
			
		||||
        select_all: 'Seleccionar todo',
 | 
			
		||||
@@ -114,6 +116,12 @@ export const es: LanguageTranslation = {
 | 
			
		||||
                add_table: 'Agregar Tabla',
 | 
			
		||||
                filter: 'Filtrar',
 | 
			
		||||
                collapse: 'Colapsar Todo',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'Campos',
 | 
			
		||||
@@ -373,6 +381,20 @@ export const es: LanguageTranslation = {
 | 
			
		||||
                    'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'Uno a Uno',
 | 
			
		||||
            one_to_many: 'Uno a Muchos',
 | 
			
		||||
@@ -389,6 +411,7 @@ export const es: LanguageTranslation = {
 | 
			
		||||
            edit_table: 'Editar Tabla',
 | 
			
		||||
            duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
            delete_table: 'Eliminar Tabla',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // TODO: Add translations
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
                new: 'Nouveau',
 | 
			
		||||
                open: 'Ouvrir',
 | 
			
		||||
                save: 'Enregistrer',
 | 
			
		||||
                import_database: 'Importer Base de Données',
 | 
			
		||||
                import: 'Importer Base de Données',
 | 
			
		||||
                export_sql: 'Exporter SQL',
 | 
			
		||||
                export_as: 'Exporter en tant que',
 | 
			
		||||
                delete_diagram: 'Supprimer le Diagramme',
 | 
			
		||||
@@ -30,14 +30,17 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
                theme: 'Thème',
 | 
			
		||||
                show_dependencies: 'Afficher les Dépendances',
 | 
			
		||||
                hide_dependencies: 'Masquer les Dépendances',
 | 
			
		||||
                show_minimap: 'Afficher la Mini Carte',
 | 
			
		||||
                hide_minimap: 'Masquer la Mini Carte',
 | 
			
		||||
            },
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Partage',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'Sauvegarde',
 | 
			
		||||
                export_diagram: 'Exporter le diagramme',
 | 
			
		||||
                import_diagram: 'Importer un diagramme',
 | 
			
		||||
                restore_diagram: 'Restaurer le diagramme',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'Aide',
 | 
			
		||||
                docs_website: 'Documentation',
 | 
			
		||||
                visit_website: 'Visitez ChartDB',
 | 
			
		||||
                join_discord: 'Rejoignez-nous sur Discord',
 | 
			
		||||
                schedule_a_call: 'Parlez avec nous !',
 | 
			
		||||
@@ -92,16 +95,14 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: 'Dernière sauvegarde',
 | 
			
		||||
        saved: 'Enregistré',
 | 
			
		||||
        diagrams: 'Diagrammes',
 | 
			
		||||
        loading_diagram: 'Chargement du diagramme...',
 | 
			
		||||
        deselect_all: 'Tout désélectionner',
 | 
			
		||||
        select_all: 'Tout sélectionner',
 | 
			
		||||
        clear: 'Effacer',
 | 
			
		||||
        show_more: 'Afficher Plus',
 | 
			
		||||
        show_less: 'Afficher Moins',
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        copy_to_clipboard: 'Copy to Clipboard',
 | 
			
		||||
        copied: 'Copied!',
 | 
			
		||||
        copy_to_clipboard: 'Copier dans le presse-papiers',
 | 
			
		||||
        copied: 'Copié !',
 | 
			
		||||
 | 
			
		||||
        side_panel: {
 | 
			
		||||
            schema: 'Schéma:',
 | 
			
		||||
@@ -114,6 +115,11 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
                add_table: 'Ajouter une Table',
 | 
			
		||||
                filter: 'Filtrer',
 | 
			
		||||
                collapse: 'Réduire Tout',
 | 
			
		||||
                clear: 'Effacer le Filtre',
 | 
			
		||||
                no_results:
 | 
			
		||||
                    'Aucune table trouvée correspondant à votre filtre.',
 | 
			
		||||
                show_list: 'Afficher la Liste des Tableaux',
 | 
			
		||||
                show_dbml: "Afficher l'éditeur DBML",
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'Champs',
 | 
			
		||||
@@ -145,7 +151,7 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
                        title: 'Actions de la Table',
 | 
			
		||||
                        add_field: 'Ajouter un Champ',
 | 
			
		||||
                        add_index: 'Ajouter un Index',
 | 
			
		||||
                        duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
                        duplicate_table: 'Tableau dupliqué',
 | 
			
		||||
                        delete_table: 'Supprimer la Table',
 | 
			
		||||
                        change_schema: 'Changer le Schéma',
 | 
			
		||||
                    },
 | 
			
		||||
@@ -228,14 +234,12 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
                    step_2: 'Si vous utilisez "Résultats en Grille", changez le nombre maximum de caractères récupérés pour les données non-XML (définir à 9999999).',
 | 
			
		||||
                },
 | 
			
		||||
                instructions_link: "Besoin d'aide ? Regardez comment",
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                check_script_result: 'Check Script Result',
 | 
			
		||||
                check_script_result: 'Vérifier le résultat du Script',
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            cancel: 'Annuler',
 | 
			
		||||
            back: 'Retour',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            import_from_file: 'Import from File',
 | 
			
		||||
            import_from_file: "Importer à partir d'un fichier",
 | 
			
		||||
            empty_diagram: 'Diagramme vide',
 | 
			
		||||
            continue: 'Continuer',
 | 
			
		||||
            import: 'Importer',
 | 
			
		||||
@@ -350,29 +354,42 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
                cancel: 'Annuler',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        export_diagram_dialog: {
 | 
			
		||||
            title: 'Export Diagram',
 | 
			
		||||
            description: 'Choose the format for export:',
 | 
			
		||||
            title: 'Exporter le Diagramme',
 | 
			
		||||
            description: "Sélectionner le format d'exportation :",
 | 
			
		||||
            format_json: 'JSON',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            export: 'Export',
 | 
			
		||||
            cancel: 'Annuler',
 | 
			
		||||
            export: 'Exporter',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error exporting diagram',
 | 
			
		||||
                title: "Erreur lors de l'exportation du diagramme",
 | 
			
		||||
                description:
 | 
			
		||||
                    'Something went wrong. Need help? chartdb.io@gmail.com',
 | 
			
		||||
                    "Une erreur s'est produite. Besoin d'aide ? chartdb.io@gmail.com",
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_diagram_dialog: {
 | 
			
		||||
            title: 'Import Diagram',
 | 
			
		||||
            description: 'Paste the diagram JSON below:',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            title: 'Importer un diagramme',
 | 
			
		||||
            description: 'Coller le diagramme au format JSON ci-dessous :',
 | 
			
		||||
            cancel: 'Annuler',
 | 
			
		||||
            import: 'Exporter',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error importing diagram',
 | 
			
		||||
                title: "Erreur lors de l'exportation du diagramme",
 | 
			
		||||
                description:
 | 
			
		||||
                    'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
 | 
			
		||||
                    "Le diagramme JSON n'est pas valide. Veuillez vérifier le JSON et réessayer. Besoin d'aide ? chartdb.io@gmail.com",
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: "Exemple d'importation DBML",
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description:
 | 
			
		||||
                'Importer un schéma de base de données à partir du format DBML.',
 | 
			
		||||
            import: 'Importer',
 | 
			
		||||
            cancel: 'Annuler',
 | 
			
		||||
            skip_and_empty: 'Passer et vider',
 | 
			
		||||
            show_example: 'Afficher un exemple',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Erreur',
 | 
			
		||||
                description:
 | 
			
		||||
                    "Erreur d'analyse du DBML. Veuillez vérifier la syntaxe.",
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
@@ -389,12 +406,13 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        table_node_context_menu: {
 | 
			
		||||
            edit_table: 'Éditer la Table',
 | 
			
		||||
            duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
            duplicate_table: 'Tableau Dupliqué',
 | 
			
		||||
            delete_table: 'Supprimer la Table',
 | 
			
		||||
            add_relationship: 'Ajouter une Relation',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // TODO: Add translations
 | 
			
		||||
        snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
 | 
			
		||||
        snap_to_grid_tooltip:
 | 
			
		||||
            'Aligner sur la grille (maintenir la touche {{key}})',
 | 
			
		||||
 | 
			
		||||
        tool_tips: {
 | 
			
		||||
            double_click_to_edit: 'Double-cliquez pour modifier',
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const gu: LanguageTranslation = {
 | 
			
		||||
                new: 'નવું',
 | 
			
		||||
                open: 'ખોલો',
 | 
			
		||||
                save: 'સાચવો',
 | 
			
		||||
                import_database: 'ડેટાબેસ આયાત કરો',
 | 
			
		||||
                import: 'ડેટાબેસ આયાત કરો',
 | 
			
		||||
                export_sql: 'SQL નિકાસ કરો',
 | 
			
		||||
                export_as: 'રૂપે નિકાસ કરો',
 | 
			
		||||
                delete_diagram: 'ડાયાગ્રામ કાઢી નાખો',
 | 
			
		||||
@@ -30,15 +30,19 @@ export const gu: LanguageTranslation = {
 | 
			
		||||
                theme: 'થિમ',
 | 
			
		||||
                show_dependencies: 'નિર્ભરતાઓ બતાવો',
 | 
			
		||||
                hide_dependencies: 'નિર્ભરતાઓ છુપાવો',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'શેર કરો',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'બેકઅપ',
 | 
			
		||||
                export_diagram: 'ડાયાગ્રામ નિકાસ કરો',
 | 
			
		||||
                import_diagram: 'ડાયાગ્રામ આયાત કરો',
 | 
			
		||||
                restore_diagram: 'ડાયાગ્રામ પુનઃસ્થાપિત કરો',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'મદદ',
 | 
			
		||||
                docs_website: 'દસ્તાવેજીકરણ',
 | 
			
		||||
                visit_website: 'ChartDB વેબસાઇટ પર જાઓ',
 | 
			
		||||
                join_discord: 'અમારા Discordમાં જોડાઓ',
 | 
			
		||||
                schedule_a_call: 'અમારી સાથે વાત કરો!',
 | 
			
		||||
@@ -102,7 +106,6 @@ export const gu: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: 'છેલ્લે સાચવ્યું',
 | 
			
		||||
        saved: 'સાચવ્યું',
 | 
			
		||||
        diagrams: 'ડાયાગ્રામ',
 | 
			
		||||
        loading_diagram: 'ડાયાગ્રામ લોડ થઈ રહ્યું છે...',
 | 
			
		||||
        deselect_all: 'બધાને ડીસેલેક્ટ કરો',
 | 
			
		||||
        select_all: 'બધા પસંદ કરો',
 | 
			
		||||
@@ -123,6 +126,12 @@ export const gu: LanguageTranslation = {
 | 
			
		||||
                add_table: 'ટેબલ ઉમેરો',
 | 
			
		||||
                filter: 'ફિલ્ટર',
 | 
			
		||||
                collapse: 'બધાને સકુચિત કરો',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'ફીલ્ડ્સ',
 | 
			
		||||
@@ -371,6 +380,20 @@ export const gu: LanguageTranslation = {
 | 
			
		||||
                    'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'એકથી એક',
 | 
			
		||||
            one_to_many: 'એકથી ઘણા',
 | 
			
		||||
@@ -387,6 +410,7 @@ export const gu: LanguageTranslation = {
 | 
			
		||||
            edit_table: 'ટેબલ સંપાદિત કરો',
 | 
			
		||||
            duplicate_table: 'ટેબલ નકલ કરો',
 | 
			
		||||
            delete_table: 'ટેબલ કાઢી નાખો',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        snap_to_grid_tooltip: 'ગ્રિડ પર સ્નેપ કરો (જમાવટ {{key}})',
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const hi: LanguageTranslation = {
 | 
			
		||||
                new: 'नया',
 | 
			
		||||
                open: 'खोलें',
 | 
			
		||||
                save: 'सहेजें',
 | 
			
		||||
                import_database: 'डेटाबेस आयात करें',
 | 
			
		||||
                import: 'डेटाबेस आयात करें',
 | 
			
		||||
                export_sql: 'SQL निर्यात करें',
 | 
			
		||||
                export_as: 'के रूप में निर्यात करें',
 | 
			
		||||
                delete_diagram: 'आरेख हटाएँ',
 | 
			
		||||
@@ -30,15 +30,18 @@ export const hi: LanguageTranslation = {
 | 
			
		||||
                theme: 'थीम',
 | 
			
		||||
                show_dependencies: 'निर्भरता दिखाएँ',
 | 
			
		||||
                hide_dependencies: 'निर्भरता छिपाएँ',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Share',
 | 
			
		||||
                export_diagram: 'Export Diagram',
 | 
			
		||||
                import_diagram: 'Import Diagram',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'बैकअप',
 | 
			
		||||
                export_diagram: 'आरेख निर्यात करें',
 | 
			
		||||
                restore_diagram: 'आरेख पुनर्स्थापित करें',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'मदद',
 | 
			
		||||
                docs_website: 'દસ્તાવેજીકરણ',
 | 
			
		||||
                visit_website: 'ChartDB वेबसाइट पर जाएँ',
 | 
			
		||||
                join_discord: 'हमसे Discord पर जुड़ें',
 | 
			
		||||
                schedule_a_call: 'हमसे बात करें!',
 | 
			
		||||
@@ -102,7 +105,6 @@ export const hi: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: 'अंतिम सहेजा गया',
 | 
			
		||||
        saved: 'सहेजा गया',
 | 
			
		||||
        diagrams: 'आरेख',
 | 
			
		||||
        loading_diagram: 'आरेख लोड हो रहा है...',
 | 
			
		||||
        deselect_all: 'सभी को अचयनित करें',
 | 
			
		||||
        select_all: 'सभी को चुनें',
 | 
			
		||||
@@ -124,6 +126,12 @@ export const hi: LanguageTranslation = {
 | 
			
		||||
                add_table: 'तालिका जोड़ें',
 | 
			
		||||
                filter: 'फ़िल्टर',
 | 
			
		||||
                collapse: 'सभी को संक्षिप्त करें',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'फ़ील्ड्स',
 | 
			
		||||
@@ -375,6 +383,20 @@ export const hi: LanguageTranslation = {
 | 
			
		||||
                    'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'एक से एक',
 | 
			
		||||
            one_to_many: 'एक से कई',
 | 
			
		||||
@@ -391,6 +413,7 @@ export const hi: LanguageTranslation = {
 | 
			
		||||
            edit_table: 'तालिका संपादित करें',
 | 
			
		||||
            duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
            delete_table: 'तालिका हटाएँ',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // TODO: Add translations
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const id_ID: LanguageTranslation = {
 | 
			
		||||
                new: 'Buat Baru',
 | 
			
		||||
                open: 'Buka',
 | 
			
		||||
                save: 'Simpan',
 | 
			
		||||
                import_database: 'Impor Database',
 | 
			
		||||
                import: 'Impor Database',
 | 
			
		||||
                export_sql: 'Ekspor SQL',
 | 
			
		||||
                export_as: 'Ekspor Sebagai',
 | 
			
		||||
                delete_diagram: 'Hapus Diagram',
 | 
			
		||||
@@ -30,14 +30,18 @@ export const id_ID: LanguageTranslation = {
 | 
			
		||||
                theme: 'Tema',
 | 
			
		||||
                show_dependencies: 'Tampilkan Dependensi',
 | 
			
		||||
                hide_dependencies: 'Sembunyikan Dependensi',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Bagikan',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'Cadangan',
 | 
			
		||||
                export_diagram: 'Ekspor Diagram',
 | 
			
		||||
                import_diagram: 'Impor Diagram',
 | 
			
		||||
                restore_diagram: 'Pulihkan Diagram',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'Bantuan',
 | 
			
		||||
                docs_website: 'દસ્તાવેજીકરણ',
 | 
			
		||||
                visit_website: 'Kunjungi ChartDB',
 | 
			
		||||
                join_discord: 'Bergabunglah di Discord kami',
 | 
			
		||||
                schedule_a_call: 'Berbicara dengan kami!',
 | 
			
		||||
@@ -101,7 +105,6 @@ export const id_ID: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: 'Terakhir disimpan',
 | 
			
		||||
        saved: 'Tersimpan',
 | 
			
		||||
        diagrams: 'Diagram',
 | 
			
		||||
        loading_diagram: 'Memuat diagram...',
 | 
			
		||||
        deselect_all: 'Batalkan Semua',
 | 
			
		||||
        select_all: 'Pilih Semua',
 | 
			
		||||
@@ -122,6 +125,12 @@ export const id_ID: LanguageTranslation = {
 | 
			
		||||
                add_table: 'Tambah Tabel',
 | 
			
		||||
                filter: 'Saring',
 | 
			
		||||
                collapse: 'Lipat Semua',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'Kolom',
 | 
			
		||||
@@ -369,6 +378,20 @@ export const id_ID: LanguageTranslation = {
 | 
			
		||||
                    'Diagram JSON tidak valid. Silakan cek JSON dan coba lagi. Butuh bantuan? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'Satu ke Satu',
 | 
			
		||||
@@ -386,6 +409,7 @@ export const id_ID: LanguageTranslation = {
 | 
			
		||||
            edit_table: 'Ubah Tabel',
 | 
			
		||||
            delete_table: 'Hapus Tabel',
 | 
			
		||||
            duplicate_table: 'Duplikat Tabel',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        snap_to_grid_tooltip: 'Snap ke Kisi (Tahan {{key}})',
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const ja: LanguageTranslation = {
 | 
			
		||||
                new: '新規',
 | 
			
		||||
                open: '開く',
 | 
			
		||||
                save: '保存',
 | 
			
		||||
                import_database: 'データベースをインポート',
 | 
			
		||||
                import: 'データベースをインポート',
 | 
			
		||||
                export_sql: 'SQLをエクスポート',
 | 
			
		||||
                export_as: '形式を指定してエクスポート',
 | 
			
		||||
                delete_diagram: 'ダイアグラムを削除',
 | 
			
		||||
@@ -31,15 +31,19 @@ export const ja: LanguageTranslation = {
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_dependencies: 'Show Dependencies',
 | 
			
		||||
                hide_dependencies: 'Hide Dependencies',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Share',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'Backup',
 | 
			
		||||
                export_diagram: 'Export Diagram',
 | 
			
		||||
                import_diagram: 'Import Diagram',
 | 
			
		||||
                restore_diagram: 'Restore Diagram',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'ヘルプ',
 | 
			
		||||
                docs_website: 'ドキュメント',
 | 
			
		||||
                visit_website: 'ChartDBにアクセス',
 | 
			
		||||
                join_discord: 'Discordに参加',
 | 
			
		||||
                schedule_a_call: '話しかけてください!',
 | 
			
		||||
@@ -104,7 +108,6 @@ export const ja: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: '最後に保存された',
 | 
			
		||||
        saved: '保存されました',
 | 
			
		||||
        diagrams: 'ダイアグラム',
 | 
			
		||||
        loading_diagram: 'ダイアグラムを読み込み中...',
 | 
			
		||||
        deselect_all: 'すべての選択を解除',
 | 
			
		||||
        select_all: 'すべてを選択',
 | 
			
		||||
@@ -126,6 +129,12 @@ export const ja: LanguageTranslation = {
 | 
			
		||||
                add_table: 'テーブルを追加',
 | 
			
		||||
                filter: 'フィルタ',
 | 
			
		||||
                collapse: 'すべて折りたたむ',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'フィールド',
 | 
			
		||||
@@ -378,6 +387,20 @@ export const ja: LanguageTranslation = {
 | 
			
		||||
                    'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: '1対1',
 | 
			
		||||
            one_to_many: '1対多',
 | 
			
		||||
@@ -394,6 +417,7 @@ export const ja: LanguageTranslation = {
 | 
			
		||||
            edit_table: 'テーブルを編集',
 | 
			
		||||
            duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
            delete_table: 'テーブルを削除',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // TODO: Add translations
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const ko_KR: LanguageTranslation = {
 | 
			
		||||
                new: '새 다이어그램',
 | 
			
		||||
                open: '열기',
 | 
			
		||||
                save: '저장',
 | 
			
		||||
                import_database: '데이터베이스 가져오기',
 | 
			
		||||
                import: '데이터베이스 가져오기',
 | 
			
		||||
                export_sql: 'SQL로 저장',
 | 
			
		||||
                export_as: '다른 형식으로 저장',
 | 
			
		||||
                delete_diagram: '다이어그램 삭제',
 | 
			
		||||
@@ -30,14 +30,18 @@ export const ko_KR: LanguageTranslation = {
 | 
			
		||||
                theme: '테마',
 | 
			
		||||
                show_dependencies: '종속성 보이기',
 | 
			
		||||
                hide_dependencies: '종속성 숨기기',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            share: {
 | 
			
		||||
                share: '공유',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: '백업',
 | 
			
		||||
                export_diagram: '다이어그램 내보내기',
 | 
			
		||||
                import_diagram: '다이어그램 가져오기',
 | 
			
		||||
                restore_diagram: '다이어그램 복구',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: '도움말',
 | 
			
		||||
                docs_website: '선적 서류 비치',
 | 
			
		||||
                visit_website: 'ChartDB 사이트 방문',
 | 
			
		||||
                join_discord: 'Discord 가입',
 | 
			
		||||
                schedule_a_call: 'Talk with us!',
 | 
			
		||||
@@ -101,7 +105,6 @@ export const ko_KR: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: '최근 저장일시: ',
 | 
			
		||||
        saved: '저장됨',
 | 
			
		||||
        diagrams: '다이어그램',
 | 
			
		||||
        loading_diagram: '다이어그램 로딩중...',
 | 
			
		||||
        deselect_all: '모두 선택 해제',
 | 
			
		||||
        select_all: '모두 선택',
 | 
			
		||||
@@ -122,6 +125,12 @@ export const ko_KR: LanguageTranslation = {
 | 
			
		||||
                add_table: '테이블 추가',
 | 
			
		||||
                filter: '필터',
 | 
			
		||||
                collapse: '모두 접기',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: '필드',
 | 
			
		||||
@@ -367,6 +376,20 @@ export const ko_KR: LanguageTranslation = {
 | 
			
		||||
                    '다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: '일대일 (1:1)',
 | 
			
		||||
            one_to_many: '일대다 (1:N)',
 | 
			
		||||
@@ -383,6 +406,7 @@ export const ko_KR: LanguageTranslation = {
 | 
			
		||||
            edit_table: '테이블 수정',
 | 
			
		||||
            duplicate_table: '테이블 복제',
 | 
			
		||||
            delete_table: '테이블 삭제',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        snap_to_grid_tooltip: '그리드에 맞추기 ({{key}}를 누른채 유지)',
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const mr: LanguageTranslation = {
 | 
			
		||||
                new: 'नवीन',
 | 
			
		||||
                open: 'उघडा',
 | 
			
		||||
                save: 'जतन करा',
 | 
			
		||||
                import_database: 'डेटाबेस इम्पोर्ट करा',
 | 
			
		||||
                import: 'डेटाबेस इम्पोर्ट करा',
 | 
			
		||||
                export_sql: 'SQL एक्स्पोर्ट करा',
 | 
			
		||||
                export_as: 'म्हणून एक्स्पोर्ट करा',
 | 
			
		||||
                delete_diagram: 'आरेख हटवा',
 | 
			
		||||
@@ -30,15 +30,19 @@ export const mr: LanguageTranslation = {
 | 
			
		||||
                theme: 'थीम',
 | 
			
		||||
                show_dependencies: 'डिपेंडेन्सि दाखवा',
 | 
			
		||||
                hide_dependencies: 'डिपेंडेन्सि लपवा',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            share: {
 | 
			
		||||
            backup: {
 | 
			
		||||
                // TODO: Add translations
 | 
			
		||||
                share: 'Share',
 | 
			
		||||
                backup: 'Backup',
 | 
			
		||||
                export_diagram: 'Export Diagram',
 | 
			
		||||
                import_diagram: 'Import Diagram',
 | 
			
		||||
                restore_diagram: 'Restore Diagram',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'मदत',
 | 
			
		||||
                docs_website: 'दस्तऐवजीकरण',
 | 
			
		||||
                visit_website: 'ChartDB ला भेट द्या',
 | 
			
		||||
                join_discord: 'आमच्या डिस्कॉर्डमध्ये सामील व्हा',
 | 
			
		||||
                schedule_a_call: 'आमच्याशी बोला!',
 | 
			
		||||
@@ -102,7 +106,6 @@ export const mr: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: 'शेवटचे जतन केले',
 | 
			
		||||
        saved: 'जतन केले',
 | 
			
		||||
        diagrams: 'आरेख',
 | 
			
		||||
        loading_diagram: 'आरेख लोड करत आहे...',
 | 
			
		||||
        deselect_all: 'सर्व निवड रद्द करा',
 | 
			
		||||
        select_all: 'सर्व निवडा',
 | 
			
		||||
@@ -125,6 +128,12 @@ export const mr: LanguageTranslation = {
 | 
			
		||||
                add_table: 'टेबल जोडा',
 | 
			
		||||
                filter: 'फिल्टर',
 | 
			
		||||
                collapse: 'सर्व संकुचित करा',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'फील्ड्स',
 | 
			
		||||
@@ -379,6 +388,20 @@ export const mr: LanguageTranslation = {
 | 
			
		||||
                    'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'एक ते एक',
 | 
			
		||||
@@ -395,8 +418,8 @@ export const mr: LanguageTranslation = {
 | 
			
		||||
        table_node_context_menu: {
 | 
			
		||||
            edit_table: 'टेबल संपादित करा',
 | 
			
		||||
            delete_table: 'टेबल हटवा',
 | 
			
		||||
            // TODO: Add translations
 | 
			
		||||
            duplicate_table: 'Duplicate Table',
 | 
			
		||||
            duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // TODO: Add translations
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const ne: LanguageTranslation = {
 | 
			
		||||
                new: 'नयाँ',
 | 
			
		||||
                open: 'खोल्नुहोस्',
 | 
			
		||||
                save: 'सुरक्षित गर्नुहोस्',
 | 
			
		||||
                import_database: 'डाटाबेस आयात गर्नुहोस्',
 | 
			
		||||
                import: 'डाटाबेस आयात गर्नुहोस्',
 | 
			
		||||
                export_sql: 'SQL निर्यात गर्नुहोस्',
 | 
			
		||||
                export_as: 'निर्यात गर्नुहोस्',
 | 
			
		||||
                delete_diagram: 'डायाग्राम हटाउनुहोस्',
 | 
			
		||||
@@ -30,14 +30,19 @@ export const ne: LanguageTranslation = {
 | 
			
		||||
                theme: 'थिम',
 | 
			
		||||
                show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
 | 
			
		||||
                hide_dependencies: 'डिपेन्डेन्सीहरू लुकाउनुहोस्',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'शेयर गर्नुहोस्',
 | 
			
		||||
                export_diagram: 'डायाग्राम निर्यात गर्नुहोस्',
 | 
			
		||||
                import_diagram: 'डायाग्राम आयात गर्नुहोस्',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'Backup',
 | 
			
		||||
                export_diagram: 'Export Diagram',
 | 
			
		||||
                restore_diagram: 'Restore Diagram',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'मद्दत',
 | 
			
		||||
                docs_website: 'कागजात',
 | 
			
		||||
                visit_website: 'वेबसाइटमा जानुहोस्',
 | 
			
		||||
                join_discord: 'डिस्कोर्डमा सामिल हुनुहोस्',
 | 
			
		||||
                schedule_a_call: 'कल अनुसूची गर्नुहोस्',
 | 
			
		||||
@@ -101,7 +106,6 @@ export const ne: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: 'अन्तिम सुरक्षित',
 | 
			
		||||
        saved: 'सुरक्षित',
 | 
			
		||||
        diagrams: 'डायाग्रामहरू',
 | 
			
		||||
        loading_diagram: 'डायाग्राम लोड हुँदैछ...',
 | 
			
		||||
        deselect_all: 'सबै चयन हटाउनुहोस्',
 | 
			
		||||
        select_all: 'सबै चयन गर्नुहोस्',
 | 
			
		||||
@@ -122,6 +126,12 @@ export const ne: LanguageTranslation = {
 | 
			
		||||
                add_table: 'तालिका थप्नुहोस्',
 | 
			
		||||
                filter: 'फिल्टर',
 | 
			
		||||
                collapse: 'सबै लुकाउनुहोस्',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'क्षेत्रहरू',
 | 
			
		||||
@@ -372,6 +382,20 @@ export const ne: LanguageTranslation = {
 | 
			
		||||
                    'डायाग्राम JSON अमान्य छ। कृपया JSON जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्। मद्दत चाहिन्छ? chartdb.io@gmail.com मा सम्पर्क गर्नुहोस्',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'एक देखि एक',
 | 
			
		||||
@@ -389,6 +413,7 @@ export const ne: LanguageTranslation = {
 | 
			
		||||
            edit_table: 'तालिका सम्पादन गर्नुहोस्',
 | 
			
		||||
            duplicate_table: 'तालिका नक्कली गर्नुहोस्',
 | 
			
		||||
            delete_table: 'तालिका हटाउनुहोस्',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        snap_to_grid_tooltip: 'ग्रिडमा स्न्याप गर्नुहोस् ({{key}} थिच्नुहोस)',
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const pt_BR: LanguageTranslation = {
 | 
			
		||||
                new: 'Novo',
 | 
			
		||||
                open: 'Abrir',
 | 
			
		||||
                save: 'Salvar',
 | 
			
		||||
                import_database: 'Importar Banco de Dados',
 | 
			
		||||
                import: 'Importar Banco de Dados',
 | 
			
		||||
                export_sql: 'Exportar SQL',
 | 
			
		||||
                export_as: 'Exportar como',
 | 
			
		||||
                delete_diagram: 'Excluir Diagrama',
 | 
			
		||||
@@ -30,15 +30,19 @@ export const pt_BR: LanguageTranslation = {
 | 
			
		||||
                theme: 'Tema',
 | 
			
		||||
                show_dependencies: 'Mostrar Dependências',
 | 
			
		||||
                hide_dependencies: 'Ocultar Dependências',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Share',
 | 
			
		||||
                export_diagram: 'Export Diagram',
 | 
			
		||||
                import_diagram: 'Import Diagram',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'Backup',
 | 
			
		||||
                export_diagram: 'Exportar Diagrama',
 | 
			
		||||
                restore_diagram: 'Restaurar Diagrama',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'Ajuda',
 | 
			
		||||
                docs_website: 'Documentação',
 | 
			
		||||
                visit_website: 'Visitar ChartDB',
 | 
			
		||||
                join_discord: 'Junte-se a nós no Discord',
 | 
			
		||||
                schedule_a_call: 'Fale Conosco!',
 | 
			
		||||
@@ -102,7 +106,6 @@ export const pt_BR: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: 'Última vez salvo',
 | 
			
		||||
        saved: 'Salvo',
 | 
			
		||||
        diagrams: 'Diagramas',
 | 
			
		||||
        loading_diagram: 'Carregando diagrama...',
 | 
			
		||||
        deselect_all: 'Desmarcar Todos',
 | 
			
		||||
        select_all: 'Selecionar Todos',
 | 
			
		||||
@@ -123,6 +126,12 @@ export const pt_BR: LanguageTranslation = {
 | 
			
		||||
                add_table: 'Adicionar Tabela',
 | 
			
		||||
                filter: 'Filtrar',
 | 
			
		||||
                collapse: 'Colapsar Todas',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'Campos',
 | 
			
		||||
@@ -372,6 +381,20 @@ export const pt_BR: LanguageTranslation = {
 | 
			
		||||
                    'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'Um para Um',
 | 
			
		||||
            one_to_many: 'Um para Muitos',
 | 
			
		||||
@@ -388,6 +411,7 @@ export const pt_BR: LanguageTranslation = {
 | 
			
		||||
            edit_table: 'Editar Tabela',
 | 
			
		||||
            duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
            delete_table: 'Excluir Tabela',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // TODO: Add translations
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const ru: LanguageTranslation = {
 | 
			
		||||
                new: 'Создать',
 | 
			
		||||
                open: 'Открыть',
 | 
			
		||||
                save: 'Сохранить',
 | 
			
		||||
                import_database: 'Импортировать базу данных',
 | 
			
		||||
                import: 'Импортировать базу данных',
 | 
			
		||||
                export_sql: 'Экспорт SQL',
 | 
			
		||||
                export_as: 'Экспортировать как',
 | 
			
		||||
                delete_diagram: 'Удалить диаграмму',
 | 
			
		||||
@@ -30,14 +30,19 @@ export const ru: LanguageTranslation = {
 | 
			
		||||
                theme: 'Тема',
 | 
			
		||||
                show_dependencies: 'Показать зависимости',
 | 
			
		||||
                hide_dependencies: 'Скрыть зависимости',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Поделиться',
 | 
			
		||||
                export_diagram: 'Экспорт кода диаграммы',
 | 
			
		||||
                import_diagram: 'Импорт кода диаграммы',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'Backup',
 | 
			
		||||
                export_diagram: 'Export Diagram',
 | 
			
		||||
                restore_diagram: 'Restore Diagram',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'Помощь',
 | 
			
		||||
                docs_website: 'Документация',
 | 
			
		||||
                visit_website: 'Перейти на сайт ChartDB',
 | 
			
		||||
                join_discord: 'Присоединиться к сообществу в Discord',
 | 
			
		||||
                schedule_a_call: 'Поговорите с нами!',
 | 
			
		||||
@@ -102,7 +107,6 @@ export const ru: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: 'Последнее сохранение',
 | 
			
		||||
        saved: 'Сохранено',
 | 
			
		||||
        diagrams: 'Диаграммы',
 | 
			
		||||
        loading_diagram: 'Загрузка диаграммы...',
 | 
			
		||||
        deselect_all: 'Отменить выбор всех',
 | 
			
		||||
        select_all: 'Выбрать все',
 | 
			
		||||
@@ -121,6 +125,12 @@ export const ru: LanguageTranslation = {
 | 
			
		||||
                add_table: 'Добавить таблицу',
 | 
			
		||||
                filter: 'Фильтр',
 | 
			
		||||
                collapse: 'Свернуть все',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'Поля',
 | 
			
		||||
@@ -368,6 +378,20 @@ export const ru: LanguageTranslation = {
 | 
			
		||||
                    'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'Один к одному',
 | 
			
		||||
            one_to_many: 'Один ко многим',
 | 
			
		||||
@@ -384,6 +408,7 @@ export const ru: LanguageTranslation = {
 | 
			
		||||
            edit_table: 'Изменить таблицу',
 | 
			
		||||
            duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
            delete_table: 'Удалить таблицу',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        copy_to_clipboard: 'Скопировать в буфер обмена',
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const te: LanguageTranslation = {
 | 
			
		||||
                new: 'కొత్తది',
 | 
			
		||||
                open: 'తెరవు',
 | 
			
		||||
                save: 'సేవ్',
 | 
			
		||||
                import_database: 'డేటాబేస్ను దిగుమతి చేసుకోండి',
 | 
			
		||||
                import: 'డేటాబేస్ను దిగుమతి చేసుకోండి',
 | 
			
		||||
                export_sql: 'SQL ఎగుమతి',
 | 
			
		||||
                export_as: 'వగా ఎగుమతి చేయండి',
 | 
			
		||||
                delete_diagram: 'చిత్రాన్ని తొలగించండి',
 | 
			
		||||
@@ -30,15 +30,19 @@ export const te: LanguageTranslation = {
 | 
			
		||||
                theme: 'థీమ్',
 | 
			
		||||
                show_dependencies: 'ఆధారాలు చూపించండి',
 | 
			
		||||
                hide_dependencies: 'ఆధారాలను దాచండి',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Share',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'Backup',
 | 
			
		||||
                export_diagram: 'Export Diagram',
 | 
			
		||||
                import_diagram: 'Import Diagram',
 | 
			
		||||
                restore_diagram: 'Restore Diagram',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'సహాయం',
 | 
			
		||||
                docs_website: 'డాక్యుమెంటేషన్',
 | 
			
		||||
                visit_website: 'ChartDB సందర్శించండి',
 | 
			
		||||
                join_discord: 'డిస్కార్డ్లో మా నుంచి చేరండి',
 | 
			
		||||
                schedule_a_call: 'మాతో మాట్లాడండి!',
 | 
			
		||||
@@ -102,7 +106,6 @@ export const te: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: 'చివరిగా సేవ్ చేయబడిన',
 | 
			
		||||
        saved: 'సేవ్ చేయబడింది',
 | 
			
		||||
        diagrams: 'చిత్రాలు',
 | 
			
		||||
        loading_diagram: 'చిత్రం లోడ్ అవుతోంది...',
 | 
			
		||||
        deselect_all: 'అన్ని ఎంచుకోకుండా ఉంచు',
 | 
			
		||||
        select_all: 'అన్ని ఎంచుకోండి',
 | 
			
		||||
@@ -123,6 +126,12 @@ export const te: LanguageTranslation = {
 | 
			
		||||
                add_table: 'పట్టికను జోడించు',
 | 
			
		||||
                filter: 'ఫిల్టర్',
 | 
			
		||||
                collapse: 'అన్ని కూల్ చేయి',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'ఫీల్డులు',
 | 
			
		||||
@@ -375,6 +384,20 @@ export const te: LanguageTranslation = {
 | 
			
		||||
                    'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'ఒకటి_కీ_ఒకటి',
 | 
			
		||||
@@ -390,9 +413,9 @@ export const te: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        table_node_context_menu: {
 | 
			
		||||
            edit_table: 'పట్టికను సవరించు',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            duplicate_table: 'Duplicate Table',
 | 
			
		||||
            duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
            delete_table: 'పట్టికను తొలగించు',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const tr: LanguageTranslation = {
 | 
			
		||||
                new: 'Yeni',
 | 
			
		||||
                open: 'Aç',
 | 
			
		||||
                save: 'Kaydet',
 | 
			
		||||
                import_database: 'Veritabanı İçe Aktar',
 | 
			
		||||
                import: 'Veritabanı İçe Aktar',
 | 
			
		||||
                export_sql: 'SQL Olarak Dışa Aktar',
 | 
			
		||||
                export_as: 'Olarak Dışa Aktar',
 | 
			
		||||
                delete_diagram: 'Diyagramı Sil',
 | 
			
		||||
@@ -30,15 +30,19 @@ export const tr: LanguageTranslation = {
 | 
			
		||||
                theme: 'Tema',
 | 
			
		||||
                show_dependencies: 'Bağımlılıkları Göster',
 | 
			
		||||
                hide_dependencies: 'Bağımlılıkları Gizle',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Share',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'Backup',
 | 
			
		||||
                export_diagram: 'Export Diagram',
 | 
			
		||||
                import_diagram: 'Import Diagram',
 | 
			
		||||
                restore_diagram: 'Restore Diagram',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'Yardım',
 | 
			
		||||
                docs_website: 'Belgeleme',
 | 
			
		||||
                visit_website: "ChartDB'yi Ziyaret Et",
 | 
			
		||||
                join_discord: "Discord'a Katıl",
 | 
			
		||||
                schedule_a_call: 'Bize Ulaş!',
 | 
			
		||||
@@ -102,8 +106,6 @@ export const tr: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: 'Son kaydedilen',
 | 
			
		||||
        saved: 'Kaydedildi',
 | 
			
		||||
        diagrams: 'Diyagramlar',
 | 
			
		||||
 | 
			
		||||
        loading_diagram: 'Diyagram yükleniyor...',
 | 
			
		||||
        deselect_all: 'Hepsini Seçme',
 | 
			
		||||
        select_all: 'Hepsini Seç',
 | 
			
		||||
@@ -123,6 +125,13 @@ export const tr: LanguageTranslation = {
 | 
			
		||||
                add_table: 'Tablo Ekle',
 | 
			
		||||
                filter: 'Filtrele',
 | 
			
		||||
                collapse: 'Hepsini Daralt',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'Alanlar',
 | 
			
		||||
                    nullable: 'Boş Bırakılabilir?',
 | 
			
		||||
@@ -362,6 +371,20 @@ export const tr: LanguageTranslation = {
 | 
			
		||||
                    'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'Bir Bir',
 | 
			
		||||
            one_to_many: 'Bir Çok',
 | 
			
		||||
@@ -375,8 +398,8 @@ export const tr: LanguageTranslation = {
 | 
			
		||||
        table_node_context_menu: {
 | 
			
		||||
            edit_table: 'Tabloyu Düzenle',
 | 
			
		||||
            delete_table: 'Tabloyu Sil',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            duplicate_table: 'Duplicate Table',
 | 
			
		||||
            duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
 
 | 
			
		||||
@@ -4,44 +4,46 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
    translation: {
 | 
			
		||||
        menu: {
 | 
			
		||||
            file: {
 | 
			
		||||
                file: 'файл',
 | 
			
		||||
                new: 'новий',
 | 
			
		||||
                open: 'відкрити',
 | 
			
		||||
                save: 'зберегти',
 | 
			
		||||
                import_database: 'Імпорт бази даних',
 | 
			
		||||
                file: 'Файл',
 | 
			
		||||
                new: 'Новий',
 | 
			
		||||
                open: 'Відкрити',
 | 
			
		||||
                save: 'Зберегти',
 | 
			
		||||
                import: 'Імпорт бази даних',
 | 
			
		||||
                export_sql: 'Експорт SQL',
 | 
			
		||||
                export_as: 'Експортувати як',
 | 
			
		||||
                delete_diagram: 'Видалити діаграму',
 | 
			
		||||
                exit: 'вийти',
 | 
			
		||||
                exit: 'Вийти',
 | 
			
		||||
            },
 | 
			
		||||
            edit: {
 | 
			
		||||
                edit: 'редагувати',
 | 
			
		||||
                edit: 'Редагувати',
 | 
			
		||||
                undo: 'Скасувати',
 | 
			
		||||
                redo: 'Повторити',
 | 
			
		||||
                clear: 'очистити',
 | 
			
		||||
                clear: 'Очистити',
 | 
			
		||||
            },
 | 
			
		||||
            view: {
 | 
			
		||||
                view: 'переглянути',
 | 
			
		||||
                view: 'Перегляд',
 | 
			
		||||
                show_sidebar: 'Показати бічну панель',
 | 
			
		||||
                hide_sidebar: 'Приховати бічну панель',
 | 
			
		||||
                hide_cardinality: 'Приховати потужність',
 | 
			
		||||
                show_cardinality: 'Показати кардинальність',
 | 
			
		||||
                zoom_on_scroll: 'Збільшити прокручування',
 | 
			
		||||
                zoom_on_scroll: 'Масштабувати прокручуванням',
 | 
			
		||||
                theme: 'Тема',
 | 
			
		||||
                show_dependencies: 'Показати залежності',
 | 
			
		||||
                hide_dependencies: 'Приховати залежності',
 | 
			
		||||
                show_minimap: 'Показати мінімапу',
 | 
			
		||||
                hide_minimap: 'Приховати мінімапу',
 | 
			
		||||
            },
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Share',
 | 
			
		||||
                export_diagram: 'Export Diagram',
 | 
			
		||||
                import_diagram: 'Import Diagram',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'Резервне копіювання',
 | 
			
		||||
                export_diagram: 'Експорт діаграми',
 | 
			
		||||
                restore_diagram: 'Відновити діаграму',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'Допомога',
 | 
			
		||||
                visit_website: 'Відвідайте ChartDB',
 | 
			
		||||
                help: 'Довідка',
 | 
			
		||||
                docs_website: 'Документація',
 | 
			
		||||
                visit_website: 'Сайт ChartDB',
 | 
			
		||||
                join_discord: 'Приєднуйтесь до нас в Діскорд',
 | 
			
		||||
                schedule_a_call: 'Поговоріть з нами!',
 | 
			
		||||
                schedule_a_call: 'Забронювати зустріч!',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -54,18 +56,18 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        clear_diagram_alert: {
 | 
			
		||||
            title: 'Чітка діаграма',
 | 
			
		||||
            title: 'Очистити діаграму',
 | 
			
		||||
            description:
 | 
			
		||||
                'Цю дію не можна скасувати. Це назавжди видалить усі дані на діаграмі.',
 | 
			
		||||
            cancel: 'Скасувати',
 | 
			
		||||
            clear: 'очистити',
 | 
			
		||||
            clear: 'Очистити',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        reorder_diagram_alert: {
 | 
			
		||||
            title: 'Діаграма зміни порядку',
 | 
			
		||||
            title: 'Перевпорядкувати діаграму',
 | 
			
		||||
            description:
 | 
			
		||||
                'Ця дія перевпорядкує всі таблиці на діаграмі. Хочете продовжити?',
 | 
			
		||||
            reorder: 'Змінити порядок',
 | 
			
		||||
            reorder: 'Перевпорядкувати',
 | 
			
		||||
            cancel: 'Скасувати',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -90,24 +92,23 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        theme: {
 | 
			
		||||
            system: 'система',
 | 
			
		||||
            light: 'світлий',
 | 
			
		||||
            dark: 'Темний',
 | 
			
		||||
            system: 'Системна',
 | 
			
		||||
            light: 'Світла',
 | 
			
		||||
            dark: 'Темна',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        zoom: {
 | 
			
		||||
            on: 'увімкнути',
 | 
			
		||||
            off: 'вимкнути',
 | 
			
		||||
            on: 'Увімкнути',
 | 
			
		||||
            off: 'Вимкнути',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        last_saved: 'Востаннє збережено',
 | 
			
		||||
        saved: 'Збережено',
 | 
			
		||||
        diagrams: 'Діаграми',
 | 
			
		||||
        loading_diagram: 'Діаграма завантаження...',
 | 
			
		||||
        deselect_all: 'Зняти вибір із усіх',
 | 
			
		||||
        loading_diagram: 'Завантаження діаграми…',
 | 
			
		||||
        deselect_all: 'Зняти виділення з усіх',
 | 
			
		||||
        select_all: 'Вибрати усі',
 | 
			
		||||
        clear: 'Очистити',
 | 
			
		||||
        show_more: 'показати більше',
 | 
			
		||||
        show_more: 'Показати більше',
 | 
			
		||||
        show_less: 'Показати менше',
 | 
			
		||||
        copy_to_clipboard: 'Копіювати в буфер обміну',
 | 
			
		||||
        copied: 'Скопійовано!',
 | 
			
		||||
@@ -115,47 +116,53 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
        side_panel: {
 | 
			
		||||
            schema: 'Схема:',
 | 
			
		||||
            filter_by_schema: 'Фільтрувати за схемою',
 | 
			
		||||
            search_schema: 'Схема пошуку...',
 | 
			
		||||
            search_schema: 'Пошук схеми…',
 | 
			
		||||
            no_schemas_found: 'Схеми не знайдено.',
 | 
			
		||||
            view_all_options: 'Переглянути всі параметри...',
 | 
			
		||||
            view_all_options: 'Переглянути всі параметри…',
 | 
			
		||||
            tables_section: {
 | 
			
		||||
                tables: 'Таблиці',
 | 
			
		||||
                add_table: 'Додати таблицю',
 | 
			
		||||
                filter: 'фільтр',
 | 
			
		||||
                filter: 'Фільтр',
 | 
			
		||||
                collapse: 'Згорнути все',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'поля',
 | 
			
		||||
                    nullable: 'Зведений нанівець?',
 | 
			
		||||
                    fields: 'Поля',
 | 
			
		||||
                    nullable: 'Може бути Null?',
 | 
			
		||||
                    primary_key: 'Первинний ключ',
 | 
			
		||||
                    indexes: 'Індекси',
 | 
			
		||||
                    comments: 'Коментарі',
 | 
			
		||||
                    no_comments: 'Без коментарів',
 | 
			
		||||
                    no_comments: 'Немає коментарів',
 | 
			
		||||
                    add_field: 'Додати поле',
 | 
			
		||||
                    add_index: 'Додати індекс',
 | 
			
		||||
                    index_select_fields: 'Виберіть поля',
 | 
			
		||||
                    no_types_found: 'Типи не знайдено',
 | 
			
		||||
                    field_name: "Ім'я",
 | 
			
		||||
                    field_name: 'Назва поля',
 | 
			
		||||
                    field_type: 'Тип',
 | 
			
		||||
                    field_actions: {
 | 
			
		||||
                        title: 'Атрибути полів',
 | 
			
		||||
                        unique: 'Унікальний',
 | 
			
		||||
                        unique: 'Унікальне',
 | 
			
		||||
                        comments: 'Коментарі',
 | 
			
		||||
                        no_comments: 'Без коментарів',
 | 
			
		||||
                        no_comments: 'Немає коментарів',
 | 
			
		||||
                        delete_field: 'Видалити поле',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'Атрибути індексу',
 | 
			
		||||
                        name: "Ім'я",
 | 
			
		||||
                        name: 'Назва індекса',
 | 
			
		||||
                        unique: 'Унікальний',
 | 
			
		||||
                        delete_index: 'Видалити індекс',
 | 
			
		||||
                    },
 | 
			
		||||
                    table_actions: {
 | 
			
		||||
                        title: 'Дії таблиці',
 | 
			
		||||
                        title: 'Дії з таблицею',
 | 
			
		||||
                        change_schema: 'Змінити схему',
 | 
			
		||||
                        add_field: 'Додати поле',
 | 
			
		||||
                        add_index: 'Додати індекс',
 | 
			
		||||
                        duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
                        duplicate_table: 'Дублювати таблицю',
 | 
			
		||||
                        delete_table: 'Видалити таблицю',
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
@@ -165,14 +172,14 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            relationships_section: {
 | 
			
		||||
                relationships: 'стосунки',
 | 
			
		||||
                filter: 'фільтр',
 | 
			
		||||
                add_relationship: "Додати зв'язок",
 | 
			
		||||
                relationships: 'Звʼязки',
 | 
			
		||||
                filter: 'Фільтр',
 | 
			
		||||
                add_relationship: 'Додати звʼязок',
 | 
			
		||||
                collapse: 'Згорнути все',
 | 
			
		||||
                relationship: {
 | 
			
		||||
                    primary: 'Первинна таблиця',
 | 
			
		||||
                    foreign: 'Посилання на таблицю',
 | 
			
		||||
                    cardinality: 'Кардинальність',
 | 
			
		||||
                    cardinality: 'Звʼязок',
 | 
			
		||||
                    delete_relationship: 'Видалити',
 | 
			
		||||
                    relationship_actions: {
 | 
			
		||||
                        title: 'Дії',
 | 
			
		||||
@@ -180,17 +187,17 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
                empty_state: {
 | 
			
		||||
                    title: 'Жодних стосунків',
 | 
			
		||||
                    description: 'Створіть зв’язок для з’єднання таблиць',
 | 
			
		||||
                    title: 'Звʼязків немає',
 | 
			
		||||
                    description: 'Створіть звʼязок для зʼєднання таблиць',
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            dependencies_section: {
 | 
			
		||||
                dependencies: 'Залежності',
 | 
			
		||||
                filter: 'фільтр',
 | 
			
		||||
                filter: 'Фільтр',
 | 
			
		||||
                collapse: 'Згорнути все',
 | 
			
		||||
                dependency: {
 | 
			
		||||
                    table: 'Таблиця',
 | 
			
		||||
                    dependent_table: 'Залежний вид',
 | 
			
		||||
                    dependent_table: 'Залежне подання',
 | 
			
		||||
                    delete_dependency: 'Видалити',
 | 
			
		||||
                    dependency_actions: {
 | 
			
		||||
                        title: 'Дії',
 | 
			
		||||
@@ -207,34 +214,34 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
        toolbar: {
 | 
			
		||||
            zoom_in: 'Збільшити',
 | 
			
		||||
            zoom_out: 'Зменшити',
 | 
			
		||||
            save: 'зберегти',
 | 
			
		||||
            save: 'Зберегти',
 | 
			
		||||
            show_all: 'Показати все',
 | 
			
		||||
            undo: 'Скасувати',
 | 
			
		||||
            redo: 'Повторити',
 | 
			
		||||
            reorder_diagram: 'Діаграма зміни порядку',
 | 
			
		||||
            highlight_overlapping_tables: 'Виділіть таблиці, що перекриваються',
 | 
			
		||||
            reorder_diagram: 'Перевпорядкувати діаграму',
 | 
			
		||||
            highlight_overlapping_tables: 'Показати таблиці, що перекриваються',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
            database_selection: {
 | 
			
		||||
                title: 'Що таке ваша база даних?',
 | 
			
		||||
                title: 'Яка у вас база даних?',
 | 
			
		||||
                description:
 | 
			
		||||
                    'Кожна база даних має свої унікальні особливості та можливості.',
 | 
			
		||||
                check_examples_long: 'Перевірте приклади',
 | 
			
		||||
                check_examples_long: 'Подивіться приклади',
 | 
			
		||||
                check_examples_short: 'Приклади',
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            import_database: {
 | 
			
		||||
                title: 'Імпортуйте вашу базу даних',
 | 
			
		||||
                database_edition: 'Редакція бази даних:',
 | 
			
		||||
                database_edition: 'Варіант бази даних:',
 | 
			
		||||
                step_1: 'Запустіть цей сценарій у своїй базі даних:',
 | 
			
		||||
                step_2: 'Вставте сюди результат сценарію:',
 | 
			
		||||
                script_results_placeholder: 'Результати сценарію тут...',
 | 
			
		||||
                script_results_placeholder: 'Результати сценарію має бути тут…',
 | 
			
		||||
                ssms_instructions: {
 | 
			
		||||
                    button_text: 'SSMS Інструкції',
 | 
			
		||||
                    title: 'Інструкції',
 | 
			
		||||
                    step_1: 'Перейдіть до Інструменти > Опції > Результати запиту > SQL Сервер.',
 | 
			
		||||
                    step_2: 'Якщо ви використовуєте «Результати в сітку», змініть максимальну кількість символів, отриманих для даних, що не є XML (встановіть на 9999999).',
 | 
			
		||||
                    step_2: 'Якщо ви використовуєте «Results to Grid», змініть максимальну кількість символів, отриманих для даних, що не є XML (встановіть на 9999999).',
 | 
			
		||||
                },
 | 
			
		||||
                instructions_link: 'Потрібна допомога? Подивіться як',
 | 
			
		||||
                check_script_result: 'Перевірте результат сценарію',
 | 
			
		||||
@@ -242,20 +249,19 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
            cancel: 'Скасувати',
 | 
			
		||||
            back: 'Назад',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            import_from_file: 'Import from File',
 | 
			
		||||
            import_from_file: 'Імпортувати з файлу',
 | 
			
		||||
            empty_diagram: 'Порожня діаграма',
 | 
			
		||||
            continue: 'Продовжити',
 | 
			
		||||
            import: 'Імпорт',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        open_diagram_dialog: {
 | 
			
		||||
            title: 'Відкрита діаграма',
 | 
			
		||||
            title: 'Відкрити діаграму',
 | 
			
		||||
            description:
 | 
			
		||||
                'Виберіть діаграму, яку потрібно відкрити, зі списку нижче.',
 | 
			
		||||
            table_columns: {
 | 
			
		||||
                name: "Ім'я",
 | 
			
		||||
                created_at: 'Створено в',
 | 
			
		||||
                name: 'Назва',
 | 
			
		||||
                created_at: 'Створено0',
 | 
			
		||||
                last_modified: 'Востаннє змінено',
 | 
			
		||||
                tables_count: 'Таблиці',
 | 
			
		||||
            },
 | 
			
		||||
@@ -269,23 +275,23 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
                'Експортуйте свою схему діаграми в {{databaseType}} сценарій',
 | 
			
		||||
            close: 'Закрити',
 | 
			
		||||
            loading: {
 | 
			
		||||
                text: 'ШІ створює SQL для {{databaseType}}...',
 | 
			
		||||
                text: 'ШІ створює SQL для {{databaseType}}…',
 | 
			
		||||
                description: 'Це має зайняти до 30 секунд.',
 | 
			
		||||
            },
 | 
			
		||||
            error: {
 | 
			
		||||
                message:
 | 
			
		||||
                    "Помилка створення сценарію SQL. Спробуйте пізніше або <0>зв'яжіться з нами</0>.",
 | 
			
		||||
                    'Помилка створення сценарію SQL. Спробуйте пізніше або <0>звʼяжіться з нами</0>.',
 | 
			
		||||
                description:
 | 
			
		||||
                    'Не соромтеся використовувати свій OPENAI_TOKEN, дивіться посібник <0>тут</0>.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        create_relationship_dialog: {
 | 
			
		||||
            title: 'Створити відносини',
 | 
			
		||||
            title: 'Створити звʼязок',
 | 
			
		||||
            primary_table: 'Первинна таблиця',
 | 
			
		||||
            primary_field: 'Первинне поле',
 | 
			
		||||
            referenced_table: 'Посилання на таблицю',
 | 
			
		||||
            referenced_field: 'Поле посилання',
 | 
			
		||||
            referenced_table: 'Звʼязана таблиця',
 | 
			
		||||
            referenced_field: 'Повʼязане поле',
 | 
			
		||||
            primary_table_placeholder: 'Виберіть таблицю',
 | 
			
		||||
            primary_field_placeholder: 'Виберіть поле',
 | 
			
		||||
            referenced_table_placeholder: 'Виберіть таблицю',
 | 
			
		||||
@@ -305,12 +311,12 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
                    new_tables:
 | 
			
		||||
                        '<bold>{{newTablesNumber}}</bold> будуть додані нові таблиці.',
 | 
			
		||||
                    new_relationships:
 | 
			
		||||
                        '<bold>{{newRelationshipsNumber}}</bold> будуть створені нові відносини.',
 | 
			
		||||
                        '<bold>{{newRelationshipsNumber}}</bold> будуть створені нові звʼязки.',
 | 
			
		||||
                    tables_override:
 | 
			
		||||
                        '<bold>{{tablesOverrideNumber}}</bold> таблиці будуть перезаписані.',
 | 
			
		||||
                    proceed: 'Ви хочете продовжити?',
 | 
			
		||||
                },
 | 
			
		||||
                import: 'Імпорт',
 | 
			
		||||
                import: 'Імпортувати',
 | 
			
		||||
                cancel: 'Скасувати',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
@@ -318,83 +324,95 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
        export_image_dialog: {
 | 
			
		||||
            title: 'Експорт зображення',
 | 
			
		||||
            description: 'Виберіть коефіцієнт масштабування для експорту:',
 | 
			
		||||
            scale_1x: '1x Регулярний',
 | 
			
		||||
            scale_1x: '1x Звичайний',
 | 
			
		||||
            scale_2x: '2x (Рекомендовано)',
 | 
			
		||||
            scale_3x: '3x',
 | 
			
		||||
            scale_4x: '4x',
 | 
			
		||||
            cancel: 'Скасувати',
 | 
			
		||||
            export: 'Експорт',
 | 
			
		||||
            export: 'Експортувати',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_table_schema_dialog: {
 | 
			
		||||
            title: 'Виберіть Схему',
 | 
			
		||||
            description:
 | 
			
		||||
                'Наразі відображається кілька схем. Виберіть один для нової таблиці.',
 | 
			
		||||
                'Наразі показується кілька схем. Виберіть одну для нової таблиці.',
 | 
			
		||||
            cancel: 'Скасувати',
 | 
			
		||||
            confirm: 'Підтвердити',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        update_table_schema_dialog: {
 | 
			
		||||
            title: 'Змінити схему',
 | 
			
		||||
            description: 'Оновити таблицю "{{tableName}}" схему',
 | 
			
		||||
            description: 'Оновити схему таблиці "{{tableName}}"',
 | 
			
		||||
            cancel: 'Скасувати',
 | 
			
		||||
            confirm: 'Змінити',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        star_us_dialog: {
 | 
			
		||||
            title: 'Допоможіть нам покращитися!',
 | 
			
		||||
            description: 'Хочете позначити нас на Ґітхаб? Це лише один клік!',
 | 
			
		||||
            description: 'Поставне на зірку на GitHub? Це лише один клік!',
 | 
			
		||||
            close: 'Не зараз',
 | 
			
		||||
            confirm: 'звичайно!',
 | 
			
		||||
            confirm: 'Звісно!',
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        export_diagram_dialog: {
 | 
			
		||||
            title: 'Export Diagram',
 | 
			
		||||
            description: 'Choose the format for export:',
 | 
			
		||||
            title: 'Експорт Діаграми',
 | 
			
		||||
            description: 'Оберіть формат експорту:',
 | 
			
		||||
            format_json: 'JSON',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            export: 'Export',
 | 
			
		||||
            cancel: 'Скасувати',
 | 
			
		||||
            export: 'Експортувати',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error exporting diagram',
 | 
			
		||||
                title: 'Помилка експорут діаграми',
 | 
			
		||||
                description:
 | 
			
		||||
                    'Something went wrong. Need help? chartdb.io@gmail.com',
 | 
			
		||||
                    'Щось пішло не так. Потрібна допомога? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        import_diagram_dialog: {
 | 
			
		||||
            title: 'Імпорт Діаграми',
 | 
			
		||||
            description: 'Вставте JSON діаграми нижче:',
 | 
			
		||||
            cancel: 'Скасувати',
 | 
			
		||||
            import: 'Імпортувати',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Помилка імпорту діаграми',
 | 
			
		||||
                description:
 | 
			
		||||
                    'JSON діаграми є неправильним. Будь ласка, перевірте JSON і спробуйте ще раз. Потрібна допомога? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_diagram_dialog: {
 | 
			
		||||
            title: 'Import Diagram',
 | 
			
		||||
            description: 'Paste the diagram JSON below:',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error importing diagram',
 | 
			
		||||
                description:
 | 
			
		||||
                    'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'Один до одного',
 | 
			
		||||
            one_to_many: 'Один до багатьох',
 | 
			
		||||
            many_to_one: 'Багато до одного',
 | 
			
		||||
            many_to_many: 'Багато до багатьох',
 | 
			
		||||
            one_to_one: 'Один до Одного',
 | 
			
		||||
            one_to_many: 'Один до Багатьох',
 | 
			
		||||
            many_to_one: 'Багато до Одного',
 | 
			
		||||
            many_to_many: 'Багато до Багатьох',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        canvas_context_menu: {
 | 
			
		||||
            new_table: 'Нова таблиця',
 | 
			
		||||
            new_relationship: 'Нові стосунки',
 | 
			
		||||
            new_relationship: 'Новий звʼязок',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        table_node_context_menu: {
 | 
			
		||||
            edit_table: 'Редагувати таблицю',
 | 
			
		||||
            duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
            duplicate_table: 'Дублювати таблицю',
 | 
			
		||||
            delete_table: 'Видалити таблицю',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // TODO: Add translations
 | 
			
		||||
        snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
 | 
			
		||||
        snap_to_grid_tooltip: 'Вирівнювати за сіткою (Отримуйте {{key}})',
 | 
			
		||||
 | 
			
		||||
        tool_tips: {
 | 
			
		||||
            double_click_to_edit: 'Двойной клик для редактирования',
 | 
			
		||||
            double_click_to_edit: 'Подвійне клацання для редагування',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        language_select: {
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const vi: LanguageTranslation = {
 | 
			
		||||
                new: 'Tạo mới',
 | 
			
		||||
                open: 'Mở',
 | 
			
		||||
                save: 'Lưu',
 | 
			
		||||
                import_database: 'Nhập cơ sở dữ liệu',
 | 
			
		||||
                import: 'Nhập cơ sở dữ liệu',
 | 
			
		||||
                export_sql: 'Xuất SQL',
 | 
			
		||||
                export_as: 'Xuất thành',
 | 
			
		||||
                delete_diagram: 'Xóa sơ đồ',
 | 
			
		||||
@@ -30,14 +30,18 @@ export const vi: LanguageTranslation = {
 | 
			
		||||
                theme: 'Chủ đề',
 | 
			
		||||
                show_dependencies: 'Hiển thị các phụ thuộc',
 | 
			
		||||
                hide_dependencies: 'Ẩn các phụ thuộc',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Chia sẻ',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: 'Hỗ trợ',
 | 
			
		||||
                export_diagram: 'Xuất sơ đồ',
 | 
			
		||||
                import_diagram: 'Nhập sơ đồ',
 | 
			
		||||
                restore_diagram: 'Khôi phục sơ đồ',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'Trợ giúp',
 | 
			
		||||
                docs_website: 'Tài liệu',
 | 
			
		||||
                visit_website: 'Truy cập ChartDB',
 | 
			
		||||
                join_discord: 'Tham gia Discord',
 | 
			
		||||
                schedule_a_call: 'Trò chuyện cùng chúng tôi!',
 | 
			
		||||
@@ -101,7 +105,6 @@ export const vi: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: 'Đã lưu lần cuối',
 | 
			
		||||
        saved: 'Đã lưu',
 | 
			
		||||
        diagrams: 'Sơ đồ',
 | 
			
		||||
        loading_diagram: 'Đang tải sơ đồ...',
 | 
			
		||||
        deselect_all: 'Bỏ chọn tất cả',
 | 
			
		||||
        select_all: 'Chọn tất cả',
 | 
			
		||||
@@ -122,6 +125,12 @@ export const vi: LanguageTranslation = {
 | 
			
		||||
                add_table: 'Thêm bảng',
 | 
			
		||||
                filter: 'Lọc',
 | 
			
		||||
                collapse: 'Thu gọn tất cả',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: 'Trường',
 | 
			
		||||
@@ -368,6 +377,20 @@ export const vi: LanguageTranslation = {
 | 
			
		||||
                    'Sơ đồ ở dạng JSON không hợp lệ. Vui lòng kiểm tra JSON và thử lại. Bạn cần trợ giúp? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'Quan hệ một-một',
 | 
			
		||||
            one_to_many: 'Quan hệ một-nhiều',
 | 
			
		||||
@@ -384,6 +407,7 @@ export const vi: LanguageTranslation = {
 | 
			
		||||
            edit_table: 'Sửa bảng',
 | 
			
		||||
            duplicate_table: 'Nhân đôi bảng',
 | 
			
		||||
            delete_table: 'Xóa bảng',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        snap_to_grid_tooltip: 'Căn lưới (Giữ phím {{key}})',
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const zh_CN: LanguageTranslation = {
 | 
			
		||||
                new: '新建',
 | 
			
		||||
                open: '打开',
 | 
			
		||||
                save: '保存',
 | 
			
		||||
                import_database: '导入数据库',
 | 
			
		||||
                import: '导入数据库',
 | 
			
		||||
                export_sql: '导出 SQL 语句',
 | 
			
		||||
                export_as: '导出为',
 | 
			
		||||
                delete_diagram: '删除关系图',
 | 
			
		||||
@@ -30,14 +30,18 @@ export const zh_CN: LanguageTranslation = {
 | 
			
		||||
                theme: '主题',
 | 
			
		||||
                show_dependencies: '展示依赖',
 | 
			
		||||
                hide_dependencies: '隐藏依赖',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            share: {
 | 
			
		||||
                share: '分享',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: '备份',
 | 
			
		||||
                export_diagram: '导出关系图',
 | 
			
		||||
                import_diagram: '导入关系图',
 | 
			
		||||
                restore_diagram: '还原图表',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: '帮助',
 | 
			
		||||
                docs_website: '文档',
 | 
			
		||||
                visit_website: '访问 ChartDB',
 | 
			
		||||
                join_discord: '在 Discord 上加入我们',
 | 
			
		||||
                schedule_a_call: '和我们交流!',
 | 
			
		||||
@@ -98,7 +102,6 @@ export const zh_CN: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: '上次保存时间:',
 | 
			
		||||
        saved: '已保存',
 | 
			
		||||
        diagrams: '关系图',
 | 
			
		||||
        loading_diagram: '加载关系图...',
 | 
			
		||||
        deselect_all: '取消全选',
 | 
			
		||||
        select_all: '全选',
 | 
			
		||||
@@ -119,6 +122,12 @@ export const zh_CN: LanguageTranslation = {
 | 
			
		||||
                add_table: '添加表',
 | 
			
		||||
                filter: '筛选',
 | 
			
		||||
                collapse: '全部折叠',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: '字段',
 | 
			
		||||
@@ -364,6 +373,20 @@ export const zh_CN: LanguageTranslation = {
 | 
			
		||||
                    '关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: '一对一',
 | 
			
		||||
            one_to_many: '一对多',
 | 
			
		||||
@@ -380,6 +403,7 @@ export const zh_CN: LanguageTranslation = {
 | 
			
		||||
            edit_table: '编辑表',
 | 
			
		||||
            duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
            delete_table: '删除表',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        snap_to_grid_tooltip: '对齐到网格(按住 {{key}})',
 | 
			
		||||
@@ -395,7 +419,7 @@ export const zh_CN: LanguageTranslation = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const zh_CNMetadata: LanguageMetadata = {
 | 
			
		||||
    name: 'Chinese',
 | 
			
		||||
    name: 'Chinese (Simplified)',
 | 
			
		||||
    nativeName: '简体中文',
 | 
			
		||||
    code: 'zh_CN',
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ export const zh_TW: LanguageTranslation = {
 | 
			
		||||
                new: '新增',
 | 
			
		||||
                open: '開啟',
 | 
			
		||||
                save: '儲存',
 | 
			
		||||
                import_database: '匯入資料庫',
 | 
			
		||||
                import: '匯入資料庫',
 | 
			
		||||
                export_sql: '匯出 SQL',
 | 
			
		||||
                export_as: '匯出為特定格式',
 | 
			
		||||
                delete_diagram: '刪除圖表',
 | 
			
		||||
@@ -30,14 +30,18 @@ export const zh_TW: LanguageTranslation = {
 | 
			
		||||
                theme: '主題',
 | 
			
		||||
                show_dependencies: '顯示相依性',
 | 
			
		||||
                hide_dependencies: '隱藏相依性',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_minimap: 'Show Mini Map',
 | 
			
		||||
                hide_minimap: 'Hide Mini Map',
 | 
			
		||||
            },
 | 
			
		||||
            share: {
 | 
			
		||||
                share: '分享',
 | 
			
		||||
            backup: {
 | 
			
		||||
                backup: '備份',
 | 
			
		||||
                export_diagram: '匯出圖表',
 | 
			
		||||
                import_diagram: '匯入圖表',
 | 
			
		||||
                restore_diagram: '恢復圖表',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: '幫助',
 | 
			
		||||
                docs_website: '文件',
 | 
			
		||||
                visit_website: '訪問 ChartDB 網站',
 | 
			
		||||
                join_discord: '加入 Discord',
 | 
			
		||||
                schedule_a_call: '與我們聯絡!',
 | 
			
		||||
@@ -98,7 +102,6 @@ export const zh_TW: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        last_saved: '上次儲存於',
 | 
			
		||||
        saved: '已儲存',
 | 
			
		||||
        diagrams: '圖表',
 | 
			
		||||
        loading_diagram: '正在載入圖表...',
 | 
			
		||||
        deselect_all: '取消所有選取',
 | 
			
		||||
        select_all: '全選',
 | 
			
		||||
@@ -119,6 +122,12 @@ export const zh_TW: LanguageTranslation = {
 | 
			
		||||
                add_table: '新增表格',
 | 
			
		||||
                filter: '篩選',
 | 
			
		||||
                collapse: '全部摺疊',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                clear: 'Clear Filter',
 | 
			
		||||
                no_results: 'No tables found matching your filter.',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_list: 'Show Table List',
 | 
			
		||||
                show_dbml: 'Show DBML Editor',
 | 
			
		||||
 | 
			
		||||
                table: {
 | 
			
		||||
                    fields: '欄位',
 | 
			
		||||
@@ -363,6 +372,20 @@ export const zh_TW: LanguageTranslation = {
 | 
			
		||||
                    '圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_dbml_dialog: {
 | 
			
		||||
            example_title: 'Import Example DBML',
 | 
			
		||||
            title: 'Import DBML',
 | 
			
		||||
            description: 'Import a database schema from DBML format.',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            skip_and_empty: 'Skip & Empty',
 | 
			
		||||
            show_example: 'Show Example',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description: 'Failed to parse DBML. Please check the syntax.',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: '一對一',
 | 
			
		||||
            one_to_many: '一對多',
 | 
			
		||||
@@ -379,6 +402,7 @@ export const zh_TW: LanguageTranslation = {
 | 
			
		||||
            edit_table: '編輯表格',
 | 
			
		||||
            duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
            delete_table: '刪除表格',
 | 
			
		||||
            add_relationship: 'Add Relationship', // TODO: Translate
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        snap_to_grid_tooltip: '對齊網格(按住 {{key}})',
 | 
			
		||||
@@ -395,6 +419,6 @@ export const zh_TW: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
export const zh_TWMetadata: LanguageMetadata = {
 | 
			
		||||
    nativeName: '繁體中文',
 | 
			
		||||
    name: 'Traditional Chinese',
 | 
			
		||||
    name: 'Chinese (Traditional)',
 | 
			
		||||
    code: 'zh_TW',
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -62,3 +62,5 @@ export function areFieldTypesCompatible(
 | 
			
		||||
        dbCompatibleTypes[type2.id]?.includes(type1.id)
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const dataTypes = Object.values(dataTypeMap).flat();
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,9 @@ export const sqliteDataTypes: readonly DataType[] = [
 | 
			
		||||
    // Blob Type
 | 
			
		||||
    { name: 'blob', id: 'blob' },
 | 
			
		||||
 | 
			
		||||
    // Blob Type
 | 
			
		||||
    { name: 'json', id: 'json' },
 | 
			
		||||
 | 
			
		||||
    // Date/Time Types (SQLite uses TEXT, REAL, or INTEGER types for dates and times)
 | 
			
		||||
    { name: 'date', id: 'date' },
 | 
			
		||||
    { name: 'datetime', id: 'datetime' },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										82
									
								
								src/lib/data/export-metadata/export-per-type/common.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/lib/data/export-metadata/export-per-type/common.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
 | 
			
		||||
export function isFunction(value: string): boolean {
 | 
			
		||||
    // Common SQL functions
 | 
			
		||||
    const functionPatterns = [
 | 
			
		||||
        /^CURRENT_TIMESTAMP$/i,
 | 
			
		||||
        /^NOW\(\)$/i,
 | 
			
		||||
        /^GETDATE\(\)$/i,
 | 
			
		||||
        /^CURRENT_DATE$/i,
 | 
			
		||||
        /^CURRENT_TIME$/i,
 | 
			
		||||
        /^UUID\(\)$/i,
 | 
			
		||||
        /^NEWID\(\)$/i,
 | 
			
		||||
        /^NEXT VALUE FOR/i,
 | 
			
		||||
        /^IDENTITY\s*\(\d+,\s*\d+\)$/i,
 | 
			
		||||
    ];
 | 
			
		||||
    return functionPatterns.some((pattern) => pattern.test(value.trim()));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function isKeyword(value: string): boolean {
 | 
			
		||||
    // Common SQL keywords that can be used as default values
 | 
			
		||||
    const keywords = [
 | 
			
		||||
        'NULL',
 | 
			
		||||
        'TRUE',
 | 
			
		||||
        'FALSE',
 | 
			
		||||
        'CURRENT_TIMESTAMP',
 | 
			
		||||
        'CURRENT_DATE',
 | 
			
		||||
        'CURRENT_TIME',
 | 
			
		||||
        'CURRENT_USER',
 | 
			
		||||
        'SESSION_USER',
 | 
			
		||||
        'SYSTEM_USER',
 | 
			
		||||
    ];
 | 
			
		||||
    return keywords.includes(value.trim().toUpperCase());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function strHasQuotes(value: string): boolean {
 | 
			
		||||
    return /^['"].*['"]$/.test(value.trim());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function exportFieldComment(comment: string): string {
 | 
			
		||||
    if (!comment) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return comment
 | 
			
		||||
        .split('\n')
 | 
			
		||||
        .map((commentLine) => `    -- ${commentLine}\n`)
 | 
			
		||||
        .join('');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getInlineFK(table: DBTable, diagram: Diagram): string {
 | 
			
		||||
    if (!diagram.relationships) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const fks = diagram.relationships
 | 
			
		||||
        .filter((r) => r.sourceTableId === table.id)
 | 
			
		||||
        .map((r) => {
 | 
			
		||||
            const targetTable = diagram.tables?.find(
 | 
			
		||||
                (t) => t.id === r.targetTableId
 | 
			
		||||
            );
 | 
			
		||||
            const sourceField = table.fields.find(
 | 
			
		||||
                (f) => f.id === r.sourceFieldId
 | 
			
		||||
            );
 | 
			
		||||
            const targetField = targetTable?.fields.find(
 | 
			
		||||
                (f) => f.id === r.targetFieldId
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (!targetTable || !sourceField || !targetField) {
 | 
			
		||||
                return '';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const targetTableName = targetTable.schema
 | 
			
		||||
                ? `"${targetTable.schema}"."${targetTable.name}"`
 | 
			
		||||
                : `"${targetTable.name}"`;
 | 
			
		||||
 | 
			
		||||
            return `    FOREIGN KEY ("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}")`;
 | 
			
		||||
        })
 | 
			
		||||
        .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
    return fks.join(',\n');
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										247
									
								
								src/lib/data/export-metadata/export-per-type/mssql.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								src/lib/data/export-metadata/export-per-type/mssql.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,247 @@
 | 
			
		||||
import {
 | 
			
		||||
    exportFieldComment,
 | 
			
		||||
    isFunction,
 | 
			
		||||
    isKeyword,
 | 
			
		||||
    strHasQuotes,
 | 
			
		||||
} from './common';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
 | 
			
		||||
 | 
			
		||||
function parseMSSQLDefault(field: DBField): string {
 | 
			
		||||
    if (!field.default) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let defaultValue = field.default.trim();
 | 
			
		||||
 | 
			
		||||
    // Remove type casting for SQL Server
 | 
			
		||||
    defaultValue = defaultValue.split('::')[0];
 | 
			
		||||
 | 
			
		||||
    // Handle nextval sequences for SQL Server
 | 
			
		||||
    if (defaultValue.includes('nextval')) {
 | 
			
		||||
        return 'IDENTITY(1,1)';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Special handling for SQL Server DEFAULT values
 | 
			
		||||
    if (defaultValue.match(/^\(\(.*\)\)$/)) {
 | 
			
		||||
        // Handle ((0)), ((0.00)) style defaults
 | 
			
		||||
        return defaultValue.replace(/^\(\(|\)\)$/g, '');
 | 
			
		||||
    } else if (defaultValue.match(/^\(N'.*'\)$/)) {
 | 
			
		||||
        // Handle (N'value') style defaults
 | 
			
		||||
        const innerValue = defaultValue.replace(/^\(N'|'\)$/g, '');
 | 
			
		||||
        return `N'${innerValue}'`;
 | 
			
		||||
    } else if (defaultValue.match(/^\(NULL\)$/i)) {
 | 
			
		||||
        // Handle (NULL) defaults
 | 
			
		||||
        return 'NULL';
 | 
			
		||||
    } else if (defaultValue.match(/^\(getdate\(\)\)$/i)) {
 | 
			
		||||
        // Handle (getdate()) defaults
 | 
			
		||||
        return 'getdate()';
 | 
			
		||||
    } else if (defaultValue.match(/^\('?\*'?\)$/i) || defaultValue === '*') {
 | 
			
		||||
        // Handle ('*') or (*) or * defaults - common for "all" values
 | 
			
		||||
        return "N'*'";
 | 
			
		||||
    } else if (defaultValue.match(/^\((['"])(.*)\1\)$/)) {
 | 
			
		||||
        // Handle ('value') or ("value") style defaults
 | 
			
		||||
        const matches = defaultValue.match(/^\((['"])(.*)\1\)$/);
 | 
			
		||||
        return matches ? `N'${matches[2]}'` : defaultValue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Handle special characters that could be interpreted as operators
 | 
			
		||||
    const sqlServerSpecialChars = /[*+\-/%&|^!=<>~]/;
 | 
			
		||||
    if (sqlServerSpecialChars.test(defaultValue)) {
 | 
			
		||||
        // If the value contains special characters and isn't already properly quoted
 | 
			
		||||
        if (
 | 
			
		||||
            !strHasQuotes(defaultValue) &&
 | 
			
		||||
            !isFunction(defaultValue) &&
 | 
			
		||||
            !isKeyword(defaultValue)
 | 
			
		||||
        ) {
 | 
			
		||||
            return `N'${defaultValue.replace(/'/g, "''")}'`;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        strHasQuotes(defaultValue) ||
 | 
			
		||||
        isFunction(defaultValue) ||
 | 
			
		||||
        isKeyword(defaultValue) ||
 | 
			
		||||
        /^-?\d+(\.\d+)?$/.test(defaultValue)
 | 
			
		||||
    ) {
 | 
			
		||||
        return defaultValue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return `'${defaultValue}'`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function exportMSSQL(diagram: Diagram): string {
 | 
			
		||||
    if (!diagram.tables || !diagram.relationships) {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const tables = diagram.tables;
 | 
			
		||||
    const relationships = diagram.relationships;
 | 
			
		||||
 | 
			
		||||
    // Create CREATE SCHEMA statements for all schemas
 | 
			
		||||
    let sqlScript = '';
 | 
			
		||||
    const schemas = new Set<string>();
 | 
			
		||||
 | 
			
		||||
    tables.forEach((table) => {
 | 
			
		||||
        if (table.schema) {
 | 
			
		||||
            schemas.add(table.schema);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Add schema creation statements
 | 
			
		||||
    schemas.forEach((schema) => {
 | 
			
		||||
        sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n    EXEC('CREATE SCHEMA [${schema}]');\nEND;\n\n`;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Generate table creation SQL
 | 
			
		||||
    sqlScript += tables
 | 
			
		||||
        .map((table: DBTable) => {
 | 
			
		||||
            // Skip views
 | 
			
		||||
            if (table.isView) {
 | 
			
		||||
                return '';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const tableName = table.schema
 | 
			
		||||
                ? `[${table.schema}].[${table.name}]`
 | 
			
		||||
                : `[${table.name}]`;
 | 
			
		||||
 | 
			
		||||
            return `${
 | 
			
		||||
                table.comments ? `/**\n${table.comments}\n*/\n` : ''
 | 
			
		||||
            }CREATE TABLE ${tableName} (\n${table.fields
 | 
			
		||||
                .map((field: DBField) => {
 | 
			
		||||
                    const fieldName = `[${field.name}]`;
 | 
			
		||||
                    const typeName = field.type.name;
 | 
			
		||||
 | 
			
		||||
                    // Handle SQL Server specific type formatting
 | 
			
		||||
                    let typeWithSize = typeName;
 | 
			
		||||
                    if (field.characterMaximumLength) {
 | 
			
		||||
                        if (
 | 
			
		||||
                            typeName.toLowerCase() === 'varchar' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'nvarchar' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'char' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'nchar'
 | 
			
		||||
                        ) {
 | 
			
		||||
                            typeWithSize = `${typeName}(${field.characterMaximumLength})`;
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (field.precision && field.scale) {
 | 
			
		||||
                        if (
 | 
			
		||||
                            typeName.toLowerCase() === 'decimal' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'numeric'
 | 
			
		||||
                        ) {
 | 
			
		||||
                            typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
 | 
			
		||||
                        }
 | 
			
		||||
                    } else if (field.precision) {
 | 
			
		||||
                        if (
 | 
			
		||||
                            typeName.toLowerCase() === 'decimal' ||
 | 
			
		||||
                            typeName.toLowerCase() === 'numeric'
 | 
			
		||||
                        ) {
 | 
			
		||||
                            typeWithSize = `${typeName}(${field.precision})`;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    const notNull = field.nullable ? '' : ' NOT NULL';
 | 
			
		||||
 | 
			
		||||
                    // Check if identity column
 | 
			
		||||
                    const identity = field.default
 | 
			
		||||
                        ?.toLowerCase()
 | 
			
		||||
                        .includes('identity')
 | 
			
		||||
                        ? ' IDENTITY(1,1)'
 | 
			
		||||
                        : '';
 | 
			
		||||
 | 
			
		||||
                    const unique =
 | 
			
		||||
                        !field.primaryKey && field.unique ? ' UNIQUE' : '';
 | 
			
		||||
 | 
			
		||||
                    // Handle default value using SQL Server specific parser
 | 
			
		||||
                    const defaultValue =
 | 
			
		||||
                        field.default &&
 | 
			
		||||
                        !field.default.toLowerCase().includes('identity')
 | 
			
		||||
                            ? ` DEFAULT ${parseMSSQLDefault(field)}`
 | 
			
		||||
                            : '';
 | 
			
		||||
 | 
			
		||||
                    // Do not add PRIMARY KEY as a column constraint - will add as table constraint
 | 
			
		||||
                    return `${exportFieldComment(field.comments ?? '')}    ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
 | 
			
		||||
                })
 | 
			
		||||
                .join(',\n')}${
 | 
			
		||||
                table.fields.filter((f) => f.primaryKey).length > 0
 | 
			
		||||
                    ? `,\n    PRIMARY KEY (${table.fields
 | 
			
		||||
                          .filter((f) => f.primaryKey)
 | 
			
		||||
                          .map((f) => `[${f.name}]`)
 | 
			
		||||
                          .join(', ')})`
 | 
			
		||||
                    : ''
 | 
			
		||||
            }\n);\n\n${table.indexes
 | 
			
		||||
                .map((index) => {
 | 
			
		||||
                    const indexName = table.schema
 | 
			
		||||
                        ? `[${table.schema}_${index.name}]`
 | 
			
		||||
                        : `[${index.name}]`;
 | 
			
		||||
                    const indexFields = index.fieldIds
 | 
			
		||||
                        .map((fieldId) => {
 | 
			
		||||
                            const field = table.fields.find(
 | 
			
		||||
                                (f) => f.id === fieldId
 | 
			
		||||
                            );
 | 
			
		||||
                            return field ? `[${field.name}]` : '';
 | 
			
		||||
                        })
 | 
			
		||||
                        .filter(Boolean);
 | 
			
		||||
 | 
			
		||||
                    // SQL Server has a limit of 32 columns in an index
 | 
			
		||||
                    if (indexFields.length > 32) {
 | 
			
		||||
                        const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`;
 | 
			
		||||
                        console.warn(
 | 
			
		||||
                            `Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.`
 | 
			
		||||
                        );
 | 
			
		||||
                        indexFields.length = 32;
 | 
			
		||||
                        return indexFields.length > 0
 | 
			
		||||
                            ? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
 | 
			
		||||
                            : '';
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return indexFields.length > 0
 | 
			
		||||
                        ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
 | 
			
		||||
                        : '';
 | 
			
		||||
                })
 | 
			
		||||
                .join('')}`;
 | 
			
		||||
        })
 | 
			
		||||
        .filter(Boolean) // Remove empty strings (views)
 | 
			
		||||
        .join('\n');
 | 
			
		||||
 | 
			
		||||
    // Generate foreign keys
 | 
			
		||||
    sqlScript += `\n${relationships
 | 
			
		||||
        .map((r: DBRelationship) => {
 | 
			
		||||
            const sourceTable = tables.find((t) => t.id === r.sourceTableId);
 | 
			
		||||
            const targetTable = tables.find((t) => t.id === r.targetTableId);
 | 
			
		||||
 | 
			
		||||
            if (
 | 
			
		||||
                !sourceTable ||
 | 
			
		||||
                !targetTable ||
 | 
			
		||||
                sourceTable.isView ||
 | 
			
		||||
                targetTable.isView
 | 
			
		||||
            ) {
 | 
			
		||||
                return '';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const sourceField = sourceTable.fields.find(
 | 
			
		||||
                (f) => f.id === r.sourceFieldId
 | 
			
		||||
            );
 | 
			
		||||
            const targetField = targetTable.fields.find(
 | 
			
		||||
                (f) => f.id === r.targetFieldId
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if (!sourceField || !targetField) {
 | 
			
		||||
                return '';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const sourceTableName = sourceTable.schema
 | 
			
		||||
                ? `[${sourceTable.schema}].[${sourceTable.name}]`
 | 
			
		||||
                : `[${sourceTable.name}]`;
 | 
			
		||||
            const targetTableName = targetTable.schema
 | 
			
		||||
                ? `[${targetTable.schema}].[${targetTable.name}]`
 | 
			
		||||
                : `[${targetTable.name}]`;
 | 
			
		||||
 | 
			
		||||
            return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT [${r.name}] FOREIGN KEY([${sourceField.name}]) REFERENCES ${targetTableName}([${targetField.name}]);\n`;
 | 
			
		||||
        })
 | 
			
		||||
        .filter(Boolean) // Remove empty strings
 | 
			
		||||
        .join('\n')}`;
 | 
			
		||||
 | 
			
		||||
    return sqlScript;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
import type { Diagram } from '../../domain/diagram';
 | 
			
		||||
import { OPENAI_API_KEY } from '@/lib/env';
 | 
			
		||||
import type { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import type { DataType } from '../data-types/data-types';
 | 
			
		||||
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
 | 
			
		||||
import { exportMSSQL } from './export-per-type/mssql';
 | 
			
		||||
 | 
			
		||||
export const exportBaseSQL = (diagram: Diagram): string => {
 | 
			
		||||
    const { tables, relationships } = diagram;
 | 
			
		||||
@@ -12,6 +13,10 @@ export const exportBaseSQL = (diagram: Diagram): string => {
 | 
			
		||||
        return '';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (diagram.databaseType === DatabaseType.SQL_SERVER) {
 | 
			
		||||
        return exportMSSQL(diagram);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Filter out the tables that are views
 | 
			
		||||
    const nonViewTables = tables.filter((table) => !table.isView);
 | 
			
		||||
 | 
			
		||||
@@ -196,6 +201,26 @@ export const exportBaseSQL = (diagram: Diagram): string => {
 | 
			
		||||
    return sqlScript;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const validateConfiguration = () => {
 | 
			
		||||
    const apiKey = window?.env?.OPENAI_API_KEY ?? OPENAI_API_KEY;
 | 
			
		||||
    const baseUrl = window?.env?.OPENAI_API_ENDPOINT ?? OPENAI_API_ENDPOINT;
 | 
			
		||||
    const modelName = window?.env?.LLM_MODEL_NAME ?? LLM_MODEL_NAME;
 | 
			
		||||
 | 
			
		||||
    // If using custom endpoint and model, don't require OpenAI API key
 | 
			
		||||
    if (baseUrl && modelName) {
 | 
			
		||||
        return { useCustomEndpoint: true };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If using OpenAI's service, require API key
 | 
			
		||||
    if (apiKey) {
 | 
			
		||||
        return { useCustomEndpoint: false };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    throw new Error(
 | 
			
		||||
        'Configuration Error: Either provide an OpenAI API key or both a custom endpoint and model name'
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const exportSQL = async (
 | 
			
		||||
    diagram: Diagram,
 | 
			
		||||
    databaseType: DatabaseType,
 | 
			
		||||
@@ -206,6 +231,10 @@ export const exportSQL = async (
 | 
			
		||||
    }
 | 
			
		||||
): Promise<string> => {
 | 
			
		||||
    const sqlScript = exportBaseSQL(diagram);
 | 
			
		||||
    if (databaseType === DatabaseType.SQL_SERVER) {
 | 
			
		||||
        return sqlScript;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const cacheKey = await generateCacheKey(databaseType, sqlScript);
 | 
			
		||||
 | 
			
		||||
    const cachedResult = getFromCache(cacheKey);
 | 
			
		||||
@@ -213,43 +242,76 @@ export const exportSQL = async (
 | 
			
		||||
        return cachedResult;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Validate configuration before proceeding
 | 
			
		||||
    const { useCustomEndpoint } = validateConfiguration();
 | 
			
		||||
 | 
			
		||||
    const [{ streamText, generateText }, { createOpenAI }] = await Promise.all([
 | 
			
		||||
        import('ai'),
 | 
			
		||||
        import('@ai-sdk/openai'),
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const openai = createOpenAI({
 | 
			
		||||
        apiKey: window?.env?.OPENAI_API_KEY ?? OPENAI_API_KEY,
 | 
			
		||||
    });
 | 
			
		||||
    const apiKey = window?.env?.OPENAI_API_KEY ?? OPENAI_API_KEY;
 | 
			
		||||
    const baseUrl = window?.env?.OPENAI_API_ENDPOINT ?? OPENAI_API_ENDPOINT;
 | 
			
		||||
    const modelName =
 | 
			
		||||
        window?.env?.LLM_MODEL_NAME ??
 | 
			
		||||
        LLM_MODEL_NAME ??
 | 
			
		||||
        'gpt-4o-mini-2024-07-18';
 | 
			
		||||
 | 
			
		||||
    let config: { apiKey: string; baseUrl?: string };
 | 
			
		||||
 | 
			
		||||
    if (useCustomEndpoint) {
 | 
			
		||||
        config = {
 | 
			
		||||
            apiKey: apiKey,
 | 
			
		||||
            baseUrl: baseUrl,
 | 
			
		||||
        };
 | 
			
		||||
    } else {
 | 
			
		||||
        config = {
 | 
			
		||||
            apiKey: apiKey,
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const openai = createOpenAI(config);
 | 
			
		||||
 | 
			
		||||
    const prompt = generateSQLPrompt(databaseType, sqlScript);
 | 
			
		||||
 | 
			
		||||
    if (options?.stream) {
 | 
			
		||||
        const { textStream, text: textPromise } = await streamText({
 | 
			
		||||
            model: openai('gpt-4o-mini-2024-07-18'),
 | 
			
		||||
    try {
 | 
			
		||||
        if (options?.stream) {
 | 
			
		||||
            const { textStream, text: textPromise } = await streamText({
 | 
			
		||||
                model: openai(modelName),
 | 
			
		||||
                prompt: prompt,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            for await (const textPart of textStream) {
 | 
			
		||||
                if (options.signal?.aborted) {
 | 
			
		||||
                    return '';
 | 
			
		||||
                }
 | 
			
		||||
                options.onResultStream(textPart);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const text = await textPromise;
 | 
			
		||||
 | 
			
		||||
            setInCache(cacheKey, text);
 | 
			
		||||
            return text;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const { text } = await generateText({
 | 
			
		||||
            model: openai(modelName),
 | 
			
		||||
            prompt: prompt,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        for await (const textPart of textStream) {
 | 
			
		||||
            if (options.signal?.aborted) {
 | 
			
		||||
                return '';
 | 
			
		||||
            }
 | 
			
		||||
            options.onResultStream(textPart);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const text = await textPromise;
 | 
			
		||||
 | 
			
		||||
        setInCache(cacheKey, text);
 | 
			
		||||
        return text;
 | 
			
		||||
    } catch (error: unknown) {
 | 
			
		||||
        console.error('Error generating SQL:', error);
 | 
			
		||||
        if (error instanceof Error && error.message.includes('API key')) {
 | 
			
		||||
            throw new Error(
 | 
			
		||||
                'Error: Please check your API configuration. If using a custom endpoint, make sure the endpoint URL is correct.'
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        throw new Error(
 | 
			
		||||
            'Error generating SQL script. Please check your configuration and try again.'
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { text } = await generateText({
 | 
			
		||||
        model: openai('gpt-4o-mini-2024-07-18'),
 | 
			
		||||
        prompt: prompt,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    setInCache(cacheKey, text);
 | 
			
		||||
    return text;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function getMySQLDataTypeSize(type: DataType) {
 | 
			
		||||
 
 | 
			
		||||
@@ -275,7 +275,7 @@ FROM fk_info${databaseEdition ? '_' + databaseEdition : ''}, pk_info, cols, inde
 | 
			
		||||
    if (options.databaseClient === DatabaseClient.POSTGRESQL_PSQL) {
 | 
			
		||||
        return `${psqlPreCommand}psql -h HOST_NAME -p PORT -U USER_NAME -d DATABASE_NAME -c "
 | 
			
		||||
${query.replace(/"/g, '\\"').replace(/\\\\/g, '\\\\\\').replace(/\\x/g, '\\\\x')}
 | 
			
		||||
" -t -A | pbcopy; LG='\\033[0;32m'; NC='\\033[0m'; echo "You got the resultset ($(pbpaste | wc -c | xargs) characters) in Copy/Paste. \${LG}Go back & paste in ChartDB :)\${NC}";`;
 | 
			
		||||
" -t -A > output.json;`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return query;
 | 
			
		||||
 
 | 
			
		||||
@@ -85,7 +85,7 @@ export const sqliteQuery = `WITH fk_info AS (
 | 
			
		||||
                      ELSE LOWER(p.type)
 | 
			
		||||
                  END,
 | 
			
		||||
              'ordinal_position', p.cid,
 | 
			
		||||
              'nullable', (CASE WHEN p."notnull" = 0 THEN 'true' ELSE 'false' END),
 | 
			
		||||
              'nullable', (CASE WHEN p."notnull" = 0 THEN true ELSE false END),
 | 
			
		||||
              'collation', '',
 | 
			
		||||
              'character_maximum_length',
 | 
			
		||||
                  CASE
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,26 @@
 | 
			
		||||
import { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
			
		||||
 | 
			
		||||
const sqlServerQuery = `WITH fk_info AS (
 | 
			
		||||
const sqlServerQuery = `${`/* SQL Server 2017 and above edition (14.0, 15.0, 16.0, 17.0)*/`}
 | 
			
		||||
WITH fk_info AS (
 | 
			
		||||
    SELECT
 | 
			
		||||
        JSON_QUERY(
 | 
			
		||||
            '[' + STRING_AGG(
 | 
			
		||||
            N'[' + STRING_AGG(
 | 
			
		||||
                CONVERT(nvarchar(max),
 | 
			
		||||
                JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(tp_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                            '", "table": "' + COALESCE(REPLACE(tp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                            '", "column": "' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                            '", "foreign_key_name": "' + COALESCE(REPLACE(fk.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                            '", "reference_schema": "' + COALESCE(REPLACE(tr_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                            '", "reference_table": "' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                            '", "reference_column": "' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                            '", "fk_def": "FOREIGN KEY (' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                            ') REFERENCES ' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                            '(' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                            ') ON DELETE ' + fk.delete_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                            ' ON UPDATE ' + fk.update_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS + '"}')
 | 
			
		||||
                ), ','
 | 
			
		||||
                    JSON_QUERY(N'{
 | 
			
		||||
                        "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tp_schema.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(tp.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "foreign_key_name": "' + STRING_ESCAPE(COALESCE(REPLACE(fk.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "reference_schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tr_schema.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "reference_table": "' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "reference_column": "' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "fk_def": "FOREIGN KEY (' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        ') REFERENCES ' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '(' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        ') ON DELETE ' + STRING_ESCAPE(fk.delete_referential_action_desc, 'json') +
 | 
			
		||||
                        ' ON UPDATE ' + STRING_ESCAPE(fk.update_referential_action_desc, 'json') +
 | 
			
		||||
                    '"}') COLLATE DATABASE_DEFAULT
 | 
			
		||||
                ), N','
 | 
			
		||||
            ) + N']'
 | 
			
		||||
        ) AS all_fks_json
 | 
			
		||||
    FROM sys.foreign_keys AS fk
 | 
			
		||||
@@ -31,299 +34,271 @@ const sqlServerQuery = `WITH fk_info AS (
 | 
			
		||||
), pk_info AS (
 | 
			
		||||
    SELECT
 | 
			
		||||
        JSON_QUERY(
 | 
			
		||||
            '[' + STRING_AGG(
 | 
			
		||||
                CONVERT(nvarchar(max),
 | 
			
		||||
                JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                '", "table": "' + COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                '", "column": "' + COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                '", "pk_def": "PRIMARY KEY (' + pk.COLUMN_NAME COLLATE SQL_Latin1_General_CP1_CI_AS + ')"}')
 | 
			
		||||
                ), ','
 | 
			
		||||
            ) + N']'
 | 
			
		||||
            N'[' +
 | 
			
		||||
                STRING_AGG(
 | 
			
		||||
                    CONVERT(nvarchar(max),
 | 
			
		||||
                        JSON_QUERY(N'{
 | 
			
		||||
                            "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), ''), 'json') +
 | 
			
		||||
                            '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), ''), 'json') +
 | 
			
		||||
                            '", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), ''), 'json') +
 | 
			
		||||
                            '", "pk_def": "PRIMARY KEY (' + STRING_ESCAPE(pk.COLUMN_NAME, 'json') + N')"}') COLLATE DATABASE_DEFAULT
 | 
			
		||||
                        ), N','
 | 
			
		||||
                ) + N']'
 | 
			
		||||
        ) AS all_pks_json
 | 
			
		||||
    FROM
 | 
			
		||||
        (
 | 
			
		||||
            SELECT
 | 
			
		||||
                kcu.TABLE_SCHEMA,
 | 
			
		||||
                kcu.TABLE_NAME,
 | 
			
		||||
                kcu.COLUMN_NAME
 | 
			
		||||
            FROM
 | 
			
		||||
                INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
 | 
			
		||||
            JOIN
 | 
			
		||||
                INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
 | 
			
		||||
                ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
 | 
			
		||||
                AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
 | 
			
		||||
            WHERE
 | 
			
		||||
                tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
 | 
			
		||||
        ) pk
 | 
			
		||||
    FROM (
 | 
			
		||||
        SELECT
 | 
			
		||||
            kcu.TABLE_SCHEMA,
 | 
			
		||||
            kcu.TABLE_NAME,
 | 
			
		||||
            kcu.COLUMN_NAME
 | 
			
		||||
        FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
 | 
			
		||||
        JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
 | 
			
		||||
            ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
 | 
			
		||||
            AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
 | 
			
		||||
        WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
 | 
			
		||||
    ) pk
 | 
			
		||||
),
 | 
			
		||||
cols AS (
 | 
			
		||||
    SELECT
 | 
			
		||||
        JSON_QUERY(
 | 
			
		||||
            '[' + STRING_AGG(
 | 
			
		||||
        JSON_QUERY(N'[' +
 | 
			
		||||
            STRING_AGG(
 | 
			
		||||
                CONVERT(nvarchar(max),
 | 
			
		||||
                JSON_QUERY('{"schema": "' + COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), '') +
 | 
			
		||||
                '", "table": "' + COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), '') +
 | 
			
		||||
                '", "name": "' + COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), '') +
 | 
			
		||||
                '", "ordinal_position": "' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
 | 
			
		||||
                '", "type": "' + LOWER(cols.DATA_TYPE) +
 | 
			
		||||
                '", "character_maximum_length": "' +
 | 
			
		||||
                    COALESCE(CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX)), 'null') +
 | 
			
		||||
                '", "precision": ' +
 | 
			
		||||
                    CASE
 | 
			
		||||
                        WHEN cols.DATA_TYPE IN ('numeric', 'decimal') THEN
 | 
			
		||||
                            CONCAT('{"precision":', COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null'),
 | 
			
		||||
                            ',"scale":', COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null'), '}')
 | 
			
		||||
                        ELSE
 | 
			
		||||
                            'null'
 | 
			
		||||
                    END +
 | 
			
		||||
                ', "nullable": ' +
 | 
			
		||||
                    CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
 | 
			
		||||
                ', "default": "' +
 | 
			
		||||
                    COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), '') +
 | 
			
		||||
                '", "collation": "' +
 | 
			
		||||
                    COALESCE(cols.COLLATION_NAME, '') +
 | 
			
		||||
                '"}')
 | 
			
		||||
                ), ','
 | 
			
		||||
            ) + ']'
 | 
			
		||||
        ) AS all_columns_json
 | 
			
		||||
    FROM
 | 
			
		||||
        INFORMATION_SCHEMA.COLUMNS cols
 | 
			
		||||
    WHERE
 | 
			
		||||
        cols.TABLE_CATALOG = DB_NAME()
 | 
			
		||||
                    JSON_QUERY(N'{
 | 
			
		||||
                        "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "ordinal_position": ' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
 | 
			
		||||
                        ', "type": "' + STRING_ESCAPE(LOWER(cols.DATA_TYPE), 'json') +
 | 
			
		||||
                        '", "character_maximum_length": ' +
 | 
			
		||||
                            CASE
 | 
			
		||||
                                WHEN cols.CHARACTER_MAXIMUM_LENGTH IS NULL THEN 'null'
 | 
			
		||||
                                ELSE CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX))
 | 
			
		||||
                            END +
 | 
			
		||||
                        ', "precision": ' +
 | 
			
		||||
                            CASE
 | 
			
		||||
                                WHEN cols.DATA_TYPE IN ('numeric', 'decimal')
 | 
			
		||||
                                THEN '{"precision":' + COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null') +
 | 
			
		||||
                                     ',"scale":' + COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null') + '}'
 | 
			
		||||
                                ELSE 'null'
 | 
			
		||||
                            END +
 | 
			
		||||
                        ', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
 | 
			
		||||
                        ', "default": ' +
 | 
			
		||||
                            '"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"' +
 | 
			
		||||
                        ', "collation": ' + CASE
 | 
			
		||||
                            WHEN cols.COLLATION_NAME IS NULL THEN 'null'
 | 
			
		||||
                            ELSE '"' + STRING_ESCAPE(cols.COLLATION_NAME, 'json') + '"'
 | 
			
		||||
                        END +
 | 
			
		||||
                    N'}') COLLATE DATABASE_DEFAULT
 | 
			
		||||
                ), N','
 | 
			
		||||
            ) +
 | 
			
		||||
        N']') AS all_columns_json
 | 
			
		||||
    FROM INFORMATION_SCHEMA.COLUMNS cols
 | 
			
		||||
    WHERE cols.TABLE_CATALOG = DB_NAME()
 | 
			
		||||
),
 | 
			
		||||
indexes AS (
 | 
			
		||||
    SELECT
 | 
			
		||||
        '[' + STRING_AGG(
 | 
			
		||||
            CONVERT(nvarchar(max),
 | 
			
		||||
            JSON_QUERY(
 | 
			
		||||
                N'{"schema": "' + COALESCE(REPLACE(s.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                '", "table": "' + COALESCE(REPLACE(t.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                '", "name": "' + COALESCE(REPLACE(i.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                '", "column": "' + COALESCE(REPLACE(c.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                '", "index_type": "' + LOWER(i.type_desc) COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                '", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END +
 | 
			
		||||
                ', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                '", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}'
 | 
			
		||||
            )
 | 
			
		||||
            ), ','
 | 
			
		||||
        ) + N']' AS all_indexes_json
 | 
			
		||||
    FROM
 | 
			
		||||
        sys.indexes i
 | 
			
		||||
    JOIN
 | 
			
		||||
        sys.tables t ON i.object_id = t.object_id
 | 
			
		||||
    JOIN
 | 
			
		||||
        sys.schemas s ON t.schema_id = s.schema_id
 | 
			
		||||
    JOIN
 | 
			
		||||
        sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
 | 
			
		||||
    JOIN
 | 
			
		||||
        sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
 | 
			
		||||
    WHERE
 | 
			
		||||
        s.name LIKE '%'
 | 
			
		||||
        AND i.name IS NOT NULL
 | 
			
		||||
        N'[' +
 | 
			
		||||
            STRING_AGG(
 | 
			
		||||
                CONVERT(nvarchar(max),
 | 
			
		||||
                    JSON_QUERY(N'{
 | 
			
		||||
                        "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(t.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(i.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(c.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "index_type": "' + STRING_ESCAPE(LOWER(i.type_desc), 'json') +
 | 
			
		||||
                        '", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END +
 | 
			
		||||
                        ', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END +
 | 
			
		||||
                        '", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}'
 | 
			
		||||
                    ) COLLATE DATABASE_DEFAULT
 | 
			
		||||
                ), N','
 | 
			
		||||
            ) +
 | 
			
		||||
        N']' AS all_indexes_json
 | 
			
		||||
    FROM sys.indexes i
 | 
			
		||||
    JOIN sys.tables t ON i.object_id = t.object_id
 | 
			
		||||
    JOIN sys.schemas s ON t.schema_id = s.schema_id
 | 
			
		||||
    JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
 | 
			
		||||
    JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
 | 
			
		||||
    WHERE s.name LIKE '%' AND i.name IS NOT NULL AND ic.is_included_column = 0
 | 
			
		||||
),
 | 
			
		||||
tbls AS (
 | 
			
		||||
    SELECT
 | 
			
		||||
        '[' + STRING_AGG(
 | 
			
		||||
            CONVERT(nvarchar(max),
 | 
			
		||||
            JSON_QUERY(
 | 
			
		||||
                N'{"schema": "' + COALESCE(REPLACE(aggregated.schema_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                '", "table": "' + COALESCE(REPLACE(aggregated.table_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                '", "row_count": "' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
 | 
			
		||||
                '", "table_type": "' + aggregated.table_type COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                '", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + '"}'
 | 
			
		||||
            )
 | 
			
		||||
            ), ','
 | 
			
		||||
        ) + N']' AS all_tables_json
 | 
			
		||||
    FROM
 | 
			
		||||
        (
 | 
			
		||||
            -- Select from tables
 | 
			
		||||
            SELECT
 | 
			
		||||
                COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name,
 | 
			
		||||
                COALESCE(REPLACE(t.name, '"', ''), '') AS table_name,
 | 
			
		||||
                SUM(p.rows) AS row_count,
 | 
			
		||||
                t.type_desc AS table_type,
 | 
			
		||||
                t.create_date AS creation_date
 | 
			
		||||
            FROM
 | 
			
		||||
                sys.tables t
 | 
			
		||||
            JOIN
 | 
			
		||||
                sys.schemas s ON t.schema_id = s.schema_id
 | 
			
		||||
            JOIN
 | 
			
		||||
                sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
 | 
			
		||||
            WHERE
 | 
			
		||||
                s.name LIKE '%'
 | 
			
		||||
            GROUP BY
 | 
			
		||||
                s.name, t.name, t.type_desc, t.create_date
 | 
			
		||||
        N'[' + STRING_AGG(
 | 
			
		||||
                CONVERT(nvarchar(max),
 | 
			
		||||
                        JSON_QUERY(N'{
 | 
			
		||||
                            "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.schema_name, '"', ''), ''), 'json') +
 | 
			
		||||
                            '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.table_name, '"', ''), ''), 'json') +
 | 
			
		||||
                            '", "row_count": ' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
 | 
			
		||||
                            ', "table_type": "' + STRING_ESCAPE(aggregated.table_type, 'json') +
 | 
			
		||||
                            '", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + N'"}'
 | 
			
		||||
                        ) COLLATE DATABASE_DEFAULT
 | 
			
		||||
                    ), N','
 | 
			
		||||
                ) +
 | 
			
		||||
        N']' AS all_tables_json
 | 
			
		||||
    FROM (
 | 
			
		||||
        SELECT
 | 
			
		||||
            COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name,
 | 
			
		||||
            COALESCE(REPLACE(t.name, '"', ''), '') AS table_name,
 | 
			
		||||
            SUM(p.rows) AS row_count,
 | 
			
		||||
            t.type_desc AS table_type,
 | 
			
		||||
            t.create_date AS creation_date
 | 
			
		||||
        FROM sys.tables t
 | 
			
		||||
        JOIN sys.schemas s ON t.schema_id = s.schema_id
 | 
			
		||||
        JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
 | 
			
		||||
        WHERE s.name LIKE '%'
 | 
			
		||||
        GROUP BY s.name, t.name, t.type_desc, t.create_date
 | 
			
		||||
 | 
			
		||||
            UNION ALL
 | 
			
		||||
        UNION ALL
 | 
			
		||||
 | 
			
		||||
            -- Select from views
 | 
			
		||||
            SELECT
 | 
			
		||||
                COALESCE(REPLACE(s.name, '"', ''), '') AS table_name,
 | 
			
		||||
                COALESCE(REPLACE(v.name, '"', ''), '') AS object_name,
 | 
			
		||||
                0 AS row_count,  -- Views don't have row counts
 | 
			
		||||
                'VIEW' AS table_type,
 | 
			
		||||
                v.create_date AS creation_date
 | 
			
		||||
            FROM
 | 
			
		||||
                sys.views v
 | 
			
		||||
            JOIN
 | 
			
		||||
                sys.schemas s ON v.schema_id = s.schema_id
 | 
			
		||||
            WHERE
 | 
			
		||||
                s.name LIKE '%'
 | 
			
		||||
        ) AS aggregated
 | 
			
		||||
        SELECT
 | 
			
		||||
            COALESCE(REPLACE(s.name, '"', ''), '') AS table_name,
 | 
			
		||||
            COALESCE(REPLACE(v.name, '"', ''), '') AS object_name,
 | 
			
		||||
            0 AS row_count,
 | 
			
		||||
            'VIEW' AS table_type,
 | 
			
		||||
            v.create_date AS creation_date
 | 
			
		||||
        FROM sys.views v
 | 
			
		||||
        JOIN sys.schemas s ON v.schema_id = s.schema_id
 | 
			
		||||
        WHERE s.name LIKE '%'
 | 
			
		||||
    ) AS aggregated
 | 
			
		||||
),
 | 
			
		||||
views AS (
 | 
			
		||||
    SELECT
 | 
			
		||||
        '[' + STRING_AGG(
 | 
			
		||||
            CONVERT(nvarchar(max),
 | 
			
		||||
            JSON_QUERY(
 | 
			
		||||
                N'{"schema": "' + STRING_ESCAPE(COALESCE(s.name, ''), 'json') +
 | 
			
		||||
                '", "view_name": "' + STRING_ESCAPE(COALESCE(v.name, ''), 'json') +
 | 
			
		||||
                '", "view_definition": "' +
 | 
			
		||||
                STRING_ESCAPE(
 | 
			
		||||
                    CAST(
 | 
			
		||||
                        '' AS XML
 | 
			
		||||
                    ).value(
 | 
			
		||||
                        'xs:base64Binary(sql:column("DefinitionBinary"))',
 | 
			
		||||
                        'VARCHAR(MAX)'
 | 
			
		||||
                    ), 'json') +
 | 
			
		||||
                '"}'
 | 
			
		||||
            )
 | 
			
		||||
            ), ','
 | 
			
		||||
                CONVERT(nvarchar(max),
 | 
			
		||||
                JSON_QUERY(N'{
 | 
			
		||||
                    "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
 | 
			
		||||
                    '", "view_name": "' + STRING_ESCAPE(COALESCE(REPLACE(v.name, '"', ''), ''), 'json') +
 | 
			
		||||
                    '", "view_definition": "' +
 | 
			
		||||
                    STRING_ESCAPE(
 | 
			
		||||
                        CAST(
 | 
			
		||||
                            '' AS XML
 | 
			
		||||
                        ).value(
 | 
			
		||||
                            'xs:base64Binary(sql:column("DefinitionBinary"))',
 | 
			
		||||
                            'VARCHAR(MAX)'
 | 
			
		||||
                        ), 'json') +
 | 
			
		||||
                    N'"}') COLLATE DATABASE_DEFAULT
 | 
			
		||||
                ), N','
 | 
			
		||||
        ) + N']' AS all_views_json
 | 
			
		||||
    FROM
 | 
			
		||||
        sys.views v
 | 
			
		||||
    JOIN
 | 
			
		||||
        sys.schemas s ON v.schema_id = s.schema_id
 | 
			
		||||
    JOIN
 | 
			
		||||
        sys.sql_modules m ON v.object_id = m.object_id
 | 
			
		||||
    FROM sys.views v
 | 
			
		||||
    JOIN sys.schemas s ON v.schema_id = s.schema_id
 | 
			
		||||
    JOIN sys.sql_modules m ON v.object_id = m.object_id
 | 
			
		||||
    CROSS APPLY
 | 
			
		||||
        (SELECT CONVERT(VARBINARY(MAX), m.definition) AS DefinitionBinary) AS bin
 | 
			
		||||
    WHERE
 | 
			
		||||
        s.name LIKE '%'
 | 
			
		||||
    WHERE s.name LIKE '%'
 | 
			
		||||
)
 | 
			
		||||
SELECT JSON_QUERY(
 | 
			
		||||
        N'{"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
 | 
			
		||||
    N'{
 | 
			
		||||
        "fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
 | 
			
		||||
        ', "pk_info": ' + ISNULL((SELECT cast(all_pks_json as nvarchar(max)) FROM pk_info), N'[]') +
 | 
			
		||||
        ', "columns": ' + ISNULL((SELECT cast(all_columns_json as nvarchar(max)) FROM cols), N'[]') +
 | 
			
		||||
        ', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') +
 | 
			
		||||
        ', "tables": ' + ISNULL((SELECT cast(all_tables_json as nvarchar(max)) FROM tbls), N'[]') +
 | 
			
		||||
        ', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') +
 | 
			
		||||
        ', "database_name": "' + DB_NAME() + '"' +
 | 
			
		||||
        ', "version": ""}'
 | 
			
		||||
        ', "database_name": "' + STRING_ESCAPE(DB_NAME(), 'json') +
 | 
			
		||||
        '", "version": ""
 | 
			
		||||
    }'
 | 
			
		||||
) AS metadata_json_to_import;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
const sqlServer2016AndBelowQuery = `WITH fk_info AS (
 | 
			
		||||
    SELECT
 | 
			
		||||
        JSON_QUERY(
 | 
			
		||||
            '[' + ISNULL(
 | 
			
		||||
                STUFF((
 | 
			
		||||
                    SELECT ',' +
 | 
			
		||||
                        CONVERT(nvarchar(max),
 | 
			
		||||
                        JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(tp_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                                    '", "table": "' + COALESCE(REPLACE(tp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                                    '", "column": "' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                                    '", "foreign_key_name": "' + COALESCE(REPLACE(fk.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                                    '", "reference_schema": "' + COALESCE(REPLACE(tr_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                                    '", "reference_table": "' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                                    '", "reference_column": "' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                                    '", "fk_def": "FOREIGN KEY (' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                                    ') REFERENCES ' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                                    '(' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                                    ') ON DELETE ' + fk.delete_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                                    ' ON UPDATE ' + fk.update_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS + '"}')
 | 
			
		||||
                        )
 | 
			
		||||
                    FROM
 | 
			
		||||
                        sys.foreign_keys AS fk
 | 
			
		||||
                    JOIN
 | 
			
		||||
                        sys.foreign_key_columns AS fkc ON fk.object_id = fkc.constraint_object_id
 | 
			
		||||
                    JOIN
 | 
			
		||||
                        sys.tables AS tp ON fkc.parent_object_id = tp.object_id
 | 
			
		||||
                    JOIN
 | 
			
		||||
                        sys.schemas AS tp_schema ON tp.schema_id = tp_schema.schema_id
 | 
			
		||||
                    JOIN
 | 
			
		||||
                        sys.columns AS cp ON fkc.parent_object_id = cp.object_id AND fkc.parent_column_id = cp.column_id
 | 
			
		||||
                    JOIN
 | 
			
		||||
                        sys.tables AS tr ON fkc.referenced_object_id = tr.object_id
 | 
			
		||||
                    JOIN
 | 
			
		||||
                        sys.schemas AS tr_schema ON tr.schema_id = tr_schema.schema_id
 | 
			
		||||
                    JOIN
 | 
			
		||||
                        sys.columns AS cr ON fkc.referenced_object_id = cr.object_id AND fkc.referenced_column_id = cr.column_id
 | 
			
		||||
                    FOR XML PATH('')
 | 
			
		||||
                ), 1, 1, ''), '')
 | 
			
		||||
            + N']'
 | 
			
		||||
        ) AS all_fks_json
 | 
			
		||||
const sqlServer2016AndBelowQuery = `${`/* SQL Server 2016 and below edition (13.0, 12.0, 11.0..) */`}
 | 
			
		||||
WITH fk_info AS (
 | 
			
		||||
    SELECT  JSON_QUERY('[' +
 | 
			
		||||
        ISNULL(
 | 
			
		||||
            STUFF((
 | 
			
		||||
                SELECT ',' +
 | 
			
		||||
                    CONVERT(nvarchar(max),
 | 
			
		||||
                        JSON_QUERY(N'{
 | 
			
		||||
                            "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tp_schema.name, '"', ''), ''), 'json') +
 | 
			
		||||
                            '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(tp.name, '"', ''), ''), 'json') +
 | 
			
		||||
                            '", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
 | 
			
		||||
                            '", "foreign_key_name": "' + STRING_ESCAPE(COALESCE(REPLACE(fk.name, '"', ''), ''), 'json') +
 | 
			
		||||
                            '", "reference_schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tr_schema.name, '"', ''), ''), 'json') +
 | 
			
		||||
                            '", "reference_table": "' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
 | 
			
		||||
                            '", "reference_column": "' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
 | 
			
		||||
                            '", "fk_def": "FOREIGN KEY (' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
 | 
			
		||||
                            ') REFERENCES ' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
 | 
			
		||||
                            '(' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
 | 
			
		||||
                            ') ON DELETE ' + STRING_ESCAPE(fk.delete_referential_action_desc, 'json') +
 | 
			
		||||
                            ' ON UPDATE ' + STRING_ESCAPE(fk.update_referential_action_desc, 'json') +
 | 
			
		||||
                        '"}') COLLATE DATABASE_DEFAULT
 | 
			
		||||
                    )
 | 
			
		||||
                FROM sys.foreign_keys AS fk
 | 
			
		||||
                JOIN sys.foreign_key_columns AS fkc ON fk.object_id = fkc.constraint_object_id
 | 
			
		||||
                JOIN sys.tables AS tp ON fkc.parent_object_id = tp.object_id
 | 
			
		||||
                JOIN sys.schemas AS tp_schema ON tp.schema_id = tp_schema.schema_id
 | 
			
		||||
                JOIN sys.columns AS cp ON fkc.parent_object_id = cp.object_id AND fkc.parent_column_id = cp.column_id
 | 
			
		||||
                JOIN sys.tables AS tr ON fkc.referenced_object_id = tr.object_id
 | 
			
		||||
                JOIN sys.schemas AS tr_schema ON tr.schema_id = tr_schema.schema_id
 | 
			
		||||
                JOIN sys.columns AS cr ON fkc.referenced_object_id = cr.object_id AND fkc.referenced_column_id = cr.column_id
 | 
			
		||||
                FOR XML PATH('')
 | 
			
		||||
            ), 1, 1, ''), '')
 | 
			
		||||
    + N']') AS all_fks_json
 | 
			
		||||
),
 | 
			
		||||
pk_info AS (
 | 
			
		||||
    SELECT
 | 
			
		||||
        JSON_QUERY(
 | 
			
		||||
            '[' + ISNULL(
 | 
			
		||||
                STUFF((
 | 
			
		||||
    SELECT  JSON_QUERY('[' +
 | 
			
		||||
                ISNULL(STUFF((
 | 
			
		||||
                    SELECT ',' +
 | 
			
		||||
                        CONVERT(nvarchar(max),
 | 
			
		||||
                        JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                                    '", "table": "' + COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                                    '", "column": "' + COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                                    '", "pk_def": "PRIMARY KEY (' + pk.COLUMN_NAME COLLATE SQL_Latin1_General_CP1_CI_AS + ')"}')
 | 
			
		||||
                        JSON_QUERY(N'{
 | 
			
		||||
                            "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), ''), 'json') +
 | 
			
		||||
                            '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), ''), 'json') +
 | 
			
		||||
                            '", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), ''), 'json') +
 | 
			
		||||
                            '", "pk_def": "PRIMARY KEY (' + STRING_ESCAPE(pk.COLUMN_NAME, 'json') + N')"}') COLLATE DATABASE_DEFAULT
 | 
			
		||||
                        )
 | 
			
		||||
                    FROM
 | 
			
		||||
                        (
 | 
			
		||||
                            SELECT
 | 
			
		||||
                                kcu.TABLE_SCHEMA,
 | 
			
		||||
                                kcu.TABLE_NAME,
 | 
			
		||||
                                kcu.COLUMN_NAME
 | 
			
		||||
                            FROM
 | 
			
		||||
                                INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
 | 
			
		||||
                            JOIN
 | 
			
		||||
                                INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
 | 
			
		||||
                            SELECT  kcu.TABLE_SCHEMA,
 | 
			
		||||
                                    kcu.TABLE_NAME,
 | 
			
		||||
                                    kcu.COLUMN_NAME
 | 
			
		||||
                            FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
 | 
			
		||||
                            JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
 | 
			
		||||
                                ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
 | 
			
		||||
                                AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
 | 
			
		||||
                            WHERE
 | 
			
		||||
                                tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
 | 
			
		||||
                            WHERE   tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
 | 
			
		||||
                        ) pk
 | 
			
		||||
                    FOR XML PATH('')
 | 
			
		||||
                ), 1, 1, ''), '')
 | 
			
		||||
            + N']'
 | 
			
		||||
        ) AS all_pks_json
 | 
			
		||||
    + N']') AS all_pks_json
 | 
			
		||||
),
 | 
			
		||||
cols AS (
 | 
			
		||||
    SELECT
 | 
			
		||||
        JSON_QUERY(
 | 
			
		||||
            '[' + ISNULL(
 | 
			
		||||
                STUFF((
 | 
			
		||||
                    SELECT ',' +
 | 
			
		||||
                        CONVERT(nvarchar(max),
 | 
			
		||||
                        JSON_QUERY('{"schema": "' + COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), '') +
 | 
			
		||||
                                    '", "table": "' + COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), '') +
 | 
			
		||||
                                    '", "name": "' + COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), '') +
 | 
			
		||||
                                    '", "ordinal_position": "' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
 | 
			
		||||
                                    '", "type": "' + LOWER(cols.DATA_TYPE) +
 | 
			
		||||
                                    '", "character_maximum_length": "' +
 | 
			
		||||
                                        COALESCE(CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX)), 'null') +
 | 
			
		||||
                                    '", "precision": ' +
 | 
			
		||||
                                        CASE
 | 
			
		||||
                                            WHEN cols.DATA_TYPE IN ('numeric', 'decimal') THEN
 | 
			
		||||
                                                CONCAT('{"precision":', COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null'),
 | 
			
		||||
                                                ',"scale":', COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null'), '}')
 | 
			
		||||
                                            ELSE
 | 
			
		||||
                                                'null'
 | 
			
		||||
                                        END +
 | 
			
		||||
                                    ', "nullable": ' +
 | 
			
		||||
                                        CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
 | 
			
		||||
                                    ', "default": "' +
 | 
			
		||||
                                        COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '"'), '') +
 | 
			
		||||
                                    '", "collation": "' +
 | 
			
		||||
                                        COALESCE(cols.COLLATION_NAME, '') +
 | 
			
		||||
                                    '"}')
 | 
			
		||||
                        )
 | 
			
		||||
                    FROM
 | 
			
		||||
                        INFORMATION_SCHEMA.COLUMNS cols
 | 
			
		||||
                    WHERE
 | 
			
		||||
                        cols.TABLE_CATALOG = DB_NAME()
 | 
			
		||||
                    FOR XML PATH('')
 | 
			
		||||
                ), 1, 1, ''), '')
 | 
			
		||||
            + ']'
 | 
			
		||||
        ) AS all_columns_json
 | 
			
		||||
    SELECT  JSON_QUERY('[' +
 | 
			
		||||
        ISNULL(
 | 
			
		||||
            STUFF((
 | 
			
		||||
                SELECT ',' +
 | 
			
		||||
                    CONVERT(nvarchar(max),
 | 
			
		||||
                    JSON_QUERY('{
 | 
			
		||||
                                "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), ''), 'json') +
 | 
			
		||||
                                '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), ''), 'json') +
 | 
			
		||||
                                '", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), ''), 'json') +
 | 
			
		||||
                                '", "ordinal_position": ' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
 | 
			
		||||
                                ', "type": "' + STRING_ESCAPE(LOWER(cols.DATA_TYPE), 'json') +
 | 
			
		||||
                                '", "character_maximum_length": ' +
 | 
			
		||||
                                    CASE
 | 
			
		||||
                                        WHEN cols.CHARACTER_MAXIMUM_LENGTH IS NULL THEN 'null'
 | 
			
		||||
                                        ELSE CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX))
 | 
			
		||||
                                    END +
 | 
			
		||||
                                ', "precision": ' +
 | 
			
		||||
                                    CASE
 | 
			
		||||
                                        WHEN cols.DATA_TYPE IN ('numeric', 'decimal')
 | 
			
		||||
                                        THEN '{"precision":' + COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null') +
 | 
			
		||||
                                             ',"scale":' + COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null') + '}'
 | 
			
		||||
                                        ELSE 'null'
 | 
			
		||||
                                    END +
 | 
			
		||||
                                ', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
 | 
			
		||||
                                ', "default": ' +
 | 
			
		||||
                                    '"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"' +
 | 
			
		||||
                                ', "collation": ' +
 | 
			
		||||
                                    CASE
 | 
			
		||||
                                        WHEN cols.COLLATION_NAME IS NULL THEN 'null'
 | 
			
		||||
                                        ELSE '"' + STRING_ESCAPE(cols.COLLATION_NAME, 'json') + '"'
 | 
			
		||||
                                    END +
 | 
			
		||||
                                N'}')
 | 
			
		||||
                    )
 | 
			
		||||
                FROM
 | 
			
		||||
                    INFORMATION_SCHEMA.COLUMNS cols
 | 
			
		||||
                WHERE
 | 
			
		||||
                    cols.TABLE_CATALOG = DB_NAME()
 | 
			
		||||
                FOR XML PATH('')
 | 
			
		||||
            ), 1, 1, ''), '')
 | 
			
		||||
    + ']') AS all_columns_json
 | 
			
		||||
),
 | 
			
		||||
indexes AS (
 | 
			
		||||
    SELECT
 | 
			
		||||
@@ -331,30 +306,25 @@ indexes AS (
 | 
			
		||||
            STUFF((
 | 
			
		||||
                SELECT ',' +
 | 
			
		||||
                    CONVERT(nvarchar(max),
 | 
			
		||||
                    JSON_QUERY(
 | 
			
		||||
                        N'{"schema": "' + COALESCE(REPLACE(s.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                        '", "table": "' + COALESCE(REPLACE(t.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                        '", "name": "' + COALESCE(REPLACE(i.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                        '", "column": "' + COALESCE(REPLACE(c.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                        '", "index_type": "' + LOWER(i.type_desc) COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                    JSON_QUERY(N'{
 | 
			
		||||
                        "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(t.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(i.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(c.name, '"', ''), ''), 'json') +
 | 
			
		||||
                        '", "index_type": "' + STRING_ESCAPE(LOWER(i.type_desc), 'json') +
 | 
			
		||||
                        '", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END +
 | 
			
		||||
                        ', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                        ', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END +
 | 
			
		||||
                        '", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}'
 | 
			
		||||
                    )
 | 
			
		||||
                    ) COLLATE DATABASE_DEFAULT
 | 
			
		||||
                )
 | 
			
		||||
                FROM
 | 
			
		||||
                    sys.indexes i
 | 
			
		||||
                JOIN
 | 
			
		||||
                    sys.tables t ON i.object_id = t.object_id
 | 
			
		||||
                JOIN
 | 
			
		||||
                    sys.schemas s ON t.schema_id = s.schema_id
 | 
			
		||||
                JOIN
 | 
			
		||||
                    sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
 | 
			
		||||
                JOIN
 | 
			
		||||
                    sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
 | 
			
		||||
                WHERE
 | 
			
		||||
                    s.name LIKE '%'
 | 
			
		||||
                    AND i.name IS NOT NULL
 | 
			
		||||
                FROM sys.indexes i
 | 
			
		||||
                JOIN sys.tables t ON i.object_id = t.object_id
 | 
			
		||||
                JOIN sys.schemas s ON t.schema_id = s.schema_id
 | 
			
		||||
                JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
 | 
			
		||||
                JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
 | 
			
		||||
                WHERE s.name LIKE '%'
 | 
			
		||||
                        AND i.name IS NOT NULL
 | 
			
		||||
                        AND ic.is_included_column = 0
 | 
			
		||||
                FOR XML PATH('')
 | 
			
		||||
            ), 1, 1, ''), '')
 | 
			
		||||
        + N']' AS all_indexes_json
 | 
			
		||||
@@ -365,12 +335,12 @@ tbls AS (
 | 
			
		||||
        STUFF((
 | 
			
		||||
            SELECT ',' +
 | 
			
		||||
                CONVERT(nvarchar(max),
 | 
			
		||||
                JSON_QUERY(
 | 
			
		||||
                    N'{"schema": "' + COALESCE(REPLACE(aggregated.schema_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                    '", "table": "' + COALESCE(REPLACE(aggregated.object_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                    '", "row_count": "' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
 | 
			
		||||
                    '", "object_type": "' + aggregated.object_type COLLATE SQL_Latin1_General_CP1_CI_AS +
 | 
			
		||||
                    '", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + '"}'
 | 
			
		||||
                JSON_QUERY(N'{
 | 
			
		||||
                    "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.schema_name, '"', ''), ''), 'json') +
 | 
			
		||||
                    '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.table_name, '"', ''), ''), 'json') +
 | 
			
		||||
                    '", "row_count": ' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
 | 
			
		||||
                    ', "table_type": "' + STRING_ESCAPE(aggregated.table_type, 'json') +
 | 
			
		||||
                    '", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + N'"}'
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            FROM
 | 
			
		||||
@@ -378,20 +348,15 @@ tbls AS (
 | 
			
		||||
                    -- Select from tables
 | 
			
		||||
                    SELECT
 | 
			
		||||
                        COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name,
 | 
			
		||||
                        COALESCE(REPLACE(t.name, '"', ''), '') AS object_name,
 | 
			
		||||
                        COALESCE(REPLACE(t.name, '"', ''), '') AS table_name,
 | 
			
		||||
                        SUM(p.rows) AS row_count,
 | 
			
		||||
                        t.type_desc AS object_type,
 | 
			
		||||
                        t.type_desc AS table_type,
 | 
			
		||||
                        t.create_date AS creation_date
 | 
			
		||||
                    FROM
 | 
			
		||||
                        sys.tables t
 | 
			
		||||
                    JOIN
 | 
			
		||||
                        sys.schemas s ON t.schema_id = s.schema_id
 | 
			
		||||
                    JOIN
 | 
			
		||||
                        sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
 | 
			
		||||
                    WHERE
 | 
			
		||||
                        s.name LIKE '%'
 | 
			
		||||
                    GROUP BY
 | 
			
		||||
                        s.name, t.name, t.type_desc, t.create_date
 | 
			
		||||
                    FROM sys.tables t
 | 
			
		||||
                    JOIN sys.schemas s ON t.schema_id = s.schema_id
 | 
			
		||||
                    JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
 | 
			
		||||
                    WHERE s.name LIKE '%'
 | 
			
		||||
                    GROUP BY s.name, t.name, t.type_desc, t.create_date
 | 
			
		||||
 | 
			
		||||
                    UNION ALL
 | 
			
		||||
 | 
			
		||||
@@ -402,12 +367,9 @@ tbls AS (
 | 
			
		||||
                        0 AS row_count,  -- Views don't have row counts
 | 
			
		||||
                        'VIEW' AS object_type,
 | 
			
		||||
                        v.create_date AS creation_date
 | 
			
		||||
                    FROM
 | 
			
		||||
                        sys.views v
 | 
			
		||||
                    JOIN
 | 
			
		||||
                        sys.schemas s ON v.schema_id = s.schema_id
 | 
			
		||||
                    WHERE
 | 
			
		||||
                        s.name LIKE '%'
 | 
			
		||||
                    FROM sys.views v
 | 
			
		||||
                    JOIN sys.schemas s ON v.schema_id = s.schema_id
 | 
			
		||||
                    WHERE s.name LIKE '%'
 | 
			
		||||
                ) AS aggregated
 | 
			
		||||
            FOR XML PATH('')
 | 
			
		||||
        ), 1, 1, ''), '')
 | 
			
		||||
@@ -417,38 +379,40 @@ views AS (
 | 
			
		||||
    SELECT
 | 
			
		||||
        '[' +
 | 
			
		||||
        (
 | 
			
		||||
            SELECT
 | 
			
		||||
                STUFF((
 | 
			
		||||
                    SELECT ',' + CONVERT(nvarchar(max),
 | 
			
		||||
                        JSON_QUERY(
 | 
			
		||||
                            N'{"schema": "' + COALESCE(REPLACE(s.name, '"', ''), '') +
 | 
			
		||||
                            '", "view_name": "' + COALESCE(REPLACE(v.name, '"', ''), '') +
 | 
			
		||||
                            '", "view_definition": "' +
 | 
			
		||||
                            CAST(
 | 
			
		||||
                                (
 | 
			
		||||
                                    SELECT CAST(OBJECT_DEFINITION(v.object_id) AS VARBINARY(MAX)) FOR XML PATH('')
 | 
			
		||||
                                ) AS NVARCHAR(MAX)
 | 
			
		||||
                            ) + '"}'
 | 
			
		||||
            SELECT  STUFF((
 | 
			
		||||
                        SELECT ',' + CONVERT(nvarchar(max),
 | 
			
		||||
                            JSON_QUERY(
 | 
			
		||||
                                N'{
 | 
			
		||||
                                "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
 | 
			
		||||
                                '", "view_name": "' + STRING_ESCAPE(COALESCE(REPLACE(v.name, '"', ''), ''), 'json') +
 | 
			
		||||
                                '", "view_definition": "' +
 | 
			
		||||
                                CAST(
 | 
			
		||||
                                    (
 | 
			
		||||
                                        SELECT CAST(OBJECT_DEFINITION(v.object_id) AS VARBINARY(MAX)) FOR XML PATH('')
 | 
			
		||||
                                    ) AS NVARCHAR(MAX)
 | 
			
		||||
                                ) + N'"}'
 | 
			
		||||
                            )
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                    FROM
 | 
			
		||||
                        sys.views v
 | 
			
		||||
                    JOIN
 | 
			
		||||
                        sys.schemas s ON v.schema_id = s.schema_id
 | 
			
		||||
                    WHERE
 | 
			
		||||
                        s.name LIKE '%'
 | 
			
		||||
                    FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
 | 
			
		||||
                        FROM
 | 
			
		||||
                            sys.views v
 | 
			
		||||
                        JOIN
 | 
			
		||||
                            sys.schemas s ON v.schema_id = s.schema_id
 | 
			
		||||
                        WHERE
 | 
			
		||||
                            s.name LIKE '%'
 | 
			
		||||
                        FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
 | 
			
		||||
        ) + ']' AS all_views_json
 | 
			
		||||
)
 | 
			
		||||
SELECT JSON_QUERY(
 | 
			
		||||
        N'{"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
 | 
			
		||||
    N'{
 | 
			
		||||
        "fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
 | 
			
		||||
        ', "pk_info": ' + ISNULL((SELECT cast(all_pks_json as nvarchar(max)) FROM pk_info), N'[]') +
 | 
			
		||||
        ', "columns": ' + ISNULL((SELECT cast(all_columns_json as nvarchar(max)) FROM cols), N'[]') +
 | 
			
		||||
        ', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') +
 | 
			
		||||
        ', "tables": ' + ISNULL((SELECT cast(all_objects_json as nvarchar(max)) FROM tbls), N'[]') +
 | 
			
		||||
        ', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') +
 | 
			
		||||
        ', "database_name": "' + DB_NAME() + '"' +
 | 
			
		||||
        ', "version": ""}'
 | 
			
		||||
        ', "version": ""
 | 
			
		||||
    }'
 | 
			
		||||
) AS metadata_json_to_import;`;
 | 
			
		||||
 | 
			
		||||
export const getSqlServerQuery = (
 | 
			
		||||
 
 | 
			
		||||
@@ -10,14 +10,20 @@ export const fixMetadataJson = async (
 | 
			
		||||
    return (
 | 
			
		||||
        metadataJson
 | 
			
		||||
            .trim()
 | 
			
		||||
            // First unescape the JSON string
 | 
			
		||||
            .replace(/\\"/g, '"')
 | 
			
		||||
            .replace(/\\\\/g, '\\')
 | 
			
		||||
            .replace(/^[^{]*/, '') // Remove everything before the first '{'
 | 
			
		||||
            .replace(/}[^}]*$/, '}') // Remove everything after the last '}'
 | 
			
		||||
            .replace(/:""([^"]+)""/g, ':"$1"') // Convert :""value"" to :"value"
 | 
			
		||||
            .replace(/""(\w+)""/g, '"$1"') // Convert ""key"" to "key"
 | 
			
		||||
            .replace(/^\s+|\s+$/g, '')
 | 
			
		||||
            .replace(/^"|"$/g, '')
 | 
			
		||||
            .replace(/^'|'$/g, '')
 | 
			
		||||
            .replace(/""""/g, '""') // Remove Quadruple quotes from keys
 | 
			
		||||
            .replace(/"""([^",}]+)"""/g, '"$1"') // Remove tripple quotes from keys
 | 
			
		||||
            .replace(/""([^",}]+)""/g, '"$1"') // Remove double quotes from keys
 | 
			
		||||
 | 
			
		||||
            /* eslint-disable-next-line no-useless-escape */
 | 
			
		||||
            .replace(/\"/g, '___ESCAPED_QUOTE___') // Temporarily replace empty strings
 | 
			
		||||
            .replace(/(?<=:\s*)""(?=\s*[,}])/g, '___EMPTY___') // Temporarily replace empty strings
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										304
									
								
								src/lib/dbml-import.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										304
									
								
								src/lib/dbml-import.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,304 @@
 | 
			
		||||
import { Parser } from '@dbml/core';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import { generateDiagramId, generateId } from '@/lib/utils';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import type { Cardinality, DBRelationship } from '@/lib/domain/db-relationship';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DataType } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import { genericDataTypes } from '@/lib/data/data-types/generic-data-types';
 | 
			
		||||
import { randomColor } from '@/lib/colors';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
 | 
			
		||||
interface DBMLTypeArgs {
 | 
			
		||||
    length?: number;
 | 
			
		||||
    precision?: number;
 | 
			
		||||
    scale?: number;
 | 
			
		||||
    values?: string[]; // For enum types
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DBMLField {
 | 
			
		||||
    name: string;
 | 
			
		||||
    type: {
 | 
			
		||||
        type_name: string;
 | 
			
		||||
        args?: DBMLTypeArgs;
 | 
			
		||||
    };
 | 
			
		||||
    unique?: boolean;
 | 
			
		||||
    pk?: boolean;
 | 
			
		||||
    not_null?: boolean;
 | 
			
		||||
    increment?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DBMLIndexColumn {
 | 
			
		||||
    value: string;
 | 
			
		||||
    type?: string;
 | 
			
		||||
    length?: number;
 | 
			
		||||
    order?: 'asc' | 'desc';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DBMLIndex {
 | 
			
		||||
    columns: string | (string | DBMLIndexColumn)[];
 | 
			
		||||
    unique?: boolean;
 | 
			
		||||
    name?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DBMLTable {
 | 
			
		||||
    name: string;
 | 
			
		||||
    schema?: string | { name: string };
 | 
			
		||||
    fields: DBMLField[];
 | 
			
		||||
    indexes?: DBMLIndex[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DBMLEndpoint {
 | 
			
		||||
    tableName: string;
 | 
			
		||||
    fieldNames: string[];
 | 
			
		||||
    relation: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DBMLRef {
 | 
			
		||||
    endpoints: [DBMLEndpoint, DBMLEndpoint];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const mapDBMLTypeToGenericType = (dbmlType: string): DataType => {
 | 
			
		||||
    const normalizedType = dbmlType.toLowerCase().replace(/\(.*\)/, '');
 | 
			
		||||
    const matchedType = genericDataTypes.find((t) => t.id === normalizedType);
 | 
			
		||||
    if (matchedType) return matchedType;
 | 
			
		||||
    const typeMap: Record<string, string> = {
 | 
			
		||||
        int: 'integer',
 | 
			
		||||
        varchar: 'varchar',
 | 
			
		||||
        bool: 'boolean',
 | 
			
		||||
        number: 'numeric',
 | 
			
		||||
        string: 'varchar',
 | 
			
		||||
        text: 'text',
 | 
			
		||||
        timestamp: 'timestamp',
 | 
			
		||||
        datetime: 'timestamp',
 | 
			
		||||
        float: 'float',
 | 
			
		||||
        double: 'double',
 | 
			
		||||
        decimal: 'decimal',
 | 
			
		||||
        bigint: 'bigint',
 | 
			
		||||
        smallint: 'smallint',
 | 
			
		||||
        char: 'char',
 | 
			
		||||
    };
 | 
			
		||||
    const mappedType = typeMap[normalizedType];
 | 
			
		||||
    if (mappedType) {
 | 
			
		||||
        const foundType = genericDataTypes.find((t) => t.id === mappedType);
 | 
			
		||||
        if (foundType) return foundType;
 | 
			
		||||
    }
 | 
			
		||||
    return genericDataTypes.find((t) => t.id === 'varchar')!;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const determineCardinality = (
 | 
			
		||||
    field: DBField,
 | 
			
		||||
    referencedField: DBField
 | 
			
		||||
): { sourceCardinality: string; targetCardinality: string } => {
 | 
			
		||||
    const isSourceUnique = field.unique || field.primaryKey;
 | 
			
		||||
    const isTargetUnique = referencedField.unique || referencedField.primaryKey;
 | 
			
		||||
    if (isSourceUnique && isTargetUnique) {
 | 
			
		||||
        return { sourceCardinality: 'one', targetCardinality: 'one' };
 | 
			
		||||
    } else if (isSourceUnique) {
 | 
			
		||||
        return { sourceCardinality: 'one', targetCardinality: 'many' };
 | 
			
		||||
    } else if (isTargetUnique) {
 | 
			
		||||
        return { sourceCardinality: 'many', targetCardinality: 'one' };
 | 
			
		||||
    } else {
 | 
			
		||||
        return { sourceCardinality: 'many', targetCardinality: 'many' };
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const importDBMLToDiagram = async (
 | 
			
		||||
    dbmlContent: string
 | 
			
		||||
): Promise<Diagram> => {
 | 
			
		||||
    try {
 | 
			
		||||
        const parser = new Parser();
 | 
			
		||||
        const parsedData = parser.parse(dbmlContent, 'dbml');
 | 
			
		||||
        const dbmlData = parsedData.schemas[0];
 | 
			
		||||
 | 
			
		||||
        // Extract only the necessary data from the parsed DBML
 | 
			
		||||
        const extractedData = {
 | 
			
		||||
            tables: (dbmlData.tables as unknown as DBMLTable[]).map(
 | 
			
		||||
                (table) => ({
 | 
			
		||||
                    name: table.name,
 | 
			
		||||
                    schema: table.schema,
 | 
			
		||||
                    fields: table.fields.map((field: DBMLField) => ({
 | 
			
		||||
                        name: field.name,
 | 
			
		||||
                        type: field.type,
 | 
			
		||||
                        unique: field.unique,
 | 
			
		||||
                        pk: field.pk,
 | 
			
		||||
                        not_null: field.not_null,
 | 
			
		||||
                        increment: field.increment,
 | 
			
		||||
                    })),
 | 
			
		||||
                    indexes:
 | 
			
		||||
                        table.indexes?.map((dbmlIndex) => {
 | 
			
		||||
                            let indexColumns: string[];
 | 
			
		||||
 | 
			
		||||
                            // Handle composite index case "(col1, col2)"
 | 
			
		||||
                            if (typeof dbmlIndex.columns === 'string') {
 | 
			
		||||
                                if (dbmlIndex.columns.includes('(')) {
 | 
			
		||||
                                    // Composite index
 | 
			
		||||
                                    const columnsStr =
 | 
			
		||||
                                        dbmlIndex.columns.replace(/[()]/g, '');
 | 
			
		||||
                                    indexColumns = columnsStr
 | 
			
		||||
                                        .split(',')
 | 
			
		||||
                                        .map((c) => c.trim());
 | 
			
		||||
                                } else {
 | 
			
		||||
                                    // Single column
 | 
			
		||||
                                    indexColumns = [dbmlIndex.columns.trim()];
 | 
			
		||||
                                }
 | 
			
		||||
                            } else {
 | 
			
		||||
                                // Handle array of columns
 | 
			
		||||
                                indexColumns = Array.isArray(dbmlIndex.columns)
 | 
			
		||||
                                    ? dbmlIndex.columns.map((col) =>
 | 
			
		||||
                                          typeof col === 'object' &&
 | 
			
		||||
                                          'value' in col
 | 
			
		||||
                                              ? (col.value as string).trim()
 | 
			
		||||
                                              : (col as string).trim()
 | 
			
		||||
                                      )
 | 
			
		||||
                                    : [String(dbmlIndex.columns).trim()];
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            // Generate a consistent index name
 | 
			
		||||
                            const indexName =
 | 
			
		||||
                                dbmlIndex.name ||
 | 
			
		||||
                                `idx_${table.name}_${indexColumns.join('_')}`;
 | 
			
		||||
 | 
			
		||||
                            return {
 | 
			
		||||
                                columns: indexColumns,
 | 
			
		||||
                                unique: dbmlIndex.unique || false,
 | 
			
		||||
                                name: indexName,
 | 
			
		||||
                            };
 | 
			
		||||
                        }) || [],
 | 
			
		||||
                })
 | 
			
		||||
            ),
 | 
			
		||||
            refs: (dbmlData.refs as unknown as DBMLRef[]).map((ref) => ({
 | 
			
		||||
                endpoints: (ref.endpoints as [DBMLEndpoint, DBMLEndpoint]).map(
 | 
			
		||||
                    (endpoint) => ({
 | 
			
		||||
                        tableName: endpoint.tableName,
 | 
			
		||||
                        fieldNames: endpoint.fieldNames,
 | 
			
		||||
                        relation: endpoint.relation,
 | 
			
		||||
                    })
 | 
			
		||||
                ),
 | 
			
		||||
            })),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Convert DBML tables to ChartDB table objects
 | 
			
		||||
        const tables: DBTable[] = extractedData.tables.map((table, index) => {
 | 
			
		||||
            const row = Math.floor(index / 4);
 | 
			
		||||
            const col = index % 4;
 | 
			
		||||
            const tableSpacing = 300;
 | 
			
		||||
 | 
			
		||||
            // Create fields first so we have their IDs
 | 
			
		||||
            const fields = table.fields.map((field) => ({
 | 
			
		||||
                id: generateId(),
 | 
			
		||||
                name: field.name.replace(/['"]/g, ''),
 | 
			
		||||
                type: mapDBMLTypeToGenericType(field.type.type_name),
 | 
			
		||||
                nullable: !field.not_null,
 | 
			
		||||
                primaryKey: field.pk || false,
 | 
			
		||||
                unique: field.unique || false,
 | 
			
		||||
                createdAt: Date.now(),
 | 
			
		||||
            }));
 | 
			
		||||
 | 
			
		||||
            // Convert DBML indexes to ChartDB indexes
 | 
			
		||||
            const indexes =
 | 
			
		||||
                table.indexes?.map((dbmlIndex) => {
 | 
			
		||||
                    const fieldIds = dbmlIndex.columns.map((columnName) => {
 | 
			
		||||
                        const field = fields.find((f) => f.name === columnName);
 | 
			
		||||
                        if (!field) {
 | 
			
		||||
                            throw new Error(
 | 
			
		||||
                                `Index references non-existent column: ${columnName}`
 | 
			
		||||
                            );
 | 
			
		||||
                        }
 | 
			
		||||
                        return field.id;
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    return {
 | 
			
		||||
                        id: generateId(),
 | 
			
		||||
                        name:
 | 
			
		||||
                            dbmlIndex.name ||
 | 
			
		||||
                            `idx_${table.name}_${dbmlIndex.columns.join('_')}`,
 | 
			
		||||
                        fieldIds,
 | 
			
		||||
                        unique: dbmlIndex.unique || false,
 | 
			
		||||
                        createdAt: Date.now(),
 | 
			
		||||
                    };
 | 
			
		||||
                }) || [];
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                id: generateId(),
 | 
			
		||||
                name: table.name.replace(/['"]/g, ''),
 | 
			
		||||
                schema:
 | 
			
		||||
                    typeof table.schema === 'string'
 | 
			
		||||
                        ? table.schema
 | 
			
		||||
                        : table.schema?.name || '',
 | 
			
		||||
                order: index,
 | 
			
		||||
                fields,
 | 
			
		||||
                indexes,
 | 
			
		||||
                x: col * tableSpacing,
 | 
			
		||||
                y: row * tableSpacing,
 | 
			
		||||
                color: randomColor(),
 | 
			
		||||
                isView: false,
 | 
			
		||||
                createdAt: Date.now(),
 | 
			
		||||
            };
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Create relationships using the refs
 | 
			
		||||
        const relationships: DBRelationship[] = extractedData.refs.map(
 | 
			
		||||
            (ref) => {
 | 
			
		||||
                const [source, target] = ref.endpoints;
 | 
			
		||||
                const sourceTable = tables.find(
 | 
			
		||||
                    (t) =>
 | 
			
		||||
                        t.name === source.tableName.replace(/['"]/g, '') &&
 | 
			
		||||
                        (!source.tableName.includes('.') ||
 | 
			
		||||
                            t.schema === source.tableName.split('.')[0])
 | 
			
		||||
                );
 | 
			
		||||
                const targetTable = tables.find(
 | 
			
		||||
                    (t) =>
 | 
			
		||||
                        t.name === target.tableName.replace(/['"]/g, '') &&
 | 
			
		||||
                        (!target.tableName.includes('.') ||
 | 
			
		||||
                            t.schema === target.tableName.split('.')[0])
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                if (!sourceTable || !targetTable) {
 | 
			
		||||
                    throw new Error('Invalid relationship: tables not found');
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const sourceField = sourceTable.fields.find(
 | 
			
		||||
                    (f) => f.name === source.fieldNames[0].replace(/['"]/g, '')
 | 
			
		||||
                );
 | 
			
		||||
                const targetField = targetTable.fields.find(
 | 
			
		||||
                    (f) => f.name === target.fieldNames[0].replace(/['"]/g, '')
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                if (!sourceField || !targetField) {
 | 
			
		||||
                    throw new Error('Invalid relationship: fields not found');
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const { sourceCardinality, targetCardinality } =
 | 
			
		||||
                    determineCardinality(sourceField, targetField);
 | 
			
		||||
 | 
			
		||||
                return {
 | 
			
		||||
                    id: generateId(),
 | 
			
		||||
                    name: `${sourceTable.name}_${sourceField.name}_${targetTable.name}_${targetField.name}`,
 | 
			
		||||
                    sourceSchema: sourceTable.schema,
 | 
			
		||||
                    targetSchema: targetTable.schema,
 | 
			
		||||
                    sourceTableId: sourceTable.id,
 | 
			
		||||
                    targetTableId: targetTable.id,
 | 
			
		||||
                    sourceFieldId: sourceField.id,
 | 
			
		||||
                    targetFieldId: targetField.id,
 | 
			
		||||
                    sourceCardinality: sourceCardinality as Cardinality,
 | 
			
		||||
                    targetCardinality: targetCardinality as Cardinality,
 | 
			
		||||
                    createdAt: Date.now(),
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            id: generateDiagramId(),
 | 
			
		||||
            name: 'DBML Import',
 | 
			
		||||
            databaseType: DatabaseType.GENERIC,
 | 
			
		||||
            tables,
 | 
			
		||||
            relationships,
 | 
			
		||||
            createdAt: new Date(),
 | 
			
		||||
            updatedAt: new Date(),
 | 
			
		||||
        };
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
        console.error('DBML parsing error:', error);
 | 
			
		||||
        throw error;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
@@ -1,5 +1,11 @@
 | 
			
		||||
export const OPENAI_API_KEY: string = import.meta.env.VITE_OPENAI_API_KEY;
 | 
			
		||||
export const OPENAI_API_ENDPOINT: string = import.meta.env
 | 
			
		||||
    .VITE_OPENAI_API_ENDPOINT;
 | 
			
		||||
export const LLM_MODEL_NAME: string = import.meta.env.VITE_LLM_MODEL_NAME;
 | 
			
		||||
export const IS_CHARTDB_IO: boolean =
 | 
			
		||||
    import.meta.env.VITE_IS_CHARTDB_IO === 'true';
 | 
			
		||||
export const APP_URL: string = import.meta.env.VITE_APP_URL;
 | 
			
		||||
export const HOST_URL: string = import.meta.env.VITE_HOST_URL ?? '';
 | 
			
		||||
export const HIDE_BUCKLE_DOT_DEV: boolean =
 | 
			
		||||
    (window?.env?.HIDE_BUCKLE_DOT_DEV ??
 | 
			
		||||
        import.meta.env.VITE_HIDE_BUCKLE_DOT_DEV) === 'true';
 | 
			
		||||
 
 | 
			
		||||
@@ -45,17 +45,13 @@ import { Badge } from '@/components/badge/badge';
 | 
			
		||||
import { useTheme } from '@/hooks/use-theme';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import {
 | 
			
		||||
    adjustTablePositions,
 | 
			
		||||
    shouldShowTablesBySchemaFilter,
 | 
			
		||||
} from '@/lib/domain/db-table';
 | 
			
		||||
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
 | 
			
		||||
import { useLocalConfig } from '@/hooks/use-local-config';
 | 
			
		||||
import {
 | 
			
		||||
    Tooltip,
 | 
			
		||||
    TooltipTrigger,
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import { MarkerDefinitions } from './marker-definitions';
 | 
			
		||||
import { CanvasContextMenu } from './canvas-context-menu';
 | 
			
		||||
import { areFieldTypesCompatible } from '@/lib/data/data-types/data-types';
 | 
			
		||||
@@ -65,7 +61,7 @@ import {
 | 
			
		||||
    findTableOverlapping,
 | 
			
		||||
} from './canvas-utils';
 | 
			
		||||
import type { Graph } from '@/lib/graph';
 | 
			
		||||
import { createGraph, removeVertex } from '@/lib/graph';
 | 
			
		||||
import { removeVertex } from '@/lib/graph';
 | 
			
		||||
import type { ChartDBEvent } from '@/context/chartdb-context/chartdb-context';
 | 
			
		||||
import { cn, debounce, getOperatingSystem } from '@/lib/utils';
 | 
			
		||||
import type { DependencyEdgeType } from './dependency-edge';
 | 
			
		||||
@@ -76,6 +72,8 @@ import {
 | 
			
		||||
    TOP_SOURCE_HANDLE_ID_PREFIX,
 | 
			
		||||
} from './table-node/table-node-dependency-indicator';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import { useAlert } from '@/context/alert-context/alert-context';
 | 
			
		||||
import { useCanvas } from '@/hooks/use-canvas';
 | 
			
		||||
 | 
			
		||||
export type EdgeType = RelationshipEdgeType | DependencyEdgeType;
 | 
			
		||||
 | 
			
		||||
@@ -109,8 +107,7 @@ export interface CanvasProps {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
 | 
			
		||||
    const { getEdge, getInternalNode, fitView, getEdges, getNode } =
 | 
			
		||||
        useReactFlow();
 | 
			
		||||
    const { getEdge, getInternalNode, getEdges, getNode } = useReactFlow();
 | 
			
		||||
    const [selectedTableIds, setSelectedTableIds] = useState<string[]>([]);
 | 
			
		||||
    const [selectedRelationshipIds, setSelectedRelationshipIds] = useState<
 | 
			
		||||
        string[]
 | 
			
		||||
@@ -133,16 +130,17 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
 | 
			
		||||
    } = useChartDB();
 | 
			
		||||
    const { showSidePanel } = useLayout();
 | 
			
		||||
    const { effectiveTheme } = useTheme();
 | 
			
		||||
    const { scrollAction, showDependenciesOnCanvas } = useLocalConfig();
 | 
			
		||||
    const { showAlert } = useDialog();
 | 
			
		||||
    const { scrollAction, showDependenciesOnCanvas, showMiniMapOnCanvas } =
 | 
			
		||||
        useLocalConfig();
 | 
			
		||||
    const { showAlert } = useAlert();
 | 
			
		||||
    const { isMd: isDesktop } = useBreakpoint('md');
 | 
			
		||||
    const nodeTypes = useMemo(() => ({ table: TableNode }), []);
 | 
			
		||||
    const [highlightOverlappingTables, setHighlightOverlappingTables] =
 | 
			
		||||
        useState(false);
 | 
			
		||||
    const { reorderTables, fitView, setOverlapGraph, overlapGraph } =
 | 
			
		||||
        useCanvas();
 | 
			
		||||
 | 
			
		||||
    const [isInitialLoadingNodes, setIsInitialLoadingNodes] = useState(true);
 | 
			
		||||
    const [overlapGraph, setOverlapGraph] =
 | 
			
		||||
        useState<Graph<string>>(createGraph());
 | 
			
		||||
 | 
			
		||||
    const [nodes, setNodes, onNodesChange] = useNodesState<TableNodeType>(
 | 
			
		||||
        initialTables.map((table) => tableToTableNode(table, filteredSchemas))
 | 
			
		||||
@@ -344,7 +342,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
 | 
			
		||||
            }, 500)();
 | 
			
		||||
            prevFilteredSchemas.current = filteredSchemas;
 | 
			
		||||
        }
 | 
			
		||||
    }, [filteredSchemas, fitView, tables]);
 | 
			
		||||
    }, [filteredSchemas, fitView, tables, setOverlapGraph]);
 | 
			
		||||
 | 
			
		||||
    const onConnectHandler = useCallback(
 | 
			
		||||
        async (params: AddEdgeParams) => {
 | 
			
		||||
@@ -656,33 +654,6 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
 | 
			
		||||
    const isLoadingDOM =
 | 
			
		||||
        tables.length > 0 ? !getInternalNode(tables[0].id) : false;
 | 
			
		||||
 | 
			
		||||
    const reorderTables = useCallback(() => {
 | 
			
		||||
        const newTables = adjustTablePositions({
 | 
			
		||||
            relationships,
 | 
			
		||||
            tables: tables.filter((table) =>
 | 
			
		||||
                shouldShowTablesBySchemaFilter(table, filteredSchemas)
 | 
			
		||||
            ),
 | 
			
		||||
            mode: 'all', // Use 'all' mode for manual reordering
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        const updatedOverlapGraph = findOverlappingTables({
 | 
			
		||||
            tables: newTables,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        updateTablesState((currentTables) =>
 | 
			
		||||
            currentTables.map((table) => {
 | 
			
		||||
                const newTable = newTables.find((t) => t.id === table.id);
 | 
			
		||||
                return {
 | 
			
		||||
                    id: table.id,
 | 
			
		||||
                    x: newTable?.x ?? table.x,
 | 
			
		||||
                    y: newTable?.y ?? table.y,
 | 
			
		||||
                };
 | 
			
		||||
            })
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        setOverlapGraph(updatedOverlapGraph);
 | 
			
		||||
    }, [filteredSchemas, relationships, tables, updateTablesState]);
 | 
			
		||||
 | 
			
		||||
    const showReorderConfirmation = useCallback(() => {
 | 
			
		||||
        showAlert({
 | 
			
		||||
            title: t('reorder_diagram_alert.title'),
 | 
			
		||||
@@ -871,12 +842,14 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
 | 
			
		||||
                    >
 | 
			
		||||
                        <Toolbar readonly={readonly} />
 | 
			
		||||
                    </Controls>
 | 
			
		||||
                    <MiniMap
 | 
			
		||||
                        style={{
 | 
			
		||||
                            width: isDesktop ? 100 : 60,
 | 
			
		||||
                            height: isDesktop ? 100 : 60,
 | 
			
		||||
                        }}
 | 
			
		||||
                    />
 | 
			
		||||
                    {showMiniMapOnCanvas && (
 | 
			
		||||
                        <MiniMap
 | 
			
		||||
                            style={{
 | 
			
		||||
                                width: isDesktop ? 100 : 60,
 | 
			
		||||
                                height: isDesktop ? 100 : 60,
 | 
			
		||||
                            }}
 | 
			
		||||
                        />
 | 
			
		||||
                    )}
 | 
			
		||||
                    <Background
 | 
			
		||||
                        variant={BackgroundVariant.Dots}
 | 
			
		||||
                        gap={16}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,9 +9,10 @@ import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useLayout } from '@/hooks/use-layout';
 | 
			
		||||
import { cloneTable } from '@/lib/clone';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import { Copy, Pencil, Trash2 } from 'lucide-react';
 | 
			
		||||
import { Copy, Pencil, Trash2, Workflow } from 'lucide-react';
 | 
			
		||||
import React, { useCallback } from 'react';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
 | 
			
		||||
export interface TableNodeContextMenuProps {
 | 
			
		||||
    table: DBTable;
 | 
			
		||||
@@ -24,6 +25,7 @@ export const TableNodeContextMenu: React.FC<
 | 
			
		||||
    const { openTableFromSidebar } = useLayout();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { isMd: isDesktop } = useBreakpoint('md');
 | 
			
		||||
    const { openCreateRelationshipDialog } = useDialog();
 | 
			
		||||
 | 
			
		||||
    const duplicateTableHandler = useCallback(() => {
 | 
			
		||||
        const clonedTable = cloneTable(table);
 | 
			
		||||
@@ -43,6 +45,12 @@ export const TableNodeContextMenu: React.FC<
 | 
			
		||||
        removeTable(table.id);
 | 
			
		||||
    }, [removeTable, table.id]);
 | 
			
		||||
 | 
			
		||||
    const addRelationshipHandler = useCallback(() => {
 | 
			
		||||
        openCreateRelationshipDialog({
 | 
			
		||||
            sourceTableId: table.id,
 | 
			
		||||
        });
 | 
			
		||||
    }, [openCreateRelationshipDialog, table.id]);
 | 
			
		||||
 | 
			
		||||
    if (!isDesktop || readonly) {
 | 
			
		||||
        return <>{children}</>;
 | 
			
		||||
    }
 | 
			
		||||
@@ -64,6 +72,13 @@ export const TableNodeContextMenu: React.FC<
 | 
			
		||||
                    <span>{t('table_node_context_menu.duplicate_table')}</span>
 | 
			
		||||
                    <Copy className="size-3.5" />
 | 
			
		||||
                </ContextMenuItem>
 | 
			
		||||
                <ContextMenuItem
 | 
			
		||||
                    onClick={addRelationshipHandler}
 | 
			
		||||
                    className="flex justify-between gap-3"
 | 
			
		||||
                >
 | 
			
		||||
                    <span>{t('table_node_context_menu.add_relationship')}</span>
 | 
			
		||||
                    <Workflow className="size-3.5" />
 | 
			
		||||
                </ContextMenuItem>
 | 
			
		||||
                <ContextMenuItem
 | 
			
		||||
                    onClick={removeTableHandler}
 | 
			
		||||
                    className="flex justify-between gap-3"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,10 @@
 | 
			
		||||
import React, { useEffect, useMemo, useRef } from 'react';
 | 
			
		||||
import React, {
 | 
			
		||||
    useCallback,
 | 
			
		||||
    useEffect,
 | 
			
		||||
    useMemo,
 | 
			
		||||
    useRef,
 | 
			
		||||
    useState,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import {
 | 
			
		||||
    Handle,
 | 
			
		||||
    Position,
 | 
			
		||||
@@ -6,11 +12,17 @@ import {
 | 
			
		||||
    useUpdateNodeInternals,
 | 
			
		||||
} from '@xyflow/react';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { KeyRound, Trash2 } from 'lucide-react';
 | 
			
		||||
 | 
			
		||||
import { Check, KeyRound, MessageCircleMore, Trash2 } from 'lucide-react';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import {
 | 
			
		||||
    Tooltip,
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
    TooltipTrigger,
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import { useClickAway, useKeyPressEvent } from 'react-use';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
 | 
			
		||||
export const LEFT_HANDLE_ID_PREFIX = 'left_rel_';
 | 
			
		||||
export const RIGHT_HANDLE_ID_PREFIX = 'right_rel_';
 | 
			
		||||
@@ -27,7 +39,12 @@ export interface TableNodeFieldProps {
 | 
			
		||||
 | 
			
		||||
export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
    ({ field, focused, tableNodeId, highlighted, visible, isConnectable }) => {
 | 
			
		||||
        const { removeField, relationships, readonly } = useChartDB();
 | 
			
		||||
        const { removeField, relationships, readonly, updateField } =
 | 
			
		||||
            useChartDB();
 | 
			
		||||
        const [editMode, setEditMode] = useState(false);
 | 
			
		||||
        const [fieldName, setFieldName] = useState(field.name);
 | 
			
		||||
        const inputRef = React.useRef<HTMLInputElement>(null);
 | 
			
		||||
 | 
			
		||||
        const updateNodeInternals = useUpdateNodeInternals();
 | 
			
		||||
        const connection = useConnection();
 | 
			
		||||
        const isTarget = useMemo(
 | 
			
		||||
@@ -61,6 +78,28 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
            }
 | 
			
		||||
        }, [tableNodeId, updateNodeInternals, numberOfEdgesToField]);
 | 
			
		||||
 | 
			
		||||
        const editFieldName = useCallback(() => {
 | 
			
		||||
            if (!editMode) return;
 | 
			
		||||
            if (fieldName.trim()) {
 | 
			
		||||
                updateField(tableNodeId, field.id, { name: fieldName.trim() });
 | 
			
		||||
            }
 | 
			
		||||
            setEditMode(false);
 | 
			
		||||
        }, [fieldName, field.id, updateField, editMode, tableNodeId]);
 | 
			
		||||
 | 
			
		||||
        const abortEdit = useCallback(() => {
 | 
			
		||||
            setEditMode(false);
 | 
			
		||||
            setFieldName(field.name);
 | 
			
		||||
        }, [field.name]);
 | 
			
		||||
 | 
			
		||||
        useClickAway(inputRef, editFieldName);
 | 
			
		||||
        useKeyPressEvent('Enter', editFieldName);
 | 
			
		||||
        useKeyPressEvent('Escape', abortEdit);
 | 
			
		||||
 | 
			
		||||
        const enterEditMode = (e: React.MouseEvent) => {
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            setEditMode(true);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div
 | 
			
		||||
                className={`group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800 ${
 | 
			
		||||
@@ -113,42 +152,100 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                        />
 | 
			
		||||
                    </>
 | 
			
		||||
                )}
 | 
			
		||||
                <div className="block truncate text-left">{field.name}</div>
 | 
			
		||||
                <div className="flex max-w-[35%] justify-end gap-2 truncate hover:shrink-0">
 | 
			
		||||
                    {field.primaryKey ? (
 | 
			
		||||
                <div
 | 
			
		||||
                    className={cn(
 | 
			
		||||
                        'flex items-center gap-1 truncate text-left',
 | 
			
		||||
                        {
 | 
			
		||||
                            'font-semibold': field.primaryKey || field.unique,
 | 
			
		||||
                            'w-full': editMode,
 | 
			
		||||
                        }
 | 
			
		||||
                    )}
 | 
			
		||||
                >
 | 
			
		||||
                    {editMode ? (
 | 
			
		||||
                        <>
 | 
			
		||||
                            <Input
 | 
			
		||||
                                ref={inputRef}
 | 
			
		||||
                                onBlur={editFieldName}
 | 
			
		||||
                                placeholder={field.name}
 | 
			
		||||
                                autoFocus
 | 
			
		||||
                                type="text"
 | 
			
		||||
                                value={fieldName}
 | 
			
		||||
                                onClick={(e) => e.stopPropagation()}
 | 
			
		||||
                                onChange={(e) => setFieldName(e.target.value)}
 | 
			
		||||
                                className="h-5 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900"
 | 
			
		||||
                            />
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant="ghost"
 | 
			
		||||
                                className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
 | 
			
		||||
                                onClick={editFieldName}
 | 
			
		||||
                            >
 | 
			
		||||
                                <Check className="size-4" />
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </>
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        // <span
 | 
			
		||||
                        //     className="truncate"
 | 
			
		||||
                        //     onClick={readonly ? undefined : enterEditMode}
 | 
			
		||||
                        // >
 | 
			
		||||
                        //     {field.name}
 | 
			
		||||
                        // </span>
 | 
			
		||||
                        <span
 | 
			
		||||
                            className="truncate"
 | 
			
		||||
                            onDoubleClick={enterEditMode}
 | 
			
		||||
                        >
 | 
			
		||||
                            {field.name}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    )}
 | 
			
		||||
                    {/* <span className="truncate">{field.name}</span> */}
 | 
			
		||||
                    {field.comments && !editMode ? (
 | 
			
		||||
                        <Tooltip>
 | 
			
		||||
                            <TooltipTrigger asChild>
 | 
			
		||||
                                <div className="shrink-0 cursor-pointer text-muted-foreground">
 | 
			
		||||
                                    <MessageCircleMore size={14} />
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </TooltipTrigger>
 | 
			
		||||
                            <TooltipContent>{field.comments}</TooltipContent>
 | 
			
		||||
                        </Tooltip>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                </div>
 | 
			
		||||
                {editMode ? null : (
 | 
			
		||||
                    <div className="flex max-w-[35%] justify-end gap-1.5 truncate hover:shrink-0">
 | 
			
		||||
                        {field.primaryKey ? (
 | 
			
		||||
                            <div
 | 
			
		||||
                                className={cn(
 | 
			
		||||
                                    'text-muted-foreground',
 | 
			
		||||
                                    !readonly ? 'group-hover:hidden' : ''
 | 
			
		||||
                                )}
 | 
			
		||||
                            >
 | 
			
		||||
                                <KeyRound size={14} />
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ) : null}
 | 
			
		||||
 | 
			
		||||
                        <div
 | 
			
		||||
                            className={cn(
 | 
			
		||||
                                'text-muted-foreground',
 | 
			
		||||
                                'content-center truncate text-right text-xs text-muted-foreground shrink-0',
 | 
			
		||||
                                !readonly ? 'group-hover:hidden' : ''
 | 
			
		||||
                            )}
 | 
			
		||||
                        >
 | 
			
		||||
                            <KeyRound size={14} />
 | 
			
		||||
                            {field.type.name}
 | 
			
		||||
                            {field.nullable ? '?' : ''}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
 | 
			
		||||
                    <div
 | 
			
		||||
                        className={cn(
 | 
			
		||||
                            'content-center truncate text-right text-xs text-muted-foreground',
 | 
			
		||||
                            !readonly ? 'group-hover:hidden' : ''
 | 
			
		||||
                        {readonly ? null : (
 | 
			
		||||
                            <div className="hidden flex-row group-hover:flex">
 | 
			
		||||
                                <Button
 | 
			
		||||
                                    variant="ghost"
 | 
			
		||||
                                    className="size-6 p-0 hover:bg-primary-foreground"
 | 
			
		||||
                                    onClick={(e) => {
 | 
			
		||||
                                        e.stopPropagation();
 | 
			
		||||
                                        removeField(tableNodeId, field.id);
 | 
			
		||||
                                    }}
 | 
			
		||||
                                >
 | 
			
		||||
                                    <Trash2 className="size-3.5 text-red-700" />
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        )}
 | 
			
		||||
                    >
 | 
			
		||||
                        {field.type.name}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {readonly ? null : (
 | 
			
		||||
                        <div className="hidden flex-row group-hover:flex">
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant="ghost"
 | 
			
		||||
                                className="size-6 p-0 hover:bg-primary-foreground"
 | 
			
		||||
                                onClick={(e) => {
 | 
			
		||||
                                    e.stopPropagation();
 | 
			
		||||
                                    removeField(tableNodeId, field.id);
 | 
			
		||||
                                }}
 | 
			
		||||
                            >
 | 
			
		||||
                                <Trash2 className="size-3.5 text-red-700" />
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    )}
 | 
			
		||||
                </div>
 | 
			
		||||
                )}
 | 
			
		||||
            </div>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,11 @@ import { Button } from '@/components/button/button';
 | 
			
		||||
import {
 | 
			
		||||
    ChevronsLeftRight,
 | 
			
		||||
    ChevronsRightLeft,
 | 
			
		||||
    Pencil,
 | 
			
		||||
    Table2,
 | 
			
		||||
    ChevronDown,
 | 
			
		||||
    ChevronUp,
 | 
			
		||||
    Check,
 | 
			
		||||
    CircleDotDashed,
 | 
			
		||||
} from 'lucide-react';
 | 
			
		||||
import { Label } from '@/components/label/label';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
@@ -22,6 +23,13 @@ import { TableNodeContextMenu } from './table-node-context-menu';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import { TableNodeDependencyIndicator } from './table-node-dependency-indicator';
 | 
			
		||||
import type { EdgeType } from '../canvas';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { useClickAway, useKeyPressEvent } from 'react-use';
 | 
			
		||||
import {
 | 
			
		||||
    Tooltip,
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
    TooltipTrigger,
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
 | 
			
		||||
export type TableNodeType = Node<
 | 
			
		||||
    {
 | 
			
		||||
@@ -49,6 +57,9 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
        const { openTableFromSidebar, selectSidebarSection } = useLayout();
 | 
			
		||||
        const [expanded, setExpanded] = useState(false);
 | 
			
		||||
        const { t } = useTranslation();
 | 
			
		||||
        const [editMode, setEditMode] = useState(false);
 | 
			
		||||
        const [tableName, setTableName] = useState(table.name);
 | 
			
		||||
        const inputRef = React.useRef<HTMLInputElement>(null);
 | 
			
		||||
 | 
			
		||||
        const selectedRelEdges = edges.filter(
 | 
			
		||||
            (edge) =>
 | 
			
		||||
@@ -125,6 +136,28 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
            ].sort((a, b) => table.fields.indexOf(a) - table.fields.indexOf(b));
 | 
			
		||||
        }, [expanded, table.fields, isMustDisplayedField]);
 | 
			
		||||
 | 
			
		||||
        const editTableName = useCallback(() => {
 | 
			
		||||
            if (!editMode) return;
 | 
			
		||||
            if (tableName.trim()) {
 | 
			
		||||
                updateTable(table.id, { name: tableName.trim() });
 | 
			
		||||
            }
 | 
			
		||||
            setEditMode(false);
 | 
			
		||||
        }, [tableName, table.id, updateTable, editMode]);
 | 
			
		||||
 | 
			
		||||
        const abortEdit = useCallback(() => {
 | 
			
		||||
            setEditMode(false);
 | 
			
		||||
            setTableName(table.name);
 | 
			
		||||
        }, [table.name]);
 | 
			
		||||
 | 
			
		||||
        useClickAway(inputRef, editTableName);
 | 
			
		||||
        useKeyPressEvent('Enter', editTableName);
 | 
			
		||||
        useKeyPressEvent('Escape', abortEdit);
 | 
			
		||||
 | 
			
		||||
        const enterEditMode = (e: React.MouseEvent) => {
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            setEditMode(true);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <TableNodeContextMenu table={table}>
 | 
			
		||||
                <div
 | 
			
		||||
@@ -168,35 +201,72 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                    <div className="group flex h-9 items-center justify-between bg-slate-200 px-2 dark:bg-slate-900">
 | 
			
		||||
                        <div className="flex min-w-0 flex-1 items-center gap-2">
 | 
			
		||||
                            <Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
 | 
			
		||||
                            <Label className="truncate text-sm font-bold">
 | 
			
		||||
                                {table.name}
 | 
			
		||||
                            </Label>
 | 
			
		||||
                            {editMode ? (
 | 
			
		||||
                                <>
 | 
			
		||||
                                    <Input
 | 
			
		||||
                                        ref={inputRef}
 | 
			
		||||
                                        onBlur={editTableName}
 | 
			
		||||
                                        placeholder={table.name}
 | 
			
		||||
                                        autoFocus
 | 
			
		||||
                                        type="text"
 | 
			
		||||
                                        value={tableName}
 | 
			
		||||
                                        onClick={(e) => e.stopPropagation()}
 | 
			
		||||
                                        onChange={(e) =>
 | 
			
		||||
                                            setTableName(e.target.value)
 | 
			
		||||
                                        }
 | 
			
		||||
                                        className="h-6 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900"
 | 
			
		||||
                                    />
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                        variant="ghost"
 | 
			
		||||
                                        className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
 | 
			
		||||
                                        onClick={editTableName}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <Check className="size-4" />
 | 
			
		||||
                                    </Button>
 | 
			
		||||
                                </>
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                <Tooltip>
 | 
			
		||||
                                    <TooltipTrigger asChild>
 | 
			
		||||
                                        <Label
 | 
			
		||||
                                            className="text-editable truncate px-2 py-0.5 text-sm font-bold"
 | 
			
		||||
                                            onDoubleClick={enterEditMode}
 | 
			
		||||
                                        >
 | 
			
		||||
                                            {table.name}
 | 
			
		||||
                                        </Label>
 | 
			
		||||
                                    </TooltipTrigger>
 | 
			
		||||
                                    <TooltipContent>
 | 
			
		||||
                                        {t('tool_tips.double_click_to_edit')}
 | 
			
		||||
                                    </TooltipContent>
 | 
			
		||||
                                </Tooltip>
 | 
			
		||||
                            )}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div className="hidden shrink-0 flex-row group-hover:flex">
 | 
			
		||||
                            {readonly ? null : (
 | 
			
		||||
                            {readonly || editMode ? null : (
 | 
			
		||||
                                <Button
 | 
			
		||||
                                    variant="ghost"
 | 
			
		||||
                                    className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
 | 
			
		||||
                                    onClick={openTableInEditor}
 | 
			
		||||
                                >
 | 
			
		||||
                                    <Pencil className="size-4" />
 | 
			
		||||
                                    <CircleDotDashed className="size-4" />
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            )}
 | 
			
		||||
                            {editMode ? null : (
 | 
			
		||||
                                <Button
 | 
			
		||||
                                    variant="ghost"
 | 
			
		||||
                                    className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
 | 
			
		||||
                                    onClick={
 | 
			
		||||
                                        table.width !== MAX_TABLE_SIZE
 | 
			
		||||
                                            ? expandTable
 | 
			
		||||
                                            : shrinkTable
 | 
			
		||||
                                    }
 | 
			
		||||
                                >
 | 
			
		||||
                                    {table.width !== MAX_TABLE_SIZE ? (
 | 
			
		||||
                                        <ChevronsLeftRight className="size-4" />
 | 
			
		||||
                                    ) : (
 | 
			
		||||
                                        <ChevronsRightLeft className="size-4" />
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            )}
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant="ghost"
 | 
			
		||||
                                className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
 | 
			
		||||
                                onClick={
 | 
			
		||||
                                    table.width !== MAX_TABLE_SIZE
 | 
			
		||||
                                        ? expandTable
 | 
			
		||||
                                        : shrinkTable
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                {table.width !== MAX_TABLE_SIZE ? (
 | 
			
		||||
                                    <ChevronsLeftRight className="size-4" />
 | 
			
		||||
                                ) : (
 | 
			
		||||
                                    <ChevronsRightLeft className="size-4" />
 | 
			
		||||
                                )}
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,8 @@ import {
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { keyboardShortcutsForOS } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
 | 
			
		||||
import { KeyboardShortcutAction } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
 | 
			
		||||
 | 
			
		||||
const convertToPercentage = (value: number) => `${Math.round(value * 100)}%`;
 | 
			
		||||
 | 
			
		||||
@@ -75,6 +77,14 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
 | 
			
		||||
                                </TooltipTrigger>
 | 
			
		||||
                                <TooltipContent>
 | 
			
		||||
                                    {t('toolbar.save')}
 | 
			
		||||
                                    <span className="ml-2 text-muted-foreground">
 | 
			
		||||
                                        {
 | 
			
		||||
                                            keyboardShortcutsForOS[
 | 
			
		||||
                                                KeyboardShortcutAction
 | 
			
		||||
                                                    .SAVE_DIAGRAM
 | 
			
		||||
                                            ].keyCombinationLabel
 | 
			
		||||
                                        }
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </TooltipContent>
 | 
			
		||||
                            </Tooltip>
 | 
			
		||||
                            <Separator orientation="vertical" />
 | 
			
		||||
@@ -88,7 +98,16 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
 | 
			
		||||
                                </ToolbarButton>
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </TooltipTrigger>
 | 
			
		||||
                        <TooltipContent>{t('toolbar.show_all')}</TooltipContent>
 | 
			
		||||
                        <TooltipContent>
 | 
			
		||||
                            {t('toolbar.show_all')}
 | 
			
		||||
                            <span className="ml-2 text-muted-foreground">
 | 
			
		||||
                                {
 | 
			
		||||
                                    keyboardShortcutsForOS[
 | 
			
		||||
                                        KeyboardShortcutAction.SHOW_ALL
 | 
			
		||||
                                    ].keyCombinationLabel
 | 
			
		||||
                                }
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </TooltipContent>
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
                    <Separator orientation="vertical" />
 | 
			
		||||
                    <Tooltip>
 | 
			
		||||
@@ -130,7 +149,16 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
 | 
			
		||||
                                </ToolbarButton>
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </TooltipTrigger>
 | 
			
		||||
                        <TooltipContent>{t('toolbar.undo')}</TooltipContent>
 | 
			
		||||
                        <TooltipContent>
 | 
			
		||||
                            {t('toolbar.undo')}
 | 
			
		||||
                            <span className="ml-2 text-muted-foreground">
 | 
			
		||||
                                {
 | 
			
		||||
                                    keyboardShortcutsForOS[
 | 
			
		||||
                                        KeyboardShortcutAction.UNDO
 | 
			
		||||
                                    ].keyCombinationLabel
 | 
			
		||||
                                }
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </TooltipContent>
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
                    <Tooltip>
 | 
			
		||||
                        <TooltipTrigger asChild>
 | 
			
		||||
@@ -143,7 +171,16 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
 | 
			
		||||
                                </ToolbarButton>
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </TooltipTrigger>
 | 
			
		||||
                        <TooltipContent>{t('toolbar.redo')}</TooltipContent>
 | 
			
		||||
                        <TooltipContent>
 | 
			
		||||
                            {t('toolbar.redo')}
 | 
			
		||||
                            <span className="ml-2 text-muted-foreground">
 | 
			
		||||
                                {
 | 
			
		||||
                                    keyboardShortcutsForOS[
 | 
			
		||||
                                        KeyboardShortcutAction.REDO
 | 
			
		||||
                                    ].keyCombinationLabel
 | 
			
		||||
                                }
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </TooltipContent>
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
                </CardContent>
 | 
			
		||||
            </Card>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +1,12 @@
 | 
			
		||||
import React, {
 | 
			
		||||
    Suspense,
 | 
			
		||||
    useCallback,
 | 
			
		||||
    useEffect,
 | 
			
		||||
    useRef,
 | 
			
		||||
    useState,
 | 
			
		||||
} from 'react';
 | 
			
		||||
import React, { Suspense, useCallback, useEffect, useRef } from 'react';
 | 
			
		||||
import { TopNavbar } from './top-navbar/top-navbar';
 | 
			
		||||
import { useNavigate, useParams } from 'react-router-dom';
 | 
			
		||||
import { useConfig } from '@/hooks/use-config';
 | 
			
		||||
import { useParams } from 'react-router-dom';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
 | 
			
		||||
import { Toaster } from '@/components/toast/toaster';
 | 
			
		||||
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
 | 
			
		||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
 | 
			
		||||
import { useLayout } from '@/hooks/use-layout';
 | 
			
		||||
import { useToast } from '@/components/toast/use-toast';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import { ToastAction } from '@/components/toast/toast';
 | 
			
		||||
import { useLocalConfig } from '@/hooks/use-local-config';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
@@ -35,11 +25,18 @@ import { DialogProvider } from '@/context/dialog-context/dialog-provider';
 | 
			
		||||
import { KeyboardShortcutsProvider } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts-provider';
 | 
			
		||||
import { Spinner } from '@/components/spinner/spinner';
 | 
			
		||||
import { Helmet } from 'react-helmet-async';
 | 
			
		||||
import { useStorage } from '@/hooks/use-storage';
 | 
			
		||||
import { AlertProvider } from '@/context/alert-context/alert-provider';
 | 
			
		||||
import { CanvasProvider } from '@/context/canvas-context/canvas-provider';
 | 
			
		||||
import { HIDE_BUCKLE_DOT_DEV } from '@/lib/env';
 | 
			
		||||
import { useDiagramLoader } from './use-diagram-loader';
 | 
			
		||||
 | 
			
		||||
const OPEN_STAR_US_AFTER_SECONDS = 30;
 | 
			
		||||
const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1;
 | 
			
		||||
 | 
			
		||||
const OPEN_BUCKLE_AFTER_SECONDS = 60;
 | 
			
		||||
const SHOW_BUCKLE_AGAIN_AFTER_DAYS = 1;
 | 
			
		||||
const SHOW_BUCKLE_AGAIN_OPENED_AFTER_DAYS = 7;
 | 
			
		||||
 | 
			
		||||
export const EditorDesktopLayoutLazy = React.lazy(
 | 
			
		||||
    () => import('./editor-desktop-layout')
 | 
			
		||||
);
 | 
			
		||||
@@ -49,100 +46,31 @@ export const EditorMobileLayoutLazy = React.lazy(
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const EditorPageComponent: React.FC = () => {
 | 
			
		||||
    const {
 | 
			
		||||
        loadDiagram,
 | 
			
		||||
        diagramName,
 | 
			
		||||
        currentDiagram,
 | 
			
		||||
        schemas,
 | 
			
		||||
        filteredSchemas,
 | 
			
		||||
    } = useChartDB();
 | 
			
		||||
    const { diagramName, currentDiagram, schemas, filteredSchemas } =
 | 
			
		||||
        useChartDB();
 | 
			
		||||
    const { openSelectSchema, showSidePanel } = useLayout();
 | 
			
		||||
    const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
 | 
			
		||||
    const { showLoader, hideLoader } = useFullScreenLoader();
 | 
			
		||||
    const { openCreateDiagramDialog, openStarUsDialog } = useDialog();
 | 
			
		||||
    const { openStarUsDialog, openBuckleDialog } = useDialog();
 | 
			
		||||
    const { diagramId } = useParams<{ diagramId: string }>();
 | 
			
		||||
    const { config, updateConfig } = useConfig();
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
    const { isMd: isDesktop } = useBreakpoint('md');
 | 
			
		||||
    const [initialDiagram, setInitialDiagram] = useState<Diagram | undefined>();
 | 
			
		||||
    const {
 | 
			
		||||
        hideMultiSchemaNotification,
 | 
			
		||||
        setHideMultiSchemaNotification,
 | 
			
		||||
        starUsDialogLastOpen,
 | 
			
		||||
        setStarUsDialogLastOpen,
 | 
			
		||||
        githubRepoOpened,
 | 
			
		||||
        setBuckleDialogLastOpen,
 | 
			
		||||
        buckleDialogLastOpen,
 | 
			
		||||
        buckleWaitlistOpened,
 | 
			
		||||
    } = useLocalConfig();
 | 
			
		||||
    const { toast } = useToast();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { listDiagrams } = useStorage();
 | 
			
		||||
    const { initialDiagram } = useDiagramLoader();
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!config) {
 | 
			
		||||
        if (HIDE_BUCKLE_DOT_DEV) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (currentDiagram?.id === diagramId) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const loadDefaultDiagram = async () => {
 | 
			
		||||
            if (diagramId) {
 | 
			
		||||
                setInitialDiagram(undefined);
 | 
			
		||||
                showLoader();
 | 
			
		||||
                resetRedoStack();
 | 
			
		||||
                resetUndoStack();
 | 
			
		||||
                const diagram = await loadDiagram(diagramId);
 | 
			
		||||
                if (!diagram) {
 | 
			
		||||
                    if (currentDiagram?.id) {
 | 
			
		||||
                        await updateConfig({
 | 
			
		||||
                            defaultDiagramId: currentDiagram.id,
 | 
			
		||||
                        });
 | 
			
		||||
                        navigate(`/diagrams/${currentDiagram.id}`);
 | 
			
		||||
                    } else {
 | 
			
		||||
                        navigate('/');
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                setInitialDiagram(diagram);
 | 
			
		||||
                hideLoader();
 | 
			
		||||
            } else if (!diagramId && config.defaultDiagramId) {
 | 
			
		||||
                const diagram = await loadDiagram(config.defaultDiagramId);
 | 
			
		||||
                if (!diagram) {
 | 
			
		||||
                    await updateConfig({
 | 
			
		||||
                        defaultDiagramId: '',
 | 
			
		||||
                    });
 | 
			
		||||
                    navigate('/');
 | 
			
		||||
                } else {
 | 
			
		||||
                    navigate(`/diagrams/${config.defaultDiagramId}`);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                const diagrams = await listDiagrams();
 | 
			
		||||
 | 
			
		||||
                if (diagrams.length > 0) {
 | 
			
		||||
                    const defaultDiagramId = diagrams[0].id;
 | 
			
		||||
                    await updateConfig({ defaultDiagramId });
 | 
			
		||||
                    navigate(`/diagrams/${defaultDiagramId}`);
 | 
			
		||||
                } else {
 | 
			
		||||
                    openCreateDiagramDialog();
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        loadDefaultDiagram();
 | 
			
		||||
    }, [
 | 
			
		||||
        diagramId,
 | 
			
		||||
        openCreateDiagramDialog,
 | 
			
		||||
        config,
 | 
			
		||||
        navigate,
 | 
			
		||||
        listDiagrams,
 | 
			
		||||
        loadDiagram,
 | 
			
		||||
        resetRedoStack,
 | 
			
		||||
        resetUndoStack,
 | 
			
		||||
        hideLoader,
 | 
			
		||||
        showLoader,
 | 
			
		||||
        currentDiagram?.id,
 | 
			
		||||
        updateConfig,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!currentDiagram?.id || githubRepoOpened) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -163,6 +91,37 @@ const EditorPageComponent: React.FC = () => {
 | 
			
		||||
        starUsDialogLastOpen,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (HIDE_BUCKLE_DOT_DEV) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!currentDiagram?.id) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            new Date().getTime() - buckleDialogLastOpen >
 | 
			
		||||
            1000 *
 | 
			
		||||
                60 *
 | 
			
		||||
                60 *
 | 
			
		||||
                24 *
 | 
			
		||||
                (buckleWaitlistOpened
 | 
			
		||||
                    ? SHOW_BUCKLE_AGAIN_OPENED_AFTER_DAYS
 | 
			
		||||
                    : SHOW_BUCKLE_AGAIN_AFTER_DAYS)
 | 
			
		||||
        ) {
 | 
			
		||||
            const lastOpen = new Date().getTime();
 | 
			
		||||
            setBuckleDialogLastOpen(lastOpen);
 | 
			
		||||
            setTimeout(openBuckleDialog, OPEN_BUCKLE_AFTER_SECONDS * 1000);
 | 
			
		||||
        }
 | 
			
		||||
    }, [
 | 
			
		||||
        currentDiagram?.id,
 | 
			
		||||
        buckleWaitlistOpened,
 | 
			
		||||
        openBuckleDialog,
 | 
			
		||||
        setBuckleDialogLastOpen,
 | 
			
		||||
        buckleDialogLastOpen,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const lastDiagramId = useRef<string>('');
 | 
			
		||||
 | 
			
		||||
    const handleChangeSchema = useCallback(async () => {
 | 
			
		||||
@@ -279,13 +238,17 @@ export const EditorPage: React.FC = () => (
 | 
			
		||||
                                <ChartDBProvider>
 | 
			
		||||
                                    <HistoryProvider>
 | 
			
		||||
                                        <ReactFlowProvider>
 | 
			
		||||
                                            <ExportImageProvider>
 | 
			
		||||
                                                <DialogProvider>
 | 
			
		||||
                                                    <KeyboardShortcutsProvider>
 | 
			
		||||
                                                        <EditorPageComponent />
 | 
			
		||||
                                                    </KeyboardShortcutsProvider>
 | 
			
		||||
                                                </DialogProvider>
 | 
			
		||||
                                            </ExportImageProvider>
 | 
			
		||||
                                            <CanvasProvider>
 | 
			
		||||
                                                <ExportImageProvider>
 | 
			
		||||
                                                    <AlertProvider>
 | 
			
		||||
                                                        <DialogProvider>
 | 
			
		||||
                                                            <KeyboardShortcutsProvider>
 | 
			
		||||
                                                                <EditorPageComponent />
 | 
			
		||||
                                                            </KeyboardShortcutsProvider>
 | 
			
		||||
                                                        </DialogProvider>
 | 
			
		||||
                                                    </AlertProvider>
 | 
			
		||||
                                                </ExportImageProvider>
 | 
			
		||||
                                            </CanvasProvider>
 | 
			
		||||
                                        </ReactFlowProvider>
 | 
			
		||||
                                    </HistoryProvider>
 | 
			
		||||
                                </ChartDBProvider>
 | 
			
		||||
 
 | 
			
		||||
@@ -98,10 +98,16 @@ export const RelationshipListItemContent: React.FC<
 | 
			
		||||
                        <Tooltip>
 | 
			
		||||
                            <TooltipTrigger>
 | 
			
		||||
                                <div className="truncate text-left text-sm">
 | 
			
		||||
                                    {sourceTable?.schema
 | 
			
		||||
                                        ? `${sourceTable.schema}.`
 | 
			
		||||
                                        : ''}
 | 
			
		||||
                                    {sourceTable?.name}({sourceField?.name})
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </TooltipTrigger>
 | 
			
		||||
                            <TooltipContent>
 | 
			
		||||
                                {sourceTable?.schema
 | 
			
		||||
                                    ? `${sourceTable.schema}.`
 | 
			
		||||
                                    : ''}
 | 
			
		||||
                                {sourceTable?.name}({sourceField?.name})
 | 
			
		||||
                            </TooltipContent>
 | 
			
		||||
                        </Tooltip>
 | 
			
		||||
@@ -117,11 +123,17 @@ export const RelationshipListItemContent: React.FC<
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <Tooltip>
 | 
			
		||||
                            <TooltipTrigger>
 | 
			
		||||
                                <div className="truncate text-left text-sm	">
 | 
			
		||||
                                <div className="truncate text-left text-sm">
 | 
			
		||||
                                    {targetTable?.schema
 | 
			
		||||
                                        ? `${targetTable.schema}.`
 | 
			
		||||
                                        : ''}
 | 
			
		||||
                                    {targetTable?.name}({targetField?.name})
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </TooltipTrigger>
 | 
			
		||||
                            <TooltipContent>
 | 
			
		||||
                                {targetTable?.schema
 | 
			
		||||
                                    ? `${targetTable.schema}.`
 | 
			
		||||
                                    : ''}
 | 
			
		||||
                                {targetTable?.name}({targetField?.name})
 | 
			
		||||
                            </TooltipContent>
 | 
			
		||||
                        </Tooltip>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,96 @@
 | 
			
		||||
import React, { useMemo } from 'react';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useTheme } from '@/hooks/use-theme';
 | 
			
		||||
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
 | 
			
		||||
import type { EffectiveTheme } from '@/context/theme-context/theme-context';
 | 
			
		||||
import { importer } from '@dbml/core';
 | 
			
		||||
import { exportBaseSQL } from '@/lib/data/export-metadata/export-sql-script';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import { useToast } from '@/components/toast/use-toast';
 | 
			
		||||
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
 | 
			
		||||
 | 
			
		||||
export interface TableDBMLProps {
 | 
			
		||||
    filteredTables: DBTable[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const getEditorTheme = (theme: EffectiveTheme) => {
 | 
			
		||||
    return theme === 'dark' ? 'dbml-dark' : 'dbml-light';
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
 | 
			
		||||
    const { currentDiagram } = useChartDB();
 | 
			
		||||
    const { effectiveTheme } = useTheme();
 | 
			
		||||
    const { toast } = useToast();
 | 
			
		||||
 | 
			
		||||
    const generateDBML = useMemo(() => {
 | 
			
		||||
        const filteredDiagram: Diagram = {
 | 
			
		||||
            ...currentDiagram,
 | 
			
		||||
            tables: filteredTables,
 | 
			
		||||
            relationships:
 | 
			
		||||
                currentDiagram.relationships?.filter((rel) => {
 | 
			
		||||
                    const sourceTable = filteredTables.find(
 | 
			
		||||
                        (t) => t.id === rel.sourceTableId
 | 
			
		||||
                    );
 | 
			
		||||
                    const targetTable = filteredTables.find(
 | 
			
		||||
                        (t) => t.id === rel.targetTableId
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    return sourceTable && targetTable;
 | 
			
		||||
                }) ?? [],
 | 
			
		||||
        } satisfies Diagram;
 | 
			
		||||
 | 
			
		||||
        const filteredDiagramWithoutSpaces: Diagram = {
 | 
			
		||||
            ...filteredDiagram,
 | 
			
		||||
            tables:
 | 
			
		||||
                filteredDiagram.tables?.map((table) => ({
 | 
			
		||||
                    ...table,
 | 
			
		||||
                    name: table.name.replace(/\s/g, '_'),
 | 
			
		||||
                    fields: table.fields.map((field) => ({
 | 
			
		||||
                        ...field,
 | 
			
		||||
                        name: field.name.replace(/\s/g, '_'),
 | 
			
		||||
                    })),
 | 
			
		||||
                    indexes: table.indexes?.map((index) => ({
 | 
			
		||||
                        ...index,
 | 
			
		||||
                        name: index.name.replace(/\s/g, '_'),
 | 
			
		||||
                    })),
 | 
			
		||||
                })) ?? [],
 | 
			
		||||
        } satisfies Diagram;
 | 
			
		||||
 | 
			
		||||
        const baseScript = exportBaseSQL(filteredDiagramWithoutSpaces);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            return importer.import(baseScript, 'postgres');
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            console.error(e);
 | 
			
		||||
 | 
			
		||||
            toast({
 | 
			
		||||
                title: 'Error',
 | 
			
		||||
                description:
 | 
			
		||||
                    'Failed to generate DBML. We would appreciate if you could report this issue!',
 | 
			
		||||
                variant: 'destructive',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return '';
 | 
			
		||||
        }
 | 
			
		||||
    }, [currentDiagram, filteredTables, toast]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <CodeSnippet
 | 
			
		||||
            code={generateDBML}
 | 
			
		||||
            className="my-0.5"
 | 
			
		||||
            editorProps={{
 | 
			
		||||
                height: '100%',
 | 
			
		||||
                defaultLanguage: 'dbml',
 | 
			
		||||
                beforeMount: setupDBMLLanguage,
 | 
			
		||||
                loading: false,
 | 
			
		||||
                theme: getEditorTheme(effectiveTheme),
 | 
			
		||||
                options: {
 | 
			
		||||
                    wordWrap: 'off',
 | 
			
		||||
                    mouseWheelZoom: false,
 | 
			
		||||
                    domReadOnly: true,
 | 
			
		||||
                },
 | 
			
		||||
            }}
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,18 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Toggle } from '@/components/toggle/toggle';
 | 
			
		||||
 | 
			
		||||
export const TableIndexToggle = React.forwardRef<
 | 
			
		||||
    React.ElementRef<typeof Toggle>,
 | 
			
		||||
    React.ComponentPropsWithoutRef<typeof Toggle>
 | 
			
		||||
>((props, ref) => {
 | 
			
		||||
    return (
 | 
			
		||||
        <Toggle
 | 
			
		||||
            {...props}
 | 
			
		||||
            ref={ref}
 | 
			
		||||
            variant="default"
 | 
			
		||||
            className="h-8 w-[32px] p-2 text-xs text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
TableIndexToggle.displayName = Toggle.displayName;
 | 
			
		||||
@@ -14,6 +14,12 @@ import { Label } from '@/components/label/label';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { SelectBox } from '@/components/select-box/select-box';
 | 
			
		||||
import { TableIndexToggle } from './table-index-toggle';
 | 
			
		||||
import {
 | 
			
		||||
    Tooltip,
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
    TooltipTrigger,
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
 | 
			
		||||
export interface TableIndexProps {
 | 
			
		||||
    index: DBIndex;
 | 
			
		||||
@@ -54,7 +60,28 @@ export const TableIndex: React.FC<TableIndexProps> = ({
 | 
			
		||||
                )}
 | 
			
		||||
                keepOrder
 | 
			
		||||
            />
 | 
			
		||||
            <div className="flex shrink-0">
 | 
			
		||||
            <div className="flex shrink-0 gap-1">
 | 
			
		||||
                <Tooltip>
 | 
			
		||||
                    <TooltipTrigger asChild>
 | 
			
		||||
                        <span>
 | 
			
		||||
                            <TableIndexToggle
 | 
			
		||||
                                pressed={index.unique}
 | 
			
		||||
                                onPressedChange={(value) =>
 | 
			
		||||
                                    updateIndex({
 | 
			
		||||
                                        unique: !!value,
 | 
			
		||||
                                    })
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                U
 | 
			
		||||
                            </TableIndexToggle>
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </TooltipTrigger>
 | 
			
		||||
                    <TooltipContent>
 | 
			
		||||
                        {t(
 | 
			
		||||
                            'side_panel.tables_section.table.index_actions.unique'
 | 
			
		||||
                        )}
 | 
			
		||||
                    </TooltipContent>
 | 
			
		||||
                </Tooltip>
 | 
			
		||||
                <Popover>
 | 
			
		||||
                    <PopoverTrigger asChild>
 | 
			
		||||
                        <Button
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import React, { useCallback } from 'react';
 | 
			
		||||
import { Plus, FileType2, FileKey2, MessageCircleMore } from 'lucide-react';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import {
 | 
			
		||||
@@ -70,17 +70,29 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const createIndexHandler = () => {
 | 
			
		||||
        setSelectedItems((prev) => {
 | 
			
		||||
            if (prev.includes('indexes')) {
 | 
			
		||||
                return prev;
 | 
			
		||||
            }
 | 
			
		||||
    const createIndexHandler = useCallback(
 | 
			
		||||
        (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            setSelectedItems((prev) => {
 | 
			
		||||
                if (prev.includes('indexes')) {
 | 
			
		||||
                    return prev;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
            return [...prev, 'indexes'];
 | 
			
		||||
        });
 | 
			
		||||
                return [...prev, 'indexes'];
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        createIndex(table.id);
 | 
			
		||||
    };
 | 
			
		||||
            createIndex(table.id);
 | 
			
		||||
        },
 | 
			
		||||
        [createIndex, table.id, setSelectedItems]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const createFieldHandler = useCallback(
 | 
			
		||||
        (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            createField(table.id);
 | 
			
		||||
        },
 | 
			
		||||
        [createField, table.id]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div
 | 
			
		||||
@@ -113,10 +125,7 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                        variant="ghost"
 | 
			
		||||
                                        className="size-4 p-0 text-xs hover:bg-primary-foreground"
 | 
			
		||||
                                        onClick={(e) => {
 | 
			
		||||
                                            e.stopPropagation();
 | 
			
		||||
                                            createField(table.id);
 | 
			
		||||
                                        }}
 | 
			
		||||
                                        onClick={createFieldHandler}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <Plus className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
 | 
			
		||||
                                    </Button>
 | 
			
		||||
@@ -153,6 +162,18 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
 | 
			
		||||
                                    />
 | 
			
		||||
                                ))}
 | 
			
		||||
                            </SortableContext>
 | 
			
		||||
                            <div className="flex justify-start p-1">
 | 
			
		||||
                                <Button
 | 
			
		||||
                                    variant="ghost"
 | 
			
		||||
                                    className="flex h-7 items-center gap-1 px-2 text-xs"
 | 
			
		||||
                                    onClick={createFieldHandler}
 | 
			
		||||
                                >
 | 
			
		||||
                                    <Plus className="size-4 text-muted-foreground" />
 | 
			
		||||
                                    {t(
 | 
			
		||||
                                        'side_panel.tables_section.table.add_field'
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </DndContext>
 | 
			
		||||
                    </AccordionContent>
 | 
			
		||||
                </AccordionItem>
 | 
			
		||||
@@ -173,10 +194,7 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                        variant="ghost"
 | 
			
		||||
                                        className="size-4 p-0 text-xs hover:bg-primary-foreground"
 | 
			
		||||
                                        onClick={(e) => {
 | 
			
		||||
                                            e.stopPropagation();
 | 
			
		||||
                                            createIndexHandler();
 | 
			
		||||
                                        }}
 | 
			
		||||
                                        onClick={createIndexHandler}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <Plus className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
 | 
			
		||||
                                    </Button>
 | 
			
		||||
@@ -198,6 +216,16 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
 | 
			
		||||
                                fields={table.fields}
 | 
			
		||||
                            />
 | 
			
		||||
                        ))}
 | 
			
		||||
                        <div className="flex justify-start p-1">
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant="ghost"
 | 
			
		||||
                                className="flex h-7 items-center gap-1 px-2 text-xs"
 | 
			
		||||
                                onClick={createIndexHandler}
 | 
			
		||||
                            >
 | 
			
		||||
                                <Plus className="size-4 text-muted-foreground" />
 | 
			
		||||
                                {t('side_panel.tables_section.table.add_index')}
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </AccordionContent>
 | 
			
		||||
                </AccordionItem>
 | 
			
		||||
 | 
			
		||||
@@ -248,7 +276,7 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
 | 
			
		||||
                    <Button
 | 
			
		||||
                        variant="outline"
 | 
			
		||||
                        className="h-8 p-2 text-xs"
 | 
			
		||||
                        onClick={() => createField(table.id)}
 | 
			
		||||
                        onClick={createFieldHandler}
 | 
			
		||||
                    >
 | 
			
		||||
                        <FileType2 className="h-4" />
 | 
			
		||||
                        {t('side_panel.tables_section.table.add_field')}
 | 
			
		||||
 
 | 
			
		||||
@@ -73,8 +73,14 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
 | 
			
		||||
        setEditMode(false);
 | 
			
		||||
    }, [tableName, table.id, updateTable, editMode]);
 | 
			
		||||
 | 
			
		||||
    const abortEdit = useCallback(() => {
 | 
			
		||||
        setEditMode(false);
 | 
			
		||||
        setTableName(table.name);
 | 
			
		||||
    }, [table.name]);
 | 
			
		||||
 | 
			
		||||
    useClickAway(inputRef, editTableName);
 | 
			
		||||
    useKeyPressEvent('Enter', editTableName);
 | 
			
		||||
    useKeyPressEvent('Escape', abortEdit);
 | 
			
		||||
 | 
			
		||||
    const enterEditMode = (e: React.MouseEvent) => {
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
@@ -156,7 +162,7 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
 | 
			
		||||
                        <EllipsisVertical />
 | 
			
		||||
                    </ListItemHeaderButton>
 | 
			
		||||
                </DropdownMenuTrigger>
 | 
			
		||||
                <DropdownMenuContent className="w-fit">
 | 
			
		||||
                <DropdownMenuContent className="w-fit min-w-40">
 | 
			
		||||
                    <DropdownMenuLabel>
 | 
			
		||||
                        {t(
 | 
			
		||||
                            'side_panel.tables_section.table.table_actions.title'
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,8 @@
 | 
			
		||||
import React, { useCallback, useMemo } from 'react';
 | 
			
		||||
import React, { useCallback, useMemo, useState } from 'react';
 | 
			
		||||
import { TableList } from './table-list/table-list';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { Table, ListCollapse } from 'lucide-react';
 | 
			
		||||
import { Table, List, X, Code } from 'lucide-react';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
@@ -18,6 +17,9 @@ import {
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import { useViewport } from '@xyflow/react';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import { TableDBML } from './table-dbml/table-dbml';
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook';
 | 
			
		||||
import { getOperatingSystem } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
export interface TablesSectionProps {}
 | 
			
		||||
 | 
			
		||||
@@ -26,8 +28,10 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
 | 
			
		||||
    const { openTableSchemaDialog } = useDialog();
 | 
			
		||||
    const viewport = useViewport();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { closeAllTablesInSidebar, openTableFromSidebar } = useLayout();
 | 
			
		||||
    const { openTableFromSidebar } = useLayout();
 | 
			
		||||
    const [filterText, setFilterText] = React.useState('');
 | 
			
		||||
    const [showDBML, setShowDBML] = useState(false);
 | 
			
		||||
    const filterInputRef = React.useRef<HTMLInputElement>(null);
 | 
			
		||||
 | 
			
		||||
    const filteredTables = useMemo(() => {
 | 
			
		||||
        const filterTableName: (table: DBTable) => boolean = (table) =>
 | 
			
		||||
@@ -88,6 +92,34 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
 | 
			
		||||
        setFilterText,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const handleClearFilter = useCallback(() => {
 | 
			
		||||
        setFilterText('');
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const operatingSystem = useMemo(() => getOperatingSystem(), []);
 | 
			
		||||
 | 
			
		||||
    useHotkeys(
 | 
			
		||||
        operatingSystem === 'mac' ? 'meta+f' : 'ctrl+f',
 | 
			
		||||
        () => {
 | 
			
		||||
            filterInputRef.current?.focus();
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            preventDefault: true,
 | 
			
		||||
        },
 | 
			
		||||
        [filterInputRef]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    useHotkeys(
 | 
			
		||||
        operatingSystem === 'mac' ? 'meta+p' : 'ctrl+p',
 | 
			
		||||
        () => {
 | 
			
		||||
            setShowDBML((value) => !value);
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            preventDefault: true,
 | 
			
		||||
        },
 | 
			
		||||
        [setShowDBML]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <section
 | 
			
		||||
            className="flex flex-1 flex-col overflow-hidden px-2"
 | 
			
		||||
@@ -101,19 +133,29 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
 | 
			
		||||
                                <Button
 | 
			
		||||
                                    variant="ghost"
 | 
			
		||||
                                    className="size-8 p-0"
 | 
			
		||||
                                    onClick={closeAllTablesInSidebar}
 | 
			
		||||
                                    onClick={() =>
 | 
			
		||||
                                        setShowDBML((value) => !value)
 | 
			
		||||
                                    }
 | 
			
		||||
                                >
 | 
			
		||||
                                    <ListCollapse className="size-4" />
 | 
			
		||||
                                    {showDBML ? (
 | 
			
		||||
                                        <List className="size-4" />
 | 
			
		||||
                                    ) : (
 | 
			
		||||
                                        <Code className="size-4" />
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </TooltipTrigger>
 | 
			
		||||
                        <TooltipContent>
 | 
			
		||||
                            {t('side_panel.tables_section.collapse')}
 | 
			
		||||
                            {showDBML
 | 
			
		||||
                                ? t('side_panel.tables_section.show_list')
 | 
			
		||||
                                : t('side_panel.tables_section.show_dbml')}
 | 
			
		||||
                            {operatingSystem === 'mac' ? ' (⌘P)' : ' (Ctrl+P)'}
 | 
			
		||||
                        </TooltipContent>
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="flex-1">
 | 
			
		||||
                    <Input
 | 
			
		||||
                        ref={filterInputRef}
 | 
			
		||||
                        type="text"
 | 
			
		||||
                        placeholder={t('side_panel.tables_section.filter')}
 | 
			
		||||
                        className="h-8 w-full focus-visible:ring-0"
 | 
			
		||||
@@ -131,21 +173,40 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
 | 
			
		||||
                </Button>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="flex flex-1 flex-col overflow-hidden">
 | 
			
		||||
                <ScrollArea className="h-full">
 | 
			
		||||
                    {tables.length === 0 ? (
 | 
			
		||||
                        <EmptyState
 | 
			
		||||
                            title={t(
 | 
			
		||||
                                'side_panel.tables_section.empty_state.title'
 | 
			
		||||
                            )}
 | 
			
		||||
                            description={t(
 | 
			
		||||
                                'side_panel.tables_section.empty_state.description'
 | 
			
		||||
                            )}
 | 
			
		||||
                            className="mt-20"
 | 
			
		||||
                        />
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        <TableList tables={filteredTables} />
 | 
			
		||||
                    )}
 | 
			
		||||
                </ScrollArea>
 | 
			
		||||
                {showDBML ? (
 | 
			
		||||
                    <TableDBML filteredTables={filteredTables} />
 | 
			
		||||
                ) : (
 | 
			
		||||
                    <ScrollArea className="h-full">
 | 
			
		||||
                        {tables.length === 0 ? (
 | 
			
		||||
                            <EmptyState
 | 
			
		||||
                                title={t(
 | 
			
		||||
                                    'side_panel.tables_section.empty_state.title'
 | 
			
		||||
                                )}
 | 
			
		||||
                                description={t(
 | 
			
		||||
                                    'side_panel.tables_section.empty_state.description'
 | 
			
		||||
                                )}
 | 
			
		||||
                                className="mt-20"
 | 
			
		||||
                            />
 | 
			
		||||
                        ) : filterText && filteredTables.length === 0 ? (
 | 
			
		||||
                            <div className="mt-10 flex flex-col items-center gap-2">
 | 
			
		||||
                                <div className="text-sm text-muted-foreground">
 | 
			
		||||
                                    {t('side_panel.tables_section.no_results')}
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <Button
 | 
			
		||||
                                    variant="outline"
 | 
			
		||||
                                    size="sm"
 | 
			
		||||
                                    onClick={handleClearFilter}
 | 
			
		||||
                                    className="gap-1"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <X className="size-3.5" />
 | 
			
		||||
                                    {t('side_panel.tables_section.clear')}
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ) : (
 | 
			
		||||
                            <TableList tables={filteredTables} />
 | 
			
		||||
                        )}
 | 
			
		||||
                    </ScrollArea>
 | 
			
		||||
                )}
 | 
			
		||||
            </div>
 | 
			
		||||
        </section>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,9 @@
 | 
			
		||||
import React, { useCallback, useEffect, useState } from 'react';
 | 
			
		||||
import { Label } from '@/components/label/label';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { Check } from 'lucide-react';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useClickAway, useKeyPressEvent } from 'react-use';
 | 
			
		||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
 | 
			
		||||
import { DiagramIcon } from '@/components/diagram-icon/diagram-icon';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
@@ -22,7 +20,6 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
 | 
			
		||||
    const { diagramName, updateDiagramName, currentDiagram } = useChartDB();
 | 
			
		||||
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { isMd: isDesktop } = useBreakpoint('md');
 | 
			
		||||
    const [editMode, setEditMode] = useState(false);
 | 
			
		||||
    const [editedDiagramName, setEditedDiagramName] =
 | 
			
		||||
        React.useState(diagramName);
 | 
			
		||||
@@ -54,7 +51,7 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
 | 
			
		||||
        <div className="group">
 | 
			
		||||
            <div
 | 
			
		||||
                className={cn(
 | 
			
		||||
                    'flex flex-1 flex-row items-center justify-center px-2 py-1',
 | 
			
		||||
                    'flex flex-1 flex-row items-center justify-center px-2 py-1 whitespace-nowrap',
 | 
			
		||||
                    {
 | 
			
		||||
                        'text-editable': !editMode,
 | 
			
		||||
                    }
 | 
			
		||||
@@ -64,9 +61,6 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
 | 
			
		||||
                    databaseType={currentDiagram.databaseType}
 | 
			
		||||
                    databaseEdition={currentDiagram.databaseEdition}
 | 
			
		||||
                />
 | 
			
		||||
                <div className="flex">
 | 
			
		||||
                    {isDesktop ? <Label>{t('diagrams')}/</Label> : null}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="flex flex-row items-center gap-1">
 | 
			
		||||
                    {editMode ? (
 | 
			
		||||
                        <>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,10 +7,11 @@ import {
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
    TooltipTrigger,
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import type { LocaleFunc } from 'timeago.js';
 | 
			
		||||
import { register as registerLocale } from 'timeago.js';
 | 
			
		||||
import { Save } from 'lucide-react';
 | 
			
		||||
 | 
			
		||||
export interface LastSavedProps {}
 | 
			
		||||
 | 
			
		||||
const timeAgolocaleFromLanguage = async (
 | 
			
		||||
@@ -73,8 +74,7 @@ const timeAgolocaleFromLanguage = async (
 | 
			
		||||
 | 
			
		||||
export const LastSaved: React.FC<LastSavedProps> = () => {
 | 
			
		||||
    const { currentDiagram } = useChartDB();
 | 
			
		||||
    const { t, i18n } = useTranslation();
 | 
			
		||||
    const { isMd: isDesktop } = useBreakpoint('md');
 | 
			
		||||
    const { i18n } = useTranslation();
 | 
			
		||||
    const [language, setLanguage] = useState<string>('en_US');
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
@@ -93,8 +93,11 @@ export const LastSaved: React.FC<LastSavedProps> = () => {
 | 
			
		||||
    return (
 | 
			
		||||
        <Tooltip>
 | 
			
		||||
            <TooltipTrigger>
 | 
			
		||||
                <Badge variant="secondary" className="flex gap-1">
 | 
			
		||||
                    {isDesktop ? t('last_saved') : t('saved')}
 | 
			
		||||
                <Badge
 | 
			
		||||
                    variant="secondary"
 | 
			
		||||
                    className="flex gap-1.5 whitespace-nowrap"
 | 
			
		||||
                >
 | 
			
		||||
                    <Save size={16} />
 | 
			
		||||
                    <TimeAgo
 | 
			
		||||
                        datetime={currentDiagram.updatedAt}
 | 
			
		||||
                        locale={language}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										531
									
								
								src/pages/editor-page/top-navbar/menu/menu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										531
									
								
								src/pages/editor-page/top-navbar/menu/menu.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,531 @@
 | 
			
		||||
import React, { useCallback } from 'react';
 | 
			
		||||
import {
 | 
			
		||||
    Menubar,
 | 
			
		||||
    MenubarCheckboxItem,
 | 
			
		||||
    MenubarContent,
 | 
			
		||||
    MenubarItem,
 | 
			
		||||
    MenubarMenu,
 | 
			
		||||
    MenubarSeparator,
 | 
			
		||||
    MenubarShortcut,
 | 
			
		||||
    MenubarSub,
 | 
			
		||||
    MenubarSubContent,
 | 
			
		||||
    MenubarSubTrigger,
 | 
			
		||||
    MenubarTrigger,
 | 
			
		||||
} from '@/components/menubar/menubar';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import { useExportImage } from '@/hooks/use-export-image';
 | 
			
		||||
import { databaseTypeToLabelMap } from '@/lib/databases';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import { useConfig } from '@/hooks/use-config';
 | 
			
		||||
import { IS_CHARTDB_IO } from '@/lib/env';
 | 
			
		||||
import {
 | 
			
		||||
    KeyboardShortcutAction,
 | 
			
		||||
    keyboardShortcutsForOS,
 | 
			
		||||
} from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
 | 
			
		||||
import { useHistory } from '@/hooks/use-history';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useLayout } from '@/hooks/use-layout';
 | 
			
		||||
import { useTheme } from '@/hooks/use-theme';
 | 
			
		||||
import { useLocalConfig } from '@/hooks/use-local-config';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import { useAlert } from '@/context/alert-context/alert-context';
 | 
			
		||||
 | 
			
		||||
export interface MenuProps {}
 | 
			
		||||
 | 
			
		||||
export const Menu: React.FC<MenuProps> = () => {
 | 
			
		||||
    const {
 | 
			
		||||
        clearDiagramData,
 | 
			
		||||
        deleteDiagram,
 | 
			
		||||
        updateDiagramUpdatedAt,
 | 
			
		||||
        databaseType,
 | 
			
		||||
    } = useChartDB();
 | 
			
		||||
    const {
 | 
			
		||||
        openCreateDiagramDialog,
 | 
			
		||||
        openOpenDiagramDialog,
 | 
			
		||||
        openExportSQLDialog,
 | 
			
		||||
        openImportDatabaseDialog,
 | 
			
		||||
        openExportImageDialog,
 | 
			
		||||
        openExportDiagramDialog,
 | 
			
		||||
        openImportDiagramDialog,
 | 
			
		||||
        openImportDBMLDialog,
 | 
			
		||||
    } = useDialog();
 | 
			
		||||
    const { showAlert } = useAlert();
 | 
			
		||||
    const { setTheme, theme } = useTheme();
 | 
			
		||||
    const { hideSidePanel, isSidePanelShowed, showSidePanel } = useLayout();
 | 
			
		||||
    const {
 | 
			
		||||
        scrollAction,
 | 
			
		||||
        setScrollAction,
 | 
			
		||||
        setShowCardinality,
 | 
			
		||||
        showCardinality,
 | 
			
		||||
        setShowDependenciesOnCanvas,
 | 
			
		||||
        showDependenciesOnCanvas,
 | 
			
		||||
        setShowMiniMapOnCanvas,
 | 
			
		||||
        showMiniMapOnCanvas,
 | 
			
		||||
    } = useLocalConfig();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { redo, undo, hasRedo, hasUndo } = useHistory();
 | 
			
		||||
    const { config, updateConfig } = useConfig();
 | 
			
		||||
    const { exportImage } = useExportImage();
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
    const handleDeleteDiagramAction = useCallback(() => {
 | 
			
		||||
        deleteDiagram();
 | 
			
		||||
        navigate('/');
 | 
			
		||||
    }, [deleteDiagram, navigate]);
 | 
			
		||||
 | 
			
		||||
    const createNewDiagram = () => {
 | 
			
		||||
        openCreateDiagramDialog();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const openDiagram = () => {
 | 
			
		||||
        openOpenDiagramDialog();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const exportSVG = useCallback(() => {
 | 
			
		||||
        exportImage('svg', 1);
 | 
			
		||||
    }, [exportImage]);
 | 
			
		||||
 | 
			
		||||
    const exportPNG = useCallback(() => {
 | 
			
		||||
        openExportImageDialog({
 | 
			
		||||
            format: 'png',
 | 
			
		||||
        });
 | 
			
		||||
    }, [openExportImageDialog]);
 | 
			
		||||
 | 
			
		||||
    const exportJPG = useCallback(() => {
 | 
			
		||||
        openExportImageDialog({
 | 
			
		||||
            format: 'jpeg',
 | 
			
		||||
        });
 | 
			
		||||
    }, [openExportImageDialog]);
 | 
			
		||||
 | 
			
		||||
    const openChartDBIO = useCallback(() => {
 | 
			
		||||
        window.location.href = 'https://chartdb.io';
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const openChartDBDocs = useCallback(() => {
 | 
			
		||||
        window.open('https://docs.chartdb.io', '_blank');
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const openJoinDiscord = useCallback(() => {
 | 
			
		||||
        window.open('https://discord.gg/QeFwyWSKwC', '_blank');
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const openCalendly = useCallback(() => {
 | 
			
		||||
        window.open('https://calendly.com/fishner/15min', '_blank');
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const exportSQL = useCallback(
 | 
			
		||||
        (databaseType: DatabaseType) => {
 | 
			
		||||
            if (databaseType === DatabaseType.GENERIC) {
 | 
			
		||||
                openExportSQLDialog({
 | 
			
		||||
                    targetDatabaseType: DatabaseType.GENERIC,
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (IS_CHARTDB_IO) {
 | 
			
		||||
                const now = new Date();
 | 
			
		||||
                const lastExportsInLastHalfHour =
 | 
			
		||||
                    config?.exportActions?.filter(
 | 
			
		||||
                        (date) =>
 | 
			
		||||
                            now.getTime() - date.getTime() < 30 * 60 * 1000
 | 
			
		||||
                    ) ?? [];
 | 
			
		||||
 | 
			
		||||
                if (lastExportsInLastHalfHour.length >= 5) {
 | 
			
		||||
                    showAlert({
 | 
			
		||||
                        title: 'Export SQL Limit Reached',
 | 
			
		||||
                        content: (
 | 
			
		||||
                            <div className="flex flex-col gap-1 text-sm">
 | 
			
		||||
                                <div>
 | 
			
		||||
                                    We set a budget to allow the community to
 | 
			
		||||
                                    check the feature. You have reached the
 | 
			
		||||
                                    limit of 5 AI exports every 30min.
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div>
 | 
			
		||||
                                    Feel free to use your OPENAI_TOKEN, see the
 | 
			
		||||
                                    manual{' '}
 | 
			
		||||
                                    <a
 | 
			
		||||
                                        href="https://github.com/chartdb/chartdb"
 | 
			
		||||
                                        target="_blank"
 | 
			
		||||
                                        className="text-pink-600 hover:underline"
 | 
			
		||||
                                        rel="noreferrer"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        here.
 | 
			
		||||
                                    </a>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ),
 | 
			
		||||
                        closeLabel: 'Close',
 | 
			
		||||
                    });
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                updateConfig({
 | 
			
		||||
                    exportActions: [...lastExportsInLastHalfHour, now],
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            openExportSQLDialog({
 | 
			
		||||
                targetDatabaseType: databaseType,
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [config?.exportActions, updateConfig, showAlert, openExportSQLDialog]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const showOrHideSidePanel = useCallback(() => {
 | 
			
		||||
        if (isSidePanelShowed) {
 | 
			
		||||
            hideSidePanel();
 | 
			
		||||
        } else {
 | 
			
		||||
            showSidePanel();
 | 
			
		||||
        }
 | 
			
		||||
    }, [isSidePanelShowed, showSidePanel, hideSidePanel]);
 | 
			
		||||
 | 
			
		||||
    const showOrHideCardinality = useCallback(() => {
 | 
			
		||||
        setShowCardinality(!showCardinality);
 | 
			
		||||
    }, [showCardinality, setShowCardinality]);
 | 
			
		||||
 | 
			
		||||
    const showOrHideDependencies = useCallback(() => {
 | 
			
		||||
        setShowDependenciesOnCanvas(!showDependenciesOnCanvas);
 | 
			
		||||
    }, [showDependenciesOnCanvas, setShowDependenciesOnCanvas]);
 | 
			
		||||
 | 
			
		||||
    const showOrHideMiniMap = useCallback(() => {
 | 
			
		||||
        setShowMiniMapOnCanvas(!showMiniMapOnCanvas);
 | 
			
		||||
    }, [showMiniMapOnCanvas, setShowMiniMapOnCanvas]);
 | 
			
		||||
 | 
			
		||||
    const emojiAI = '✨';
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Menubar className="h-8 border-none py-2 shadow-none md:h-10 md:py-0">
 | 
			
		||||
            <MenubarMenu>
 | 
			
		||||
                <MenubarTrigger>{t('menu.file.file')}</MenubarTrigger>
 | 
			
		||||
                <MenubarContent>
 | 
			
		||||
                    <MenubarItem onClick={createNewDiagram}>
 | 
			
		||||
                        {t('menu.file.new')}
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                    <MenubarItem onClick={openDiagram}>
 | 
			
		||||
                        {t('menu.file.open')}
 | 
			
		||||
                        <MenubarShortcut>
 | 
			
		||||
                            {
 | 
			
		||||
                                keyboardShortcutsForOS[
 | 
			
		||||
                                    KeyboardShortcutAction.OPEN_DIAGRAM
 | 
			
		||||
                                ].keyCombinationLabel
 | 
			
		||||
                            }
 | 
			
		||||
                        </MenubarShortcut>
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                    <MenubarItem onClick={updateDiagramUpdatedAt}>
 | 
			
		||||
                        {t('menu.file.save')}
 | 
			
		||||
                        <MenubarShortcut>
 | 
			
		||||
                            {
 | 
			
		||||
                                keyboardShortcutsForOS[
 | 
			
		||||
                                    KeyboardShortcutAction.SAVE_DIAGRAM
 | 
			
		||||
                                ].keyCombinationLabel
 | 
			
		||||
                            }
 | 
			
		||||
                        </MenubarShortcut>
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                    <MenubarSeparator />
 | 
			
		||||
                    <MenubarSub>
 | 
			
		||||
                        <MenubarSubTrigger>
 | 
			
		||||
                            {t('menu.file.import')}
 | 
			
		||||
                        </MenubarSubTrigger>
 | 
			
		||||
                        <MenubarSubContent>
 | 
			
		||||
                            <MenubarItem onClick={openImportDiagramDialog}>
 | 
			
		||||
                                .json
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem onClick={() => openImportDBMLDialog()}>
 | 
			
		||||
                                .dbml
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarSeparator />
 | 
			
		||||
                            <MenubarItem
 | 
			
		||||
                                onClick={() =>
 | 
			
		||||
                                    openImportDatabaseDialog({
 | 
			
		||||
                                        databaseType: DatabaseType.POSTGRESQL,
 | 
			
		||||
                                    })
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                {databaseTypeToLabelMap['postgresql']}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem
 | 
			
		||||
                                onClick={() =>
 | 
			
		||||
                                    openImportDatabaseDialog({
 | 
			
		||||
                                        databaseType: DatabaseType.MYSQL,
 | 
			
		||||
                                    })
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                {databaseTypeToLabelMap['mysql']}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem
 | 
			
		||||
                                onClick={() =>
 | 
			
		||||
                                    openImportDatabaseDialog({
 | 
			
		||||
                                        databaseType: DatabaseType.SQL_SERVER,
 | 
			
		||||
                                    })
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                {databaseTypeToLabelMap['sql_server']}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem
 | 
			
		||||
                                onClick={() =>
 | 
			
		||||
                                    openImportDatabaseDialog({
 | 
			
		||||
                                        databaseType: DatabaseType.MARIADB,
 | 
			
		||||
                                    })
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                {databaseTypeToLabelMap['mariadb']}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem
 | 
			
		||||
                                onClick={() =>
 | 
			
		||||
                                    openImportDatabaseDialog({
 | 
			
		||||
                                        databaseType: DatabaseType.SQLITE,
 | 
			
		||||
                                    })
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                {databaseTypeToLabelMap['sqlite']}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                        </MenubarSubContent>
 | 
			
		||||
                    </MenubarSub>
 | 
			
		||||
                    <MenubarSeparator />
 | 
			
		||||
                    <MenubarSub>
 | 
			
		||||
                        <MenubarSubTrigger>
 | 
			
		||||
                            {t('menu.file.export_sql')}
 | 
			
		||||
                        </MenubarSubTrigger>
 | 
			
		||||
                        <MenubarSubContent>
 | 
			
		||||
                            <MenubarItem
 | 
			
		||||
                                onClick={() => exportSQL(DatabaseType.GENERIC)}
 | 
			
		||||
                            >
 | 
			
		||||
                                {databaseTypeToLabelMap['generic']}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem
 | 
			
		||||
                                onClick={() =>
 | 
			
		||||
                                    exportSQL(DatabaseType.POSTGRESQL)
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                {databaseTypeToLabelMap['postgresql']}
 | 
			
		||||
                                <MenubarShortcut className="text-base">
 | 
			
		||||
                                    {emojiAI}
 | 
			
		||||
                                </MenubarShortcut>
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem
 | 
			
		||||
                                onClick={() => exportSQL(DatabaseType.MYSQL)}
 | 
			
		||||
                            >
 | 
			
		||||
                                {databaseTypeToLabelMap['mysql']}
 | 
			
		||||
                                <MenubarShortcut className="text-base">
 | 
			
		||||
                                    {emojiAI}
 | 
			
		||||
                                </MenubarShortcut>
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem
 | 
			
		||||
                                onClick={() =>
 | 
			
		||||
                                    exportSQL(DatabaseType.SQL_SERVER)
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                {databaseTypeToLabelMap['sql_server']}
 | 
			
		||||
                                <MenubarShortcut className="text-base">
 | 
			
		||||
                                    {emojiAI}
 | 
			
		||||
                                </MenubarShortcut>
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem
 | 
			
		||||
                                onClick={() => exportSQL(DatabaseType.MARIADB)}
 | 
			
		||||
                            >
 | 
			
		||||
                                {databaseTypeToLabelMap['mariadb']}
 | 
			
		||||
                                <MenubarShortcut className="text-base">
 | 
			
		||||
                                    {emojiAI}
 | 
			
		||||
                                </MenubarShortcut>
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem
 | 
			
		||||
                                onClick={() => exportSQL(DatabaseType.SQLITE)}
 | 
			
		||||
                            >
 | 
			
		||||
                                {databaseTypeToLabelMap['sqlite']}
 | 
			
		||||
                                <MenubarShortcut className="text-base">
 | 
			
		||||
                                    {emojiAI}
 | 
			
		||||
                                </MenubarShortcut>
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                        </MenubarSubContent>
 | 
			
		||||
                    </MenubarSub>
 | 
			
		||||
                    <MenubarSub>
 | 
			
		||||
                        <MenubarSubTrigger>
 | 
			
		||||
                            {t('menu.file.export_as')}
 | 
			
		||||
                        </MenubarSubTrigger>
 | 
			
		||||
                        <MenubarSubContent>
 | 
			
		||||
                            <MenubarItem onClick={exportPNG}>PNG</MenubarItem>
 | 
			
		||||
                            <MenubarItem onClick={exportJPG}>JPG</MenubarItem>
 | 
			
		||||
                            <MenubarItem onClick={exportSVG}>SVG</MenubarItem>
 | 
			
		||||
                            <MenubarSeparator />
 | 
			
		||||
                            <MenubarItem onClick={openExportDiagramDialog}>
 | 
			
		||||
                                JSON
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                        </MenubarSubContent>
 | 
			
		||||
                    </MenubarSub>
 | 
			
		||||
                    <MenubarSeparator />
 | 
			
		||||
                    <MenubarItem
 | 
			
		||||
                        onClick={() =>
 | 
			
		||||
                            showAlert({
 | 
			
		||||
                                title: t('delete_diagram_alert.title'),
 | 
			
		||||
                                description: t(
 | 
			
		||||
                                    'delete_diagram_alert.description'
 | 
			
		||||
                                ),
 | 
			
		||||
                                actionLabel: t('delete_diagram_alert.delete'),
 | 
			
		||||
                                closeLabel: t('delete_diagram_alert.cancel'),
 | 
			
		||||
                                onAction: handleDeleteDiagramAction,
 | 
			
		||||
                            })
 | 
			
		||||
                        }
 | 
			
		||||
                    >
 | 
			
		||||
                        {t('menu.file.delete_diagram')}
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                    <MenubarSeparator />
 | 
			
		||||
                    <MenubarItem>{t('menu.file.exit')}</MenubarItem>
 | 
			
		||||
                </MenubarContent>
 | 
			
		||||
            </MenubarMenu>
 | 
			
		||||
            <MenubarMenu>
 | 
			
		||||
                <MenubarTrigger>{t('menu.edit.edit')}</MenubarTrigger>
 | 
			
		||||
                <MenubarContent>
 | 
			
		||||
                    <MenubarItem onClick={undo} disabled={!hasUndo}>
 | 
			
		||||
                        {t('menu.edit.undo')}
 | 
			
		||||
                        <MenubarShortcut>
 | 
			
		||||
                            {
 | 
			
		||||
                                keyboardShortcutsForOS[
 | 
			
		||||
                                    KeyboardShortcutAction.UNDO
 | 
			
		||||
                                ].keyCombinationLabel
 | 
			
		||||
                            }
 | 
			
		||||
                        </MenubarShortcut>
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                    <MenubarItem onClick={redo} disabled={!hasRedo}>
 | 
			
		||||
                        {t('menu.edit.redo')}
 | 
			
		||||
                        <MenubarShortcut>
 | 
			
		||||
                            {
 | 
			
		||||
                                keyboardShortcutsForOS[
 | 
			
		||||
                                    KeyboardShortcutAction.REDO
 | 
			
		||||
                                ].keyCombinationLabel
 | 
			
		||||
                            }
 | 
			
		||||
                        </MenubarShortcut>
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                    <MenubarSeparator />
 | 
			
		||||
                    <MenubarItem
 | 
			
		||||
                        onClick={() =>
 | 
			
		||||
                            showAlert({
 | 
			
		||||
                                title: t('clear_diagram_alert.title'),
 | 
			
		||||
                                description: t(
 | 
			
		||||
                                    'clear_diagram_alert.description'
 | 
			
		||||
                                ),
 | 
			
		||||
                                actionLabel: t('clear_diagram_alert.clear'),
 | 
			
		||||
                                closeLabel: t('clear_diagram_alert.cancel'),
 | 
			
		||||
                                onAction: clearDiagramData,
 | 
			
		||||
                            })
 | 
			
		||||
                        }
 | 
			
		||||
                    >
 | 
			
		||||
                        {t('menu.edit.clear')}
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                </MenubarContent>
 | 
			
		||||
            </MenubarMenu>
 | 
			
		||||
            <MenubarMenu>
 | 
			
		||||
                <MenubarTrigger>{t('menu.view.view')}</MenubarTrigger>
 | 
			
		||||
                <MenubarContent>
 | 
			
		||||
                    <MenubarItem onClick={showOrHideSidePanel}>
 | 
			
		||||
                        {isSidePanelShowed
 | 
			
		||||
                            ? t('menu.view.hide_sidebar')
 | 
			
		||||
                            : t('menu.view.show_sidebar')}
 | 
			
		||||
                        <MenubarShortcut>
 | 
			
		||||
                            {
 | 
			
		||||
                                keyboardShortcutsForOS[
 | 
			
		||||
                                    KeyboardShortcutAction.TOGGLE_SIDE_PANEL
 | 
			
		||||
                                ].keyCombinationLabel
 | 
			
		||||
                            }
 | 
			
		||||
                        </MenubarShortcut>
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                    <MenubarSeparator />
 | 
			
		||||
                    <MenubarItem onClick={showOrHideCardinality}>
 | 
			
		||||
                        {showCardinality
 | 
			
		||||
                            ? t('menu.view.hide_cardinality')
 | 
			
		||||
                            : t('menu.view.show_cardinality')}
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                    {databaseType !== DatabaseType.CLICKHOUSE ? (
 | 
			
		||||
                        <MenubarItem onClick={showOrHideDependencies}>
 | 
			
		||||
                            {showDependenciesOnCanvas
 | 
			
		||||
                                ? t('menu.view.hide_dependencies')
 | 
			
		||||
                                : t('menu.view.show_dependencies')}
 | 
			
		||||
                        </MenubarItem>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                    <MenubarItem onClick={showOrHideMiniMap}>
 | 
			
		||||
                        {showMiniMapOnCanvas
 | 
			
		||||
                            ? t('menu.view.hide_minimap')
 | 
			
		||||
                            : t('menu.view.show_minimap')}
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                    <MenubarSeparator />
 | 
			
		||||
                    <MenubarSub>
 | 
			
		||||
                        <MenubarSubTrigger>
 | 
			
		||||
                            {t('menu.view.zoom_on_scroll')}
 | 
			
		||||
                        </MenubarSubTrigger>
 | 
			
		||||
                        <MenubarSubContent>
 | 
			
		||||
                            <MenubarCheckboxItem
 | 
			
		||||
                                checked={scrollAction === 'zoom'}
 | 
			
		||||
                                onClick={() => setScrollAction('zoom')}
 | 
			
		||||
                            >
 | 
			
		||||
                                {t('zoom.on')}
 | 
			
		||||
                            </MenubarCheckboxItem>
 | 
			
		||||
                            <MenubarCheckboxItem
 | 
			
		||||
                                checked={scrollAction === 'pan'}
 | 
			
		||||
                                onClick={() => setScrollAction('pan')}
 | 
			
		||||
                            >
 | 
			
		||||
                                {t('zoom.off')}
 | 
			
		||||
                            </MenubarCheckboxItem>
 | 
			
		||||
                        </MenubarSubContent>
 | 
			
		||||
                    </MenubarSub>
 | 
			
		||||
                    <MenubarSeparator />
 | 
			
		||||
                    <MenubarSub>
 | 
			
		||||
                        <MenubarSubTrigger>
 | 
			
		||||
                            {t('menu.view.theme')}
 | 
			
		||||
                        </MenubarSubTrigger>
 | 
			
		||||
                        <MenubarSubContent>
 | 
			
		||||
                            <MenubarCheckboxItem
 | 
			
		||||
                                checked={theme === 'system'}
 | 
			
		||||
                                onClick={() => setTheme('system')}
 | 
			
		||||
                            >
 | 
			
		||||
                                {t('theme.system')}
 | 
			
		||||
                            </MenubarCheckboxItem>
 | 
			
		||||
                            <MenubarCheckboxItem
 | 
			
		||||
                                checked={theme === 'light'}
 | 
			
		||||
                                onClick={() => setTheme('light')}
 | 
			
		||||
                            >
 | 
			
		||||
                                {t('theme.light')}
 | 
			
		||||
                            </MenubarCheckboxItem>
 | 
			
		||||
                            <MenubarCheckboxItem
 | 
			
		||||
                                checked={theme === 'dark'}
 | 
			
		||||
                                onClick={() => setTheme('dark')}
 | 
			
		||||
                            >
 | 
			
		||||
                                {t('theme.dark')}
 | 
			
		||||
                            </MenubarCheckboxItem>
 | 
			
		||||
                        </MenubarSubContent>
 | 
			
		||||
                    </MenubarSub>
 | 
			
		||||
                </MenubarContent>
 | 
			
		||||
            </MenubarMenu>
 | 
			
		||||
 | 
			
		||||
            <MenubarMenu>
 | 
			
		||||
                <MenubarTrigger>{t('menu.backup.backup')}</MenubarTrigger>
 | 
			
		||||
                <MenubarContent>
 | 
			
		||||
                    <MenubarItem onClick={openExportDiagramDialog}>
 | 
			
		||||
                        {t('menu.backup.export_diagram')}
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                    <MenubarItem onClick={openImportDiagramDialog}>
 | 
			
		||||
                        {t('menu.backup.restore_diagram')}
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                </MenubarContent>
 | 
			
		||||
            </MenubarMenu>
 | 
			
		||||
 | 
			
		||||
            <MenubarMenu>
 | 
			
		||||
                <MenubarTrigger>{t('menu.help.help')}</MenubarTrigger>
 | 
			
		||||
                <MenubarContent>
 | 
			
		||||
                    <MenubarItem onClick={openChartDBDocs}>
 | 
			
		||||
                        {t('menu.help.docs_website')}
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                    <MenubarItem onClick={openChartDBIO}>
 | 
			
		||||
                        {t('menu.help.visit_website')}
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                    <MenubarItem onClick={openJoinDiscord}>
 | 
			
		||||
                        {t('menu.help.join_discord')}
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                    <MenubarItem onClick={openCalendly}>
 | 
			
		||||
                        {t('menu.help.schedule_a_call')}
 | 
			
		||||
                    </MenubarItem>
 | 
			
		||||
                </MenubarContent>
 | 
			
		||||
            </MenubarMenu>
 | 
			
		||||
        </Menubar>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,177 +1,19 @@
 | 
			
		||||
import React, { useCallback } from 'react';
 | 
			
		||||
import {
 | 
			
		||||
    Menubar,
 | 
			
		||||
    MenubarCheckboxItem,
 | 
			
		||||
    MenubarContent,
 | 
			
		||||
    MenubarItem,
 | 
			
		||||
    MenubarMenu,
 | 
			
		||||
    MenubarSeparator,
 | 
			
		||||
    MenubarShortcut,
 | 
			
		||||
    MenubarSub,
 | 
			
		||||
    MenubarSubContent,
 | 
			
		||||
    MenubarSubTrigger,
 | 
			
		||||
    MenubarTrigger,
 | 
			
		||||
} from '@/components/menubar/menubar';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import ChartDBLogo from '@/assets/logo-light.png';
 | 
			
		||||
import ChartDBDarkLogo from '@/assets/logo-dark.png';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import { useExportImage } from '@/hooks/use-export-image';
 | 
			
		||||
import { databaseTypeToLabelMap } from '@/lib/databases';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import { useConfig } from '@/hooks/use-config';
 | 
			
		||||
import { IS_CHARTDB_IO } from '@/lib/env';
 | 
			
		||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
 | 
			
		||||
import {
 | 
			
		||||
    KeyboardShortcutAction,
 | 
			
		||||
    keyboardShortcutsForOS,
 | 
			
		||||
} from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
 | 
			
		||||
import { useHistory } from '@/hooks/use-history';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { useLayout } from '@/hooks/use-layout';
 | 
			
		||||
import { useTheme } from '@/hooks/use-theme';
 | 
			
		||||
import { useLocalConfig } from '@/hooks/use-local-config';
 | 
			
		||||
import { DiagramName } from './diagram-name';
 | 
			
		||||
import { LastSaved } from './last-saved';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import { LanguageNav } from './language-nav/language-nav';
 | 
			
		||||
import { Menu } from './menu/menu';
 | 
			
		||||
import { HIDE_BUCKLE_DOT_DEV } from '@/lib/env';
 | 
			
		||||
 | 
			
		||||
export interface TopNavbarProps {}
 | 
			
		||||
 | 
			
		||||
export const TopNavbar: React.FC<TopNavbarProps> = () => {
 | 
			
		||||
    const {
 | 
			
		||||
        clearDiagramData,
 | 
			
		||||
        deleteDiagram,
 | 
			
		||||
        updateDiagramUpdatedAt,
 | 
			
		||||
        databaseType,
 | 
			
		||||
    } = useChartDB();
 | 
			
		||||
    const {
 | 
			
		||||
        openCreateDiagramDialog,
 | 
			
		||||
        openOpenDiagramDialog,
 | 
			
		||||
        openExportSQLDialog,
 | 
			
		||||
        openImportDatabaseDialog,
 | 
			
		||||
        showAlert,
 | 
			
		||||
        openExportImageDialog,
 | 
			
		||||
        openExportDiagramDialog,
 | 
			
		||||
        openImportDiagramDialog,
 | 
			
		||||
    } = useDialog();
 | 
			
		||||
    const { setTheme, theme } = useTheme();
 | 
			
		||||
    const { hideSidePanel, isSidePanelShowed, showSidePanel } = useLayout();
 | 
			
		||||
    const {
 | 
			
		||||
        scrollAction,
 | 
			
		||||
        setScrollAction,
 | 
			
		||||
        setShowCardinality,
 | 
			
		||||
        showCardinality,
 | 
			
		||||
        setShowDependenciesOnCanvas,
 | 
			
		||||
        showDependenciesOnCanvas,
 | 
			
		||||
    } = useLocalConfig();
 | 
			
		||||
    const { effectiveTheme } = useTheme();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { redo, undo, hasRedo, hasUndo } = useHistory();
 | 
			
		||||
    const { isMd: isDesktop } = useBreakpoint('md');
 | 
			
		||||
    const { config, updateConfig } = useConfig();
 | 
			
		||||
    const { exportImage } = useExportImage();
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
    const handleDeleteDiagramAction = useCallback(() => {
 | 
			
		||||
        deleteDiagram();
 | 
			
		||||
        navigate('/');
 | 
			
		||||
    }, [deleteDiagram, navigate]);
 | 
			
		||||
 | 
			
		||||
    const createNewDiagram = () => {
 | 
			
		||||
        openCreateDiagramDialog();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const openDiagram = () => {
 | 
			
		||||
        openOpenDiagramDialog();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const exportSVG = useCallback(() => {
 | 
			
		||||
        exportImage('svg', 1);
 | 
			
		||||
    }, [exportImage]);
 | 
			
		||||
 | 
			
		||||
    const exportPNG = useCallback(() => {
 | 
			
		||||
        openExportImageDialog({
 | 
			
		||||
            format: 'png',
 | 
			
		||||
        });
 | 
			
		||||
    }, [openExportImageDialog]);
 | 
			
		||||
 | 
			
		||||
    const exportJPG = useCallback(() => {
 | 
			
		||||
        openExportImageDialog({
 | 
			
		||||
            format: 'jpeg',
 | 
			
		||||
        });
 | 
			
		||||
    }, [openExportImageDialog]);
 | 
			
		||||
 | 
			
		||||
    const openChartDBIO = useCallback(() => {
 | 
			
		||||
        window.location.href = 'https://chartdb.io';
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const openJoinDiscord = useCallback(() => {
 | 
			
		||||
        window.open('https://discord.gg/QeFwyWSKwC', '_blank');
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const openCalendly = useCallback(() => {
 | 
			
		||||
        window.open('https://calendly.com/fishner/15min', '_blank');
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const exportSQL = useCallback(
 | 
			
		||||
        (databaseType: DatabaseType) => {
 | 
			
		||||
            if (databaseType === DatabaseType.GENERIC) {
 | 
			
		||||
                openExportSQLDialog({
 | 
			
		||||
                    targetDatabaseType: DatabaseType.GENERIC,
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (IS_CHARTDB_IO) {
 | 
			
		||||
                const now = new Date();
 | 
			
		||||
                const lastExportsInLastHalfHour =
 | 
			
		||||
                    config?.exportActions?.filter(
 | 
			
		||||
                        (date) =>
 | 
			
		||||
                            now.getTime() - date.getTime() < 30 * 60 * 1000
 | 
			
		||||
                    ) ?? [];
 | 
			
		||||
 | 
			
		||||
                if (lastExportsInLastHalfHour.length >= 5) {
 | 
			
		||||
                    showAlert({
 | 
			
		||||
                        title: 'Export SQL Limit Reached',
 | 
			
		||||
                        content: (
 | 
			
		||||
                            <div className="flex flex-col gap-1 text-sm">
 | 
			
		||||
                                <div>
 | 
			
		||||
                                    We set a budget to allow the community to
 | 
			
		||||
                                    check the feature. You have reached the
 | 
			
		||||
                                    limit of 5 AI exports every 30min.
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <div>
 | 
			
		||||
                                    Feel free to use your OPENAI_TOKEN, see the
 | 
			
		||||
                                    manual{' '}
 | 
			
		||||
                                    <a
 | 
			
		||||
                                        href="https://github.com/chartdb/chartdb"
 | 
			
		||||
                                        target="_blank"
 | 
			
		||||
                                        className="text-pink-600 hover:underline"
 | 
			
		||||
                                        rel="noreferrer"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        here.
 | 
			
		||||
                                    </a>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ),
 | 
			
		||||
                        closeLabel: 'Close',
 | 
			
		||||
                    });
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                updateConfig({
 | 
			
		||||
                    exportActions: [...lastExportsInLastHalfHour, now],
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            openExportSQLDialog({
 | 
			
		||||
                targetDatabaseType: databaseType,
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [config?.exportActions, updateConfig, showAlert, openExportSQLDialog]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const renderStars = useCallback(() => {
 | 
			
		||||
        return (
 | 
			
		||||
@@ -184,27 +26,30 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
 | 
			
		||||
        );
 | 
			
		||||
    }, [isDesktop]);
 | 
			
		||||
 | 
			
		||||
    const showOrHideSidePanel = useCallback(() => {
 | 
			
		||||
        if (isSidePanelShowed) {
 | 
			
		||||
            hideSidePanel();
 | 
			
		||||
        } else {
 | 
			
		||||
            showSidePanel();
 | 
			
		||||
    const openBuckleWaitlist = useCallback(() => {
 | 
			
		||||
        window.open('https://waitlist.buckle.dev', '_blank');
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const renderGetBuckleButton = useCallback(() => {
 | 
			
		||||
        if (HIDE_BUCKLE_DOT_DEV) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }, [isSidePanelShowed, showSidePanel, hideSidePanel]);
 | 
			
		||||
 | 
			
		||||
    const showOrHideCardinality = useCallback(() => {
 | 
			
		||||
        setShowCardinality(!showCardinality);
 | 
			
		||||
    }, [showCardinality, setShowCardinality]);
 | 
			
		||||
 | 
			
		||||
    const showOrHideDependencies = useCallback(() => {
 | 
			
		||||
        setShowDependenciesOnCanvas(!showDependenciesOnCanvas);
 | 
			
		||||
    }, [showDependenciesOnCanvas, setShowDependenciesOnCanvas]);
 | 
			
		||||
 | 
			
		||||
    const emojiAI = '✨';
 | 
			
		||||
        return (
 | 
			
		||||
            <button
 | 
			
		||||
                className="gradient-background relative inline-flex items-center justify-center overflow-hidden rounded-lg p-0.5 text-base text-gray-700 focus:outline-none focus:ring-0"
 | 
			
		||||
                onClick={openBuckleWaitlist}
 | 
			
		||||
            >
 | 
			
		||||
                <span className="relative inline-flex items-center justify-center whitespace-nowrap rounded-md bg-background px-2 py-0.5 font-primary text-xs font-semibold text-foreground md:text-sm">
 | 
			
		||||
                    ChartDB v2.0 🔥
 | 
			
		||||
                </span>
 | 
			
		||||
            </button>
 | 
			
		||||
        );
 | 
			
		||||
    }, [openBuckleWaitlist]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <nav className="flex flex-col justify-between border-b px-3 md:h-12 md:flex-row md:items-center md:px-4">
 | 
			
		||||
            <div className="flex flex-1 flex-col justify-between gap-x-3 md:flex-row md:justify-normal">
 | 
			
		||||
            <div className="flex flex-1 flex-col justify-between gap-x-1 md:flex-row md:justify-normal">
 | 
			
		||||
                <div className="flex items-center justify-between pt-[8px] font-primary md:py-[10px]">
 | 
			
		||||
                    <a
 | 
			
		||||
                        href="https://chartdb.io"
 | 
			
		||||
@@ -223,357 +68,19 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
 | 
			
		||||
                    </a>
 | 
			
		||||
                    {!isDesktop ? (
 | 
			
		||||
                        <div className="flex items-center gap-2">
 | 
			
		||||
                            {renderGetBuckleButton()}
 | 
			
		||||
                            {renderStars()}
 | 
			
		||||
                            <LanguageNav />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <Menubar className="h-8 border-none py-2 shadow-none md:h-10 md:py-0">
 | 
			
		||||
                    <MenubarMenu>
 | 
			
		||||
                        <MenubarTrigger>{t('menu.file.file')}</MenubarTrigger>
 | 
			
		||||
                        <MenubarContent>
 | 
			
		||||
                            <MenubarItem onClick={createNewDiagram}>
 | 
			
		||||
                                {t('menu.file.new')}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem onClick={openDiagram}>
 | 
			
		||||
                                {t('menu.file.open')}
 | 
			
		||||
                                <MenubarShortcut>
 | 
			
		||||
                                    {
 | 
			
		||||
                                        keyboardShortcutsForOS[
 | 
			
		||||
                                            KeyboardShortcutAction.OPEN_DIAGRAM
 | 
			
		||||
                                        ].keyCombinationLabel
 | 
			
		||||
                                    }
 | 
			
		||||
                                </MenubarShortcut>
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem onClick={updateDiagramUpdatedAt}>
 | 
			
		||||
                                {t('menu.file.save')}
 | 
			
		||||
                                <MenubarShortcut>
 | 
			
		||||
                                    {
 | 
			
		||||
                                        keyboardShortcutsForOS[
 | 
			
		||||
                                            KeyboardShortcutAction.SAVE_DIAGRAM
 | 
			
		||||
                                        ].keyCombinationLabel
 | 
			
		||||
                                    }
 | 
			
		||||
                                </MenubarShortcut>
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarSeparator />
 | 
			
		||||
                            <MenubarSub>
 | 
			
		||||
                                <MenubarSubTrigger>
 | 
			
		||||
                                    {t('menu.file.import_database')}
 | 
			
		||||
                                </MenubarSubTrigger>
 | 
			
		||||
                                <MenubarSubContent>
 | 
			
		||||
                                    <MenubarItem
 | 
			
		||||
                                        onClick={() =>
 | 
			
		||||
                                            openImportDatabaseDialog({
 | 
			
		||||
                                                databaseType:
 | 
			
		||||
                                                    DatabaseType.POSTGRESQL,
 | 
			
		||||
                                            })
 | 
			
		||||
                                        }
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {databaseTypeToLabelMap['postgresql']}
 | 
			
		||||
                                    </MenubarItem>
 | 
			
		||||
                                    <MenubarItem
 | 
			
		||||
                                        onClick={() =>
 | 
			
		||||
                                            openImportDatabaseDialog({
 | 
			
		||||
                                                databaseType:
 | 
			
		||||
                                                    DatabaseType.MYSQL,
 | 
			
		||||
                                            })
 | 
			
		||||
                                        }
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {databaseTypeToLabelMap['mysql']}
 | 
			
		||||
                                    </MenubarItem>
 | 
			
		||||
                                    <MenubarItem
 | 
			
		||||
                                        onClick={() =>
 | 
			
		||||
                                            openImportDatabaseDialog({
 | 
			
		||||
                                                databaseType:
 | 
			
		||||
                                                    DatabaseType.SQL_SERVER,
 | 
			
		||||
                                            })
 | 
			
		||||
                                        }
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {databaseTypeToLabelMap['sql_server']}
 | 
			
		||||
                                    </MenubarItem>
 | 
			
		||||
                                    <MenubarItem
 | 
			
		||||
                                        onClick={() =>
 | 
			
		||||
                                            openImportDatabaseDialog({
 | 
			
		||||
                                                databaseType:
 | 
			
		||||
                                                    DatabaseType.MARIADB,
 | 
			
		||||
                                            })
 | 
			
		||||
                                        }
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {databaseTypeToLabelMap['mariadb']}
 | 
			
		||||
                                    </MenubarItem>
 | 
			
		||||
                                    <MenubarItem
 | 
			
		||||
                                        onClick={() =>
 | 
			
		||||
                                            openImportDatabaseDialog({
 | 
			
		||||
                                                databaseType:
 | 
			
		||||
                                                    DatabaseType.SQLITE,
 | 
			
		||||
                                            })
 | 
			
		||||
                                        }
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {databaseTypeToLabelMap['sqlite']}
 | 
			
		||||
                                    </MenubarItem>
 | 
			
		||||
                                </MenubarSubContent>
 | 
			
		||||
                            </MenubarSub>
 | 
			
		||||
                            <MenubarSeparator />
 | 
			
		||||
                            <MenubarSub>
 | 
			
		||||
                                <MenubarSubTrigger>
 | 
			
		||||
                                    {t('menu.file.export_sql')}
 | 
			
		||||
                                </MenubarSubTrigger>
 | 
			
		||||
                                <MenubarSubContent>
 | 
			
		||||
                                    <MenubarItem
 | 
			
		||||
                                        onClick={() =>
 | 
			
		||||
                                            exportSQL(DatabaseType.GENERIC)
 | 
			
		||||
                                        }
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {databaseTypeToLabelMap['generic']}
 | 
			
		||||
                                    </MenubarItem>
 | 
			
		||||
                                    <MenubarItem
 | 
			
		||||
                                        onClick={() =>
 | 
			
		||||
                                            exportSQL(DatabaseType.POSTGRESQL)
 | 
			
		||||
                                        }
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {databaseTypeToLabelMap['postgresql']}
 | 
			
		||||
                                        <MenubarShortcut className="text-base">
 | 
			
		||||
                                            {emojiAI}
 | 
			
		||||
                                        </MenubarShortcut>
 | 
			
		||||
                                    </MenubarItem>
 | 
			
		||||
                                    <MenubarItem
 | 
			
		||||
                                        onClick={() =>
 | 
			
		||||
                                            exportSQL(DatabaseType.MYSQL)
 | 
			
		||||
                                        }
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {databaseTypeToLabelMap['mysql']}
 | 
			
		||||
                                        <MenubarShortcut className="text-base">
 | 
			
		||||
                                            {emojiAI}
 | 
			
		||||
                                        </MenubarShortcut>
 | 
			
		||||
                                    </MenubarItem>
 | 
			
		||||
                                    <MenubarItem
 | 
			
		||||
                                        onClick={() =>
 | 
			
		||||
                                            exportSQL(DatabaseType.SQL_SERVER)
 | 
			
		||||
                                        }
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {databaseTypeToLabelMap['sql_server']}
 | 
			
		||||
                                        <MenubarShortcut className="text-base">
 | 
			
		||||
                                            {emojiAI}
 | 
			
		||||
                                        </MenubarShortcut>
 | 
			
		||||
                                    </MenubarItem>
 | 
			
		||||
                                    <MenubarItem
 | 
			
		||||
                                        onClick={() =>
 | 
			
		||||
                                            exportSQL(DatabaseType.MARIADB)
 | 
			
		||||
                                        }
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {databaseTypeToLabelMap['mariadb']}
 | 
			
		||||
                                        <MenubarShortcut className="text-base">
 | 
			
		||||
                                            {emojiAI}
 | 
			
		||||
                                        </MenubarShortcut>
 | 
			
		||||
                                    </MenubarItem>
 | 
			
		||||
                                    <MenubarItem
 | 
			
		||||
                                        onClick={() =>
 | 
			
		||||
                                            exportSQL(DatabaseType.SQLITE)
 | 
			
		||||
                                        }
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {databaseTypeToLabelMap['sqlite']}
 | 
			
		||||
                                        <MenubarShortcut className="text-base">
 | 
			
		||||
                                            {emojiAI}
 | 
			
		||||
                                        </MenubarShortcut>
 | 
			
		||||
                                    </MenubarItem>
 | 
			
		||||
                                </MenubarSubContent>
 | 
			
		||||
                            </MenubarSub>
 | 
			
		||||
                            <MenubarSub>
 | 
			
		||||
                                <MenubarSubTrigger>
 | 
			
		||||
                                    {t('menu.file.export_as')}
 | 
			
		||||
                                </MenubarSubTrigger>
 | 
			
		||||
                                <MenubarSubContent>
 | 
			
		||||
                                    <MenubarItem onClick={exportPNG}>
 | 
			
		||||
                                        PNG
 | 
			
		||||
                                    </MenubarItem>
 | 
			
		||||
                                    <MenubarItem onClick={exportJPG}>
 | 
			
		||||
                                        JPG
 | 
			
		||||
                                    </MenubarItem>
 | 
			
		||||
                                    <MenubarItem onClick={exportSVG}>
 | 
			
		||||
                                        SVG
 | 
			
		||||
                                    </MenubarItem>
 | 
			
		||||
                                </MenubarSubContent>
 | 
			
		||||
                            </MenubarSub>
 | 
			
		||||
                            <MenubarSeparator />
 | 
			
		||||
                            <MenubarItem
 | 
			
		||||
                                onClick={() =>
 | 
			
		||||
                                    showAlert({
 | 
			
		||||
                                        title: t('delete_diagram_alert.title'),
 | 
			
		||||
                                        description: t(
 | 
			
		||||
                                            'delete_diagram_alert.description'
 | 
			
		||||
                                        ),
 | 
			
		||||
                                        actionLabel: t(
 | 
			
		||||
                                            'delete_diagram_alert.delete'
 | 
			
		||||
                                        ),
 | 
			
		||||
                                        closeLabel: t(
 | 
			
		||||
                                            'delete_diagram_alert.cancel'
 | 
			
		||||
                                        ),
 | 
			
		||||
                                        onAction: handleDeleteDiagramAction,
 | 
			
		||||
                                    })
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                {t('menu.file.delete_diagram')}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarSeparator />
 | 
			
		||||
                            <MenubarItem>{t('menu.file.exit')}</MenubarItem>
 | 
			
		||||
                        </MenubarContent>
 | 
			
		||||
                    </MenubarMenu>
 | 
			
		||||
                    <MenubarMenu>
 | 
			
		||||
                        <MenubarTrigger>{t('menu.edit.edit')}</MenubarTrigger>
 | 
			
		||||
                        <MenubarContent>
 | 
			
		||||
                            <MenubarItem onClick={undo} disabled={!hasUndo}>
 | 
			
		||||
                                {t('menu.edit.undo')}
 | 
			
		||||
                                <MenubarShortcut>
 | 
			
		||||
                                    {
 | 
			
		||||
                                        keyboardShortcutsForOS[
 | 
			
		||||
                                            KeyboardShortcutAction.UNDO
 | 
			
		||||
                                        ].keyCombinationLabel
 | 
			
		||||
                                    }
 | 
			
		||||
                                </MenubarShortcut>
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem onClick={redo} disabled={!hasRedo}>
 | 
			
		||||
                                {t('menu.edit.redo')}
 | 
			
		||||
                                <MenubarShortcut>
 | 
			
		||||
                                    {
 | 
			
		||||
                                        keyboardShortcutsForOS[
 | 
			
		||||
                                            KeyboardShortcutAction.REDO
 | 
			
		||||
                                        ].keyCombinationLabel
 | 
			
		||||
                                    }
 | 
			
		||||
                                </MenubarShortcut>
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarSeparator />
 | 
			
		||||
                            <MenubarItem
 | 
			
		||||
                                onClick={() =>
 | 
			
		||||
                                    showAlert({
 | 
			
		||||
                                        title: t('clear_diagram_alert.title'),
 | 
			
		||||
                                        description: t(
 | 
			
		||||
                                            'clear_diagram_alert.description'
 | 
			
		||||
                                        ),
 | 
			
		||||
                                        actionLabel: t(
 | 
			
		||||
                                            'clear_diagram_alert.clear'
 | 
			
		||||
                                        ),
 | 
			
		||||
                                        closeLabel: t(
 | 
			
		||||
                                            'clear_diagram_alert.cancel'
 | 
			
		||||
                                        ),
 | 
			
		||||
                                        onAction: clearDiagramData,
 | 
			
		||||
                                    })
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                {t('menu.edit.clear')}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                        </MenubarContent>
 | 
			
		||||
                    </MenubarMenu>
 | 
			
		||||
                    <MenubarMenu>
 | 
			
		||||
                        <MenubarTrigger>{t('menu.view.view')}</MenubarTrigger>
 | 
			
		||||
                        <MenubarContent>
 | 
			
		||||
                            <MenubarItem onClick={showOrHideSidePanel}>
 | 
			
		||||
                                {isSidePanelShowed
 | 
			
		||||
                                    ? t('menu.view.hide_sidebar')
 | 
			
		||||
                                    : t('menu.view.show_sidebar')}
 | 
			
		||||
                                <MenubarShortcut>
 | 
			
		||||
                                    {
 | 
			
		||||
                                        keyboardShortcutsForOS[
 | 
			
		||||
                                            KeyboardShortcutAction
 | 
			
		||||
                                                .TOGGLE_SIDE_PANEL
 | 
			
		||||
                                        ].keyCombinationLabel
 | 
			
		||||
                                    }
 | 
			
		||||
                                </MenubarShortcut>
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarSeparator />
 | 
			
		||||
                            <MenubarItem onClick={showOrHideCardinality}>
 | 
			
		||||
                                {showCardinality
 | 
			
		||||
                                    ? t('menu.view.hide_cardinality')
 | 
			
		||||
                                    : t('menu.view.show_cardinality')}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            {databaseType !== DatabaseType.CLICKHOUSE ? (
 | 
			
		||||
                                <MenubarItem onClick={showOrHideDependencies}>
 | 
			
		||||
                                    {showDependenciesOnCanvas
 | 
			
		||||
                                        ? t('menu.view.hide_dependencies')
 | 
			
		||||
                                        : t('menu.view.show_dependencies')}
 | 
			
		||||
                                </MenubarItem>
 | 
			
		||||
                            ) : null}
 | 
			
		||||
                            <MenubarSeparator />
 | 
			
		||||
                            <MenubarSub>
 | 
			
		||||
                                <MenubarSubTrigger>
 | 
			
		||||
                                    {t('menu.view.zoom_on_scroll')}
 | 
			
		||||
                                </MenubarSubTrigger>
 | 
			
		||||
                                <MenubarSubContent>
 | 
			
		||||
                                    <MenubarCheckboxItem
 | 
			
		||||
                                        checked={scrollAction === 'zoom'}
 | 
			
		||||
                                        onClick={() => setScrollAction('zoom')}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {t('zoom.on')}
 | 
			
		||||
                                    </MenubarCheckboxItem>
 | 
			
		||||
                                    <MenubarCheckboxItem
 | 
			
		||||
                                        checked={scrollAction === 'pan'}
 | 
			
		||||
                                        onClick={() => setScrollAction('pan')}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {t('zoom.off')}
 | 
			
		||||
                                    </MenubarCheckboxItem>
 | 
			
		||||
                                </MenubarSubContent>
 | 
			
		||||
                            </MenubarSub>
 | 
			
		||||
                            <MenubarSeparator />
 | 
			
		||||
                            <MenubarSub>
 | 
			
		||||
                                <MenubarSubTrigger>
 | 
			
		||||
                                    {t('menu.view.theme')}
 | 
			
		||||
                                </MenubarSubTrigger>
 | 
			
		||||
                                <MenubarSubContent>
 | 
			
		||||
                                    <MenubarCheckboxItem
 | 
			
		||||
                                        checked={theme === 'system'}
 | 
			
		||||
                                        onClick={() => setTheme('system')}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {t('theme.system')}
 | 
			
		||||
                                    </MenubarCheckboxItem>
 | 
			
		||||
                                    <MenubarCheckboxItem
 | 
			
		||||
                                        checked={theme === 'light'}
 | 
			
		||||
                                        onClick={() => setTheme('light')}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {t('theme.light')}
 | 
			
		||||
                                    </MenubarCheckboxItem>
 | 
			
		||||
                                    <MenubarCheckboxItem
 | 
			
		||||
                                        checked={theme === 'dark'}
 | 
			
		||||
                                        onClick={() => setTheme('dark')}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {t('theme.dark')}
 | 
			
		||||
                                    </MenubarCheckboxItem>
 | 
			
		||||
                                </MenubarSubContent>
 | 
			
		||||
                            </MenubarSub>
 | 
			
		||||
                        </MenubarContent>
 | 
			
		||||
                    </MenubarMenu>
 | 
			
		||||
 | 
			
		||||
                    <MenubarMenu>
 | 
			
		||||
                        <MenubarTrigger>{t('menu.share.share')}</MenubarTrigger>
 | 
			
		||||
                        <MenubarContent>
 | 
			
		||||
                            <MenubarItem onClick={openExportDiagramDialog}>
 | 
			
		||||
                                {t('menu.share.export_diagram')}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem onClick={openImportDiagramDialog}>
 | 
			
		||||
                                {t('menu.share.import_diagram')}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                        </MenubarContent>
 | 
			
		||||
                    </MenubarMenu>
 | 
			
		||||
 | 
			
		||||
                    <MenubarMenu>
 | 
			
		||||
                        <MenubarTrigger>{t('menu.help.help')}</MenubarTrigger>
 | 
			
		||||
                        <MenubarContent>
 | 
			
		||||
                            <MenubarItem onClick={openChartDBIO}>
 | 
			
		||||
                                {t('menu.help.visit_website')}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem onClick={openJoinDiscord}>
 | 
			
		||||
                                {t('menu.help.join_discord')}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem onClick={openCalendly}>
 | 
			
		||||
                                {t('menu.help.schedule_a_call')}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                        </MenubarContent>
 | 
			
		||||
                    </MenubarMenu>
 | 
			
		||||
                </Menubar>
 | 
			
		||||
                <Menu />
 | 
			
		||||
            </div>
 | 
			
		||||
            {isDesktop ? (
 | 
			
		||||
                <>
 | 
			
		||||
                    <DiagramName />
 | 
			
		||||
                    <div className="hidden flex-1 items-center justify-end gap-2 sm:flex">
 | 
			
		||||
                        {renderGetBuckleButton()}
 | 
			
		||||
                        <LastSaved />
 | 
			
		||||
                        {renderStars()}
 | 
			
		||||
                        <LanguageNav />
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										92
									
								
								src/pages/editor-page/use-diagram-loader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/pages/editor-page/use-diagram-loader.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useConfig } from '@/hooks/use-config';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
 | 
			
		||||
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
 | 
			
		||||
import { useStorage } from '@/hooks/use-storage';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import { useEffect, useRef, useState } from 'react';
 | 
			
		||||
import { useNavigate, useParams } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
export const useDiagramLoader = () => {
 | 
			
		||||
    const [initialDiagram, setInitialDiagram] = useState<Diagram | undefined>();
 | 
			
		||||
    const { diagramId } = useParams<{ diagramId: string }>();
 | 
			
		||||
    const { config } = useConfig();
 | 
			
		||||
    const { loadDiagram, currentDiagram } = useChartDB();
 | 
			
		||||
    const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
 | 
			
		||||
    const { showLoader, hideLoader } = useFullScreenLoader();
 | 
			
		||||
    const { openCreateDiagramDialog, openOpenDiagramDialog } = useDialog();
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
    const { listDiagrams } = useStorage();
 | 
			
		||||
 | 
			
		||||
    const currentDiagramLoadingRef = useRef<string | undefined>(undefined);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!config) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (currentDiagram?.id === diagramId) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const loadDefaultDiagram = async () => {
 | 
			
		||||
            if (diagramId) {
 | 
			
		||||
                setInitialDiagram(undefined);
 | 
			
		||||
                showLoader();
 | 
			
		||||
                resetRedoStack();
 | 
			
		||||
                resetUndoStack();
 | 
			
		||||
                const diagram = await loadDiagram(diagramId);
 | 
			
		||||
                if (!diagram) {
 | 
			
		||||
                    openOpenDiagramDialog({ canClose: false });
 | 
			
		||||
                    hideLoader();
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                setInitialDiagram(diagram);
 | 
			
		||||
                hideLoader();
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            } else if (!diagramId && config.defaultDiagramId) {
 | 
			
		||||
                const diagram = await loadDiagram(config.defaultDiagramId);
 | 
			
		||||
                if (diagram) {
 | 
			
		||||
                    navigate(`/diagrams/${config.defaultDiagramId}`);
 | 
			
		||||
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            const diagrams = await listDiagrams();
 | 
			
		||||
 | 
			
		||||
            if (diagrams.length > 0) {
 | 
			
		||||
                openOpenDiagramDialog({ canClose: false });
 | 
			
		||||
            } else {
 | 
			
		||||
                openCreateDiagramDialog();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            currentDiagramLoadingRef.current === (diagramId ?? '') &&
 | 
			
		||||
            currentDiagramLoadingRef.current !== undefined
 | 
			
		||||
        ) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        currentDiagramLoadingRef.current = diagramId ?? '';
 | 
			
		||||
 | 
			
		||||
        loadDefaultDiagram();
 | 
			
		||||
    }, [
 | 
			
		||||
        diagramId,
 | 
			
		||||
        openCreateDiagramDialog,
 | 
			
		||||
        config,
 | 
			
		||||
        navigate,
 | 
			
		||||
        listDiagrams,
 | 
			
		||||
        loadDiagram,
 | 
			
		||||
        resetRedoStack,
 | 
			
		||||
        resetUndoStack,
 | 
			
		||||
        hideLoader,
 | 
			
		||||
        showLoader,
 | 
			
		||||
        currentDiagram?.id,
 | 
			
		||||
        openOpenDiagramDialog,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    return { initialDiagram };
 | 
			
		||||
};
 | 
			
		||||
@@ -40,7 +40,7 @@ export const examples: Example[] = [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'gaj3scrtaz46ezfmc162ingxf',
 | 
			
		||||
                            name: 'dept_no',
 | 
			
		||||
                            type: { id: 'char', name: 'chat' },
 | 
			
		||||
                            type: { id: 'char', name: 'char' },
 | 
			
		||||
                            primaryKey: true,
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
@@ -63,7 +63,7 @@ export const examples: Example[] = [
 | 
			
		||||
                    indexes: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: '87iu197demih0wymjooqm9dmh',
 | 
			
		||||
                            name: 'PRIMARY',
 | 
			
		||||
                            name: 'dept_no',
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            fieldIds: ['gaj3scrtaz46ezfmc162ingxf'],
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
@@ -99,7 +99,7 @@ export const examples: Example[] = [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'jdw1yrh9xf1i7927gzs9pob2p',
 | 
			
		||||
                            name: 'dept_no',
 | 
			
		||||
                            type: { id: 'char', name: 'chat' },
 | 
			
		||||
                            type: { id: 'char', name: 'char' },
 | 
			
		||||
                            primaryKey: true,
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
@@ -129,14 +129,14 @@ export const examples: Example[] = [
 | 
			
		||||
                    indexes: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'rqb91465yc51xpvd54o5a8d0l',
 | 
			
		||||
                            name: 'PRIMARY',
 | 
			
		||||
                            name: 'emp_no',
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            fieldIds: ['wcgycjif09xrq0ly3txkq6ocu'],
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            id: '8wh6op49abv143qdfjzm211xj',
 | 
			
		||||
                            name: 'PRIMARY',
 | 
			
		||||
                            name: 'dept_no',
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            fieldIds: ['jdw1yrh9xf1i7927gzs9pob2p'],
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
@@ -172,7 +172,7 @@ export const examples: Example[] = [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'v8plj7wq1cly03y178bysft2f',
 | 
			
		||||
                            name: 'dept_no',
 | 
			
		||||
                            type: { id: 'char', name: 'chat' },
 | 
			
		||||
                            type: { id: 'char', name: 'char' },
 | 
			
		||||
                            primaryKey: true,
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
@@ -202,14 +202,14 @@ export const examples: Example[] = [
 | 
			
		||||
                    indexes: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'cbahnbrxaaj7cg29act50izy4',
 | 
			
		||||
                            name: 'PRIMARY',
 | 
			
		||||
                            name: 'emp_no',
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            fieldIds: ['ecx2zbzdc5o54e04aeg7tlg54'],
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'vgxv8rkf4890yf659o2oklffv',
 | 
			
		||||
                            name: 'PRIMARY',
 | 
			
		||||
                            name: 'dept_no',
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            fieldIds: ['v8plj7wq1cly03y178bysft2f'],
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
@@ -297,7 +297,7 @@ export const examples: Example[] = [
 | 
			
		||||
                    indexes: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: '8zg1ccoj4jb4kv6eleih38ni5',
 | 
			
		||||
                            name: 'PRIMARY',
 | 
			
		||||
                            name: 'emp_no',
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            fieldIds: ['04csyx8ds9t3rh93txiqs4dm4'],
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
@@ -366,14 +366,14 @@ export const examples: Example[] = [
 | 
			
		||||
                    indexes: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'nky2wepp8yr5g6rzvnbta1hxb',
 | 
			
		||||
                            name: 'PRIMARY',
 | 
			
		||||
                            name: 'emp_no',
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            fieldIds: ['b8c9v5vtpbnt5tjzcd3iat85f'],
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'w40nnsrsnlz7z7vycs4yf0s8d',
 | 
			
		||||
                            name: 'PRIMARY',
 | 
			
		||||
                            name: 'from_date',
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            fieldIds: ['0s10erufqpl6y3hpqmvbcneol'],
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
@@ -433,21 +433,21 @@ export const examples: Example[] = [
 | 
			
		||||
                    indexes: [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'ijhmb7tq6i4fd72ndvotnwo45',
 | 
			
		||||
                            name: 'PRIMARY',
 | 
			
		||||
                            name: 'emp_no',
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            fieldIds: ['hr2gdoc0wtwvs4pfqo6m0fwc3'],
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'wgneqfte0nq7d5vzed2hcqie6',
 | 
			
		||||
                            name: 'PRIMARY',
 | 
			
		||||
                            name: 'title',
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            fieldIds: ['5evr59tury66sayiu59esoc61'],
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
                        },
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'jbe9t9adhluqy8d3i7w1vgygd',
 | 
			
		||||
                            name: 'PRIMARY',
 | 
			
		||||
                            name: 'from_date',
 | 
			
		||||
                            unique: true,
 | 
			
		||||
                            fieldIds: ['0vs1nqvrb6t53niz5ns2eskre'],
 | 
			
		||||
                            createdAt: Date.now(),
 | 
			
		||||
@@ -476,7 +476,7 @@ export const examples: Example[] = [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'fv7o6txqvmy2349aq3pg0hnkm',
 | 
			
		||||
                            name: 'dept_no',
 | 
			
		||||
                            type: { id: 'char', name: 'chat' },
 | 
			
		||||
                            type: { id: 'char', name: 'char' },
 | 
			
		||||
                            primaryKey: false,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
@@ -2465,7 +2465,7 @@ export const examples: Example[] = [
 | 
			
		||||
                        {
 | 
			
		||||
                            id: 'lng1oxspuc1hsny8x0sti0o72',
 | 
			
		||||
                            name: 'name',
 | 
			
		||||
                            type: { id: 'char', name: 'chat' },
 | 
			
		||||
                            type: { id: 'char', name: 'char' },
 | 
			
		||||
                            primaryKey: false,
 | 
			
		||||
                            unique: false,
 | 
			
		||||
                            nullable: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ export const employeeDb: Template = {
 | 
			
		||||
                    {
 | 
			
		||||
                        id: 'gaj3scrtaz46ezfmc162ingxf',
 | 
			
		||||
                        name: 'dept_no',
 | 
			
		||||
                        type: { id: 'char', name: 'chat' },
 | 
			
		||||
                        type: { id: 'char', name: 'char' },
 | 
			
		||||
                        primaryKey: true,
 | 
			
		||||
                        unique: true,
 | 
			
		||||
                        nullable: false,
 | 
			
		||||
@@ -88,7 +88,7 @@ export const employeeDb: Template = {
 | 
			
		||||
                    {
 | 
			
		||||
                        id: 'jdw1yrh9xf1i7927gzs9pob2p',
 | 
			
		||||
                        name: 'dept_no',
 | 
			
		||||
                        type: { id: 'char', name: 'chat' },
 | 
			
		||||
                        type: { id: 'char', name: 'char' },
 | 
			
		||||
                        primaryKey: true,
 | 
			
		||||
                        unique: true,
 | 
			
		||||
                        nullable: false,
 | 
			
		||||
@@ -161,7 +161,7 @@ export const employeeDb: Template = {
 | 
			
		||||
                    {
 | 
			
		||||
                        id: 'v8plj7wq1cly03y178bysft2f',
 | 
			
		||||
                        name: 'dept_no',
 | 
			
		||||
                        type: { id: 'char', name: 'chat' },
 | 
			
		||||
                        type: { id: 'char', name: 'char' },
 | 
			
		||||
                        primaryKey: true,
 | 
			
		||||
                        unique: true,
 | 
			
		||||
                        nullable: false,
 | 
			
		||||
@@ -465,7 +465,7 @@ export const employeeDb: Template = {
 | 
			
		||||
                    {
 | 
			
		||||
                        id: 'fv7o6txqvmy2349aq3pg0hnkm',
 | 
			
		||||
                        name: 'dept_no',
 | 
			
		||||
                        type: { id: 'char', name: 'chat' },
 | 
			
		||||
                        type: { id: 'char', name: 'char' },
 | 
			
		||||
                        primaryKey: false,
 | 
			
		||||
                        unique: false,
 | 
			
		||||
                        nullable: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ export const visualNovelDb: Template = {
 | 
			
		||||
                    {
 | 
			
		||||
                        id: 'gaj3scrtaz46ezfmc162ingxf',
 | 
			
		||||
                        name: 'dept_no',
 | 
			
		||||
                        type: { id: 'char', name: 'chat' },
 | 
			
		||||
                        type: { id: 'char', name: 'char' },
 | 
			
		||||
                        primaryKey: true,
 | 
			
		||||
                        unique: true,
 | 
			
		||||
                        nullable: false,
 | 
			
		||||
@@ -89,7 +89,7 @@ export const visualNovelDb: Template = {
 | 
			
		||||
                    {
 | 
			
		||||
                        id: 'jdw1yrh9xf1i7927gzs9pob2p',
 | 
			
		||||
                        name: 'dept_no',
 | 
			
		||||
                        type: { id: 'char', name: 'chat' },
 | 
			
		||||
                        type: { id: 'char', name: 'char' },
 | 
			
		||||
                        primaryKey: true,
 | 
			
		||||
                        unique: true,
 | 
			
		||||
                        nullable: false,
 | 
			
		||||
@@ -162,7 +162,7 @@ export const visualNovelDb: Template = {
 | 
			
		||||
                    {
 | 
			
		||||
                        id: 'v8plj7wq1cly03y178bysft2f',
 | 
			
		||||
                        name: 'dept_no',
 | 
			
		||||
                        type: { id: 'char', name: 'chat' },
 | 
			
		||||
                        type: { id: 'char', name: 'char' },
 | 
			
		||||
                        primaryKey: true,
 | 
			
		||||
                        unique: true,
 | 
			
		||||
                        nullable: false,
 | 
			
		||||
@@ -466,7 +466,7 @@ export const visualNovelDb: Template = {
 | 
			
		||||
                    {
 | 
			
		||||
                        id: 'fv7o6txqvmy2349aq3pg0hnkm',
 | 
			
		||||
                        name: 'dept_no',
 | 
			
		||||
                        type: { id: 'char', name: 'chat' },
 | 
			
		||||
                        type: { id: 'char', name: 'char' },
 | 
			
		||||
                        primaryKey: false,
 | 
			
		||||
                        unique: false,
 | 
			
		||||
                        nullable: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
const defaultTheme = require('tailwindcss/defaultTheme');
 | 
			
		||||
import defaultTheme from 'tailwindcss/defaultTheme';
 | 
			
		||||
 | 
			
		||||
/** @type {import('tailwindcss').Config} */
 | 
			
		||||
module.exports = {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user