Compare commits
	
		
			83 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					53f443d452 | ||
| 
						 | 
					134c62f931 | ||
| 
						 | 
					4bb4766e1a | ||
| 
						 | 
					24db32369a | ||
| 
						 | 
					e77ee60a5b | ||
| 
						 | 
					6c65c2e9cc | ||
| 
						 | 
					70f545f78b | ||
| 
						 | 
					fb702c87ce | ||
| 
						 | 
					eaa067814f | ||
| 
						 | 
					667685ed0f | ||
| 
						 | 
					2940431efa | ||
| 
						 | 
					94ec43b608 | ||
| 
						 | 
					a2efed803f | ||
| 
						 | 
					8749591be0 | ||
| 
						 | 
					c5e0ea6fa4 | ||
| 
						 | 
					ab07da0b03 | ||
| 
						 | 
					8397bef392 | ||
| 
						 | 
					7c3c62860e | ||
| 
						 | 
					76ba4ce4c5 | ||
| 
						 | 
					0c0fad719f | ||
| 
						 | 
					b75c6fe4e7 | ||
| 
						 | 
					d9fcbeec72 | ||
| 
						 | 
					5d79721b6d | ||
| 
						 | 
					4be3592cf4 | ||
| 
						 | 
					b4cdcbbbd7 | ||
| 
						 | 
					e9c7f4be06 | ||
| 
						 | 
					0530f9172c | ||
| 
						 | 
					e1e55c4b2a | ||
| 
						 | 
					c6f7ff70f8 | ||
| 
						 | 
					02aaabdc4e | ||
| 
						 | 
					0f673947af | ||
| 
						 | 
					f35f62fdf3 | ||
| 
						 | 
					68474e75d5 | ||
| 
						 | 
					eaf75cedb0 | ||
| 
						 | 
					fe8b9f9e91 | ||
| 
						 | 
					07d3745747 | ||
| 
						 | 
					44cf5ca264 | ||
| 
						 | 
					44d10c2390 | ||
| 
						 | 
					9698821828 | ||
| 
						 | 
					9f8500fc7e | ||
| 
						 | 
					e5dbbf2eaa | ||
| 
						 | 
					959e5402b8 | ||
| 
						 | 
					492c9324d2 | ||
| 
						 | 
					42c159605d | ||
| 
						 | 
					78c427f38e | ||
| 
						 | 
					bae74d1693 | ||
| 
						 | 
					123f40f39e | ||
| 
						 | 
					e3129cec74 | ||
| 
						 | 
					5508c1e084 | ||
| 
						 | 
					9f2893319a | ||
| 
						 | 
					125a39fb5b | ||
| 
						 | 
					4ca1832732 | ||
| 
						 | 
					3609bfea4d | ||
| 
						 | 
					94a5d84fae | ||
| 
						 | 
					85e691fcbe | ||
| 
						 | 
					709ccff8fa | ||
| 
						 | 
					6c7eb4609d | ||
| 
						 | 
					2c69b08eae | ||
| 
						 | 
					84e7591d05 | ||
| 
						 | 
					545e8578c9 | ||
| 
						 | 
					f1d073d053 | ||
| 
						 | 
					20b3396ec2 | ||
| 
						 | 
					b305be82ae | ||
| 
						 | 
					1430d2c236 | ||
| 
						 | 
					acf8ade23c | ||
| 
						 | 
					aa884b49ce | ||
| 
						 | 
					acb736e44f | ||
| 
						 | 
					180886c588 | ||
| 
						 | 
					e993476fad | ||
| 
						 | 
					efaddeebb4 | ||
| 
						 | 
					93f623a13a | ||
| 
						 | 
					87a40cff61 | ||
| 
						 | 
					f00c9b9a03 | ||
| 
						 | 
					20b2ae436c | ||
| 
						 | 
					820a4640da | ||
| 
						 | 
					0193853035 | ||
| 
						 | 
					b40344675e | ||
| 
						 | 
					df7e687f61 | ||
| 
						 | 
					ad10d26f13 | ||
| 
						 | 
					588c64b380 | ||
| 
						 | 
					3d3efc5e82 | ||
| 
						 | 
					d8a20ebbd9 | ||
| 
						 | 
					ebce8827ea | 
							
								
								
									
										99
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						@@ -1,5 +1,104 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## [1.3.1](https://github.com/chartdb/chartdb/compare/v1.3.0...v1.3.1) (2024-11-26)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* **docker:** make OPENAI_API_KEY optional in docker run ([#448](https://github.com/chartdb/chartdb/issues/448)) ([4bb4766](https://github.com/chartdb/chartdb/commit/4bb4766e1ac8d69e138668eb8a46de5affe62ceb))
 | 
			
		||||
 | 
			
		||||
## [1.3.0](https://github.com/chartdb/chartdb/compare/v1.2.0...v1.3.0) (2024-11-25)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* **side panel:** collapsible side panel on desktop view + keyboard shortcut ([#439](https://github.com/chartdb/chartdb/issues/439)) ([70f545f](https://github.com/chartdb/chartdb/commit/70f545f78bab9c510a6e5936fa5b259b806b6c69))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* **dialogs:** fix height of dialogs for small screens ([#440](https://github.com/chartdb/chartdb/issues/440)) ([667685e](https://github.com/chartdb/chartdb/commit/667685ed0f6a8cc61ae86b3ba60e052fbe6a9e1a))
 | 
			
		||||
* **drawer:** set fix min size ([#429](https://github.com/chartdb/chartdb/issues/429)) ([c5e0ea6](https://github.com/chartdb/chartdb/commit/c5e0ea6fa4017666ff3bc1e3071c487df48afd3d))
 | 
			
		||||
* **export-sql:** add unique to export script ([#422](https://github.com/chartdb/chartdb/issues/422)) ([b75c6fe](https://github.com/chartdb/chartdb/commit/b75c6fe4e78f3e2058be680f2fa0442db3b4a6bd))
 | 
			
		||||
* fix layout warnings ([#434](https://github.com/chartdb/chartdb/issues/434)) ([94ec43b](https://github.com/chartdb/chartdb/commit/94ec43b60845bb8c3592ce1b1450ca0171a53f99))
 | 
			
		||||
* **i18n:** add bahasa indonesia translation ([#331](https://github.com/chartdb/chartdb/issues/331)) ([ab07da0](https://github.com/chartdb/chartdb/commit/ab07da0b031f0d4050ff6b44ddcb94cb6c0010b6))
 | 
			
		||||
* **i18n:** add missing type to vi.ts ([#444](https://github.com/chartdb/chartdb/issues/444)) ([e77ee60](https://github.com/chartdb/chartdb/commit/e77ee60a5b47e0854d11b0ee2f16d6956737d0ff))
 | 
			
		||||
* **i18n:** Add Telugu Language ([#352](https://github.com/chartdb/chartdb/issues/352)) ([8749591](https://github.com/chartdb/chartdb/commit/8749591be036e131de4bfeed1e6eece8d62980dd))
 | 
			
		||||
* **i18n:** add Turkish translations ([#315](https://github.com/chartdb/chartdb/issues/315)) ([d9fcbee](https://github.com/chartdb/chartdb/commit/d9fcbeec726b7bde9f7d202bf09dc6b617e3ad80))
 | 
			
		||||
* **i18n:** add Vietnamese translations ([#435](https://github.com/chartdb/chartdb/issues/435)) ([6c65c2e](https://github.com/chartdb/chartdb/commit/6c65c2e9cce600b9778b84ce5b5f1625dc6f1a58))
 | 
			
		||||
* **i18n:** Translating to Gujarati language ([#433](https://github.com/chartdb/chartdb/issues/433)) ([2940431](https://github.com/chartdb/chartdb/commit/2940431efa1a6aa54d80c61d5e05f0ad47cd67ba))
 | 
			
		||||
* **i18n:** Translation of the export error into Russian ([#418](https://github.com/chartdb/chartdb/issues/418)) ([7c3c628](https://github.com/chartdb/chartdb/commit/7c3c62860efc98d3aabf2132a79ac945ffc8315a))
 | 
			
		||||
* **i18n:** update korean for 1.2.0 ([#419](https://github.com/chartdb/chartdb/issues/419)) ([8397bef](https://github.com/chartdb/chartdb/commit/8397bef3924610d94661aae99c55ba4fa376a186))
 | 
			
		||||
* **import script:** remove double quotes ([#442](https://github.com/chartdb/chartdb/issues/442)) ([fb702c8](https://github.com/chartdb/chartdb/commit/fb702c87ce5254bf6e0209c692305f5086956090))
 | 
			
		||||
* **share:** fix export to handle broken indexes & relationships ([#416](https://github.com/chartdb/chartdb/issues/416)) ([4be3592](https://github.com/chartdb/chartdb/commit/4be3592cf4d160be83ddf1db01ffe9afdef119fa))
 | 
			
		||||
* **templates:** add Five more templates (bouncer, cabot, feedbin, Pythonic, flarum, freescout) ([#441](https://github.com/chartdb/chartdb/issues/441)) ([eaa0678](https://github.com/chartdb/chartdb/commit/eaa067814fd96fcc1ee10488ee747a71a8e8ec7a))
 | 
			
		||||
 | 
			
		||||
## [1.2.0](https://github.com/chartdb/chartdb/compare/v1.1.0...v1.2.0) (2024-11-17)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* **duplicate table:** duplicate table from the canvas and sidebar ([#404](https://github.com/chartdb/chartdb/issues/404)) ([44cf5ca](https://github.com/chartdb/chartdb/commit/44cf5ca264f52851f2dffb51a752a52b6fa7ec8d))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* **AI exports:** add cahching layer to SQL exports ([#390](https://github.com/chartdb/chartdb/issues/390)) ([e5dbbf2](https://github.com/chartdb/chartdb/commit/e5dbbf2eaab6d80a531d451211b6f5a415bc7ce3))
 | 
			
		||||
* **canvas:** fix auto zoom on diagram load ([#395](https://github.com/chartdb/chartdb/issues/395)) ([492c932](https://github.com/chartdb/chartdb/commit/492c9324d27b561470c4967ce2e99f82eec467d8))
 | 
			
		||||
* **dockerfile:** support SPA refresh to resolve nginx return 404 ([#384](https://github.com/chartdb/chartdb/issues/384)) ([eaf75ce](https://github.com/chartdb/chartdb/commit/eaf75cedb0e024236c7684bb533856d7f80074da))
 | 
			
		||||
* **docs:** update license reference ([#403](https://github.com/chartdb/chartdb/issues/403)) ([44d10c2](https://github.com/chartdb/chartdb/commit/44d10c23907165288951a9d2ec3165ad23f81c61))
 | 
			
		||||
* **export image:** Add support for displaying cardinality relationships + background ([#407](https://github.com/chartdb/chartdb/issues/407)) ([68474e7](https://github.com/chartdb/chartdb/commit/68474e75d56ed4b4b445cc9b7f59cca96a4ca5db))
 | 
			
		||||
* **i18n:** add Nepali translations ([#406](https://github.com/chartdb/chartdb/issues/406)) ([e1e55c4](https://github.com/chartdb/chartdb/commit/e1e55c4b2ac7755b0810dc1f21da44903fe68a54))
 | 
			
		||||
* **i18n:** change language keeps selected language also after refreshing the page ([#409](https://github.com/chartdb/chartdb/issues/409)) ([f35f62f](https://github.com/chartdb/chartdb/commit/f35f62fdf38ca84065f171a31b80aa8123b1d8b9))
 | 
			
		||||
* **i18n:** Create Translations in Marathi language ([#266](https://github.com/chartdb/chartdb/issues/266)) ([c6f7ff7](https://github.com/chartdb/chartdb/commit/c6f7ff70f841efb9cf1766338f409fe0ea7bb998))
 | 
			
		||||
* **i18n:** fix language nav: close when lang selected, hide tooltip when lang selected ([#411](https://github.com/chartdb/chartdb/issues/411)) ([02aaabd](https://github.com/chartdb/chartdb/commit/02aaabdc4e9b1570d81ff03fe1e6da0307f22999))
 | 
			
		||||
* **templates:** add five more templates (Sylius, Monica, Attendize, SaaS Pegasus & BookStack) ([#408](https://github.com/chartdb/chartdb/issues/408)) ([0f67394](https://github.com/chartdb/chartdb/commit/0f673947af469e86f70737427ac8fb3c2420d1a2))
 | 
			
		||||
* **templates:** add six more templates (ticketit, snipe-it, refinerycms, comfortable-mexican-sofa, buddypress, lobsters) ([#402](https://github.com/chartdb/chartdb/issues/402)) ([07d3745](https://github.com/chartdb/chartdb/commit/07d374574775d132e1cba0908c47dcbbd6cd2c3f))
 | 
			
		||||
* **templates:** fix cloned indexes from a template ([#398](https://github.com/chartdb/chartdb/issues/398)) ([9f8500f](https://github.com/chartdb/chartdb/commit/9f8500fc7e36e6a819ecb9029f263d80eac88279))
 | 
			
		||||
* **templates:** fix tags urls ([#405](https://github.com/chartdb/chartdb/issues/405)) ([fe8b9f9](https://github.com/chartdb/chartdb/commit/fe8b9f9e91481d8a3272113b6f4be4da8d61ad04))
 | 
			
		||||
* **templates:** tag urls lowercase to support browsers ([#397](https://github.com/chartdb/chartdb/issues/397)) ([959e540](https://github.com/chartdb/chartdb/commit/959e5402b8c112fae6243ce9283947057506c128))
 | 
			
		||||
 | 
			
		||||
## [1.1.0](https://github.com/chartdb/chartdb/compare/v1.0.1...v1.1.0) (2024-11-13)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* **add templates:** add five more templates (laravel, django, twitter… ([#371](https://github.com/chartdb/chartdb/issues/371)) ([20b3396](https://github.com/chartdb/chartdb/commit/20b3396ec2afff09ca8bcdd91f5c6284c93cd959))
 | 
			
		||||
* **canvas:** Added Snap to grid functionality. Toggle/hold shift to enable snap to grid. ([#373](https://github.com/chartdb/chartdb/issues/373)) ([6c7eb46](https://github.com/chartdb/chartdb/commit/6c7eb4609d8466278de30317665929ec529c1f94))
 | 
			
		||||
* **share:** add sharing capabilities to import and export diagrams ([#365](https://github.com/chartdb/chartdb/issues/365)) ([94a5d84](https://github.com/chartdb/chartdb/commit/94a5d84fae819b0de6c1e471d1aad16dc8f39dd6))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* **bundle:** fix bundle size ([#382](https://github.com/chartdb/chartdb/issues/382)) ([4ca1832](https://github.com/chartdb/chartdb/commit/4ca18327324106950f0d1af851b9b74379b67b7b))
 | 
			
		||||
* **dockerfile:** support openai key in docker build ([#366](https://github.com/chartdb/chartdb/issues/366)) ([545e857](https://github.com/chartdb/chartdb/commit/545e8578c9e8aa71696f6aa8bec81cacaa602c2d))
 | 
			
		||||
* **i18n:** add korean ([#362](https://github.com/chartdb/chartdb/issues/362)) ([b305be8](https://github.com/chartdb/chartdb/commit/b305be82aee00994ef576ca6fd62d72dd491f771))
 | 
			
		||||
* **i18n:** Add simplified chinese ([#385](https://github.com/chartdb/chartdb/issues/385)) ([9f28933](https://github.com/chartdb/chartdb/commit/9f2893319a1a2aed9a7c03d15e25a17ab37c2465))
 | 
			
		||||
* **i18n:** Added Russian language ([#376](https://github.com/chartdb/chartdb/issues/376)) ([2c69b08](https://github.com/chartdb/chartdb/commit/2c69b08eaea6b86ce0c1ddb18a23e22629198bf5))
 | 
			
		||||
* **i18n:** added traditional Chinese language translation ([#356](https://github.com/chartdb/chartdb/issues/356)) ([123f40f](https://github.com/chartdb/chartdb/commit/123f40f39e703ad612635964af530ac72c387d3c))
 | 
			
		||||
* **i18n:** Fixed part of RU lang introduced in [#365](https://github.com/chartdb/chartdb/issues/365) feat(share) ([#380](https://github.com/chartdb/chartdb/issues/380)) ([5508c1e](https://github.com/chartdb/chartdb/commit/5508c1e084e0ee24d1a54f721f760b9fc14df107))
 | 
			
		||||
* **i18n:** french translation update - share menu ([#391](https://github.com/chartdb/chartdb/issues/391)) ([e3129ce](https://github.com/chartdb/chartdb/commit/e3129cec744d18f09953544d9e74cd5adc4e8afb))
 | 
			
		||||
* **import json:** for Check Script Result, default with quotes ([#358](https://github.com/chartdb/chartdb/issues/358)) ([1430d2c](https://github.com/chartdb/chartdb/commit/1430d2c2365b7b74e36b8ff9d32a163d7437448a))
 | 
			
		||||
* improve title name edit interaction ([#367](https://github.com/chartdb/chartdb/issues/367)) ([84e7591](https://github.com/chartdb/chartdb/commit/84e7591d0586b9a457f31737c6e363ef41574142))
 | 
			
		||||
* **share:** add loader to the export ([#381](https://github.com/chartdb/chartdb/issues/381)) ([3609bfe](https://github.com/chartdb/chartdb/commit/3609bfea4d4c78b03711ff8d721b4e67bf82185a))
 | 
			
		||||
* **sql export:** make loading for export interactive ([#388](https://github.com/chartdb/chartdb/issues/388)) ([125a39f](https://github.com/chartdb/chartdb/commit/125a39fb5be803f0e6db0b68fb5bc8e290fa8dae))
 | 
			
		||||
* **templates:** change the template url to be database instead of db ([#374](https://github.com/chartdb/chartdb/issues/374)) ([f1d073d](https://github.com/chartdb/chartdb/commit/f1d073d05383955da6f60a9a66ed2be879b103e4))
 | 
			
		||||
* **templates:** fix issue with double-clone on localhost ([#394](https://github.com/chartdb/chartdb/issues/394)) ([78c427f](https://github.com/chartdb/chartdb/commit/78c427f38e5c64fc340d13ceb2153c2b85db437e))
 | 
			
		||||
 | 
			
		||||
## [1.0.1](https://github.com/chartdb/chartdb/compare/v1.0.0...v1.0.1) (2024-11-06)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* **offline:** add support when running on isolated network ([#359](https://github.com/chartdb/chartdb/issues/359)) ([aa884b4](https://github.com/chartdb/chartdb/commit/aa884b49ce16d70f67881bdc940993c1fe901796))
 | 
			
		||||
* open default diagram after deleting current diagram ([#350](https://github.com/chartdb/chartdb/issues/350)) ([87a40cf](https://github.com/chartdb/chartdb/commit/87a40cff615b04b678642ba2d6e097c38b26d239))
 | 
			
		||||
* **select-box:** allow using tab & space to show choices ([#336](https://github.com/chartdb/chartdb/issues/336)) ([93f623a](https://github.com/chartdb/chartdb/commit/93f623a13a61e9143638fbe7e8346f07e37a26b2))
 | 
			
		||||
* **smart query:** import postgres FKs ([#357](https://github.com/chartdb/chartdb/issues/357)) ([acb736e](https://github.com/chartdb/chartdb/commit/acb736e44fd50d29a85b4eff42e20780aef710ed))
 | 
			
		||||
* **templates:** add two more templates (Airbnb, Wordpress) ([#317](https://github.com/chartdb/chartdb/issues/317)) ([ebce882](https://github.com/chartdb/chartdb/commit/ebce8827eab049eefa0eebcb0ec2540698bc0e15))
 | 
			
		||||
* **templates:** align database icon ([#351](https://github.com/chartdb/chartdb/issues/351)) ([efaddee](https://github.com/chartdb/chartdb/commit/efaddeebb4f24235d82f4e2bf7423fbf48b97187))
 | 
			
		||||
* **template:** separator in case of empty url ([#355](https://github.com/chartdb/chartdb/issues/355)) ([180886c](https://github.com/chartdb/chartdb/commit/180886c5882f2329c797fc284b255012d21f5b5c))
 | 
			
		||||
* **templates:** fetch templates data from router ([#321](https://github.com/chartdb/chartdb/issues/321)) ([d8a20eb](https://github.com/chartdb/chartdb/commit/d8a20ebbd9118989690a40fcd3aa59fb156b446f))
 | 
			
		||||
 | 
			
		||||
## 1.0.0 (2024-11-04)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ To get started:
 | 
			
		||||
 | 
			
		||||
### License
 | 
			
		||||
 | 
			
		||||
By contributing, you agree that your work will be licensed under ChartDB's [license](https://github.com/chartdb/chartdb/blob/main/LICENSE.md).
 | 
			
		||||
By contributing, you agree that your work will be licensed under ChartDB's [license](https://github.com/chartdb/chartdb/blob/main/LICENSE).
 | 
			
		||||
 | 
			
		||||
## Questions?
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
FROM node:22-alpine AS builder
 | 
			
		||||
 | 
			
		||||
ARG VITE_OPENAI_API_KEY
 | 
			
		||||
 | 
			
		||||
WORKDIR /usr/src/app
 | 
			
		||||
 | 
			
		||||
COPY package.json package-lock.json ./
 | 
			
		||||
@@ -14,8 +16,11 @@ RUN npm run build
 | 
			
		||||
FROM nginx:stable-alpine AS production
 | 
			
		||||
 | 
			
		||||
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
CMD ["nginx", "-g", "daemon off;"]
 | 
			
		||||
ENTRYPOINT ["/entrypoint.sh"]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								README.md
									
									
									
									
									
								
							
							
						
						@@ -15,8 +15,9 @@
 | 
			
		||||
 | 
			
		||||
<h3 align="center">
 | 
			
		||||
  <a href="https://discord.gg/QeFwyWSKwC">Community</a>  •
 | 
			
		||||
  <a href="https://www.chartdb.io">Website</a>  •
 | 
			
		||||
  <a href="https://app.chartdb.io/examples">Demo</a>
 | 
			
		||||
  <a href="https://www.chartdb.io?ref=github_readme">Website</a>  •
 | 
			
		||||
  <a href="https://chartdb.io/templates?ref=github_readme">Examples</a>  •
 | 
			
		||||
  <a href="https://app.chartdb.io?ref=github_readme">Demo</a>
 | 
			
		||||
</h3>
 | 
			
		||||
 | 
			
		||||
<h4 align="center">
 | 
			
		||||
@@ -38,7 +39,7 @@
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<p align="center">
 | 
			
		||||
  <img width='700px' src="./public/ChartDB.png">
 | 
			
		||||
  <img width='700px' src="./public/chartdb.png">
 | 
			
		||||
</p>
 | 
			
		||||
 | 
			
		||||
### 🎉 ChartDB
 | 
			
		||||
@@ -71,7 +72,7 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif
 | 
			
		||||
 | 
			
		||||
## Getting Started
 | 
			
		||||
 | 
			
		||||
Use the [cloud version](https://app.chartdb.io/) or deploy locally:
 | 
			
		||||
Use the [cloud version](https://app.chartdb.io?ref=github_readme_2) or deploy locally:
 | 
			
		||||
 | 
			
		||||
### How To Use
 | 
			
		||||
 | 
			
		||||
@@ -95,9 +96,13 @@ VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> npm run build
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### Running the Docker Container
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
docker build -t chartdb .
 | 
			
		||||
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 ghcr.io/chartdb/chartdb:latest
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### Build & run Docker image locally
 | 
			
		||||
```bash
 | 
			
		||||
docker build -t chartdb . (If you want AI capabilities, use `docker build --build-arg VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -t chartdb .`)
 | 
			
		||||
docker run -p 8080:80 chartdb
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
@@ -105,7 +110,7 @@ Open your browser and navigate to `http://localhost:8080`.
 | 
			
		||||
 | 
			
		||||
## Try it on our website
 | 
			
		||||
 | 
			
		||||
1. Go to [ChartDB.io](https://chartdb.io)
 | 
			
		||||
1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2)
 | 
			
		||||
2. Click "Go to app"
 | 
			
		||||
3. Choose the database that you are using.
 | 
			
		||||
4. Take the magic query and run it in your database.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										20
									
								
								default.conf.template
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,20 @@
 | 
			
		||||
server {
 | 
			
		||||
    listen       80;
 | 
			
		||||
    listen  [::]:80;
 | 
			
		||||
 | 
			
		||||
    location / {
 | 
			
		||||
        root   /usr/share/nginx/html;
 | 
			
		||||
        index  index.html index.htm;
 | 
			
		||||
	    try_files  $uri $uri/ /index.html;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    location /config.js {
 | 
			
		||||
        default_type application/javascript;
 | 
			
		||||
        return 200 "window.env = { OPENAI_API_KEY: \"$OPENAI_API_KEY\" };";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    error_page   500 502 503 504  /50x.html;
 | 
			
		||||
    location = /50x.html {
 | 
			
		||||
        root   /usr/share/nginx/html;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								entrypoint.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +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
 | 
			
		||||
 | 
			
		||||
# Start Nginx
 | 
			
		||||
nginx -g "daemon off;"
 | 
			
		||||
@@ -12,6 +12,7 @@
 | 
			
		||||
            href="https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100..900;1,100..900&display=swap"
 | 
			
		||||
            rel="stylesheet"
 | 
			
		||||
        />
 | 
			
		||||
        <script src="/config.js"></script>
 | 
			
		||||
        <script
 | 
			
		||||
            src="https://cdn.usefathom.com/script.js"
 | 
			
		||||
            data-site="PRHIVBNN"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						@@ -1,12 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "chartdb",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "version": "1.3.1",
 | 
			
		||||
  "lockfileVersion": 3,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "chartdb",
 | 
			
		||||
      "version": "1.0.0",
 | 
			
		||||
      "version": "1.3.1",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@ai-sdk/openai": "^0.0.51",
 | 
			
		||||
        "@dnd-kit/sortable": "^8.0.0",
 | 
			
		||||
@@ -44,6 +44,7 @@
 | 
			
		||||
        "fast-deep-equal": "^3.1.3",
 | 
			
		||||
        "html-to-image": "^1.11.11",
 | 
			
		||||
        "i18next": "^23.14.0",
 | 
			
		||||
        "i18next-browser-languagedetector": "^8.0.0",
 | 
			
		||||
        "lucide-react": "^0.441.0",
 | 
			
		||||
        "monaco-editor": "^0.52.0",
 | 
			
		||||
        "nanoid": "^5.0.7",
 | 
			
		||||
@@ -60,7 +61,8 @@
 | 
			
		||||
        "tailwind-merge": "^2.4.0",
 | 
			
		||||
        "tailwindcss-animate": "^1.0.7",
 | 
			
		||||
        "timeago-react": "^3.0.6",
 | 
			
		||||
        "vaul": "^0.9.1"
 | 
			
		||||
        "vaul": "^0.9.1",
 | 
			
		||||
        "zod": "^3.23.8"
 | 
			
		||||
      },
 | 
			
		||||
      "devDependencies": {
 | 
			
		||||
        "@types/node": "^22.1.0",
 | 
			
		||||
@@ -6754,6 +6756,15 @@
 | 
			
		||||
        "@babel/runtime": "^7.23.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/i18next-browser-languagedetector": {
 | 
			
		||||
      "version": "8.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/runtime": "^7.23.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/ignore": {
 | 
			
		||||
      "version": "5.3.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
 | 
			
		||||
@@ -10539,7 +10550,6 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
 | 
			
		||||
      "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "peer": true,
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "url": "https://github.com/sponsors/colinhacks"
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										188
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -1,95 +1,97 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "chartdb",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "vite",
 | 
			
		||||
    "build": "npm run lint && tsc -b && vite build",
 | 
			
		||||
    "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
 | 
			
		||||
    "lint:fix": "npm run lint -- --fix",
 | 
			
		||||
    "preview": "vite preview",
 | 
			
		||||
    "prepare": "husky"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@ai-sdk/openai": "^0.0.51",
 | 
			
		||||
    "@dnd-kit/sortable": "^8.0.0",
 | 
			
		||||
    "@monaco-editor/react": "^4.6.0",
 | 
			
		||||
    "@radix-ui/react-accordion": "^1.2.0",
 | 
			
		||||
    "@radix-ui/react-alert-dialog": "^1.1.1",
 | 
			
		||||
    "@radix-ui/react-avatar": "^1.1.0",
 | 
			
		||||
    "@radix-ui/react-checkbox": "^1.1.1",
 | 
			
		||||
    "@radix-ui/react-collapsible": "^1.1.0",
 | 
			
		||||
    "@radix-ui/react-context-menu": "^2.2.1",
 | 
			
		||||
    "@radix-ui/react-dialog": "^1.1.1",
 | 
			
		||||
    "@radix-ui/react-dropdown-menu": "^2.1.1",
 | 
			
		||||
    "@radix-ui/react-hover-card": "^1.1.1",
 | 
			
		||||
    "@radix-ui/react-icons": "^1.3.0",
 | 
			
		||||
    "@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-select": "^2.1.1",
 | 
			
		||||
    "@radix-ui/react-separator": "^1.1.0",
 | 
			
		||||
    "@radix-ui/react-slot": "^1.1.0",
 | 
			
		||||
    "@radix-ui/react-tabs": "^1.1.0",
 | 
			
		||||
    "@radix-ui/react-toast": "^1.2.1",
 | 
			
		||||
    "@radix-ui/react-toggle": "^1.1.0",
 | 
			
		||||
    "@radix-ui/react-toggle-group": "^1.1.0",
 | 
			
		||||
    "@radix-ui/react-tooltip": "^1.1.2",
 | 
			
		||||
    "@uidotdev/usehooks": "^2.4.1",
 | 
			
		||||
    "@xyflow/react": "^12.3.1",
 | 
			
		||||
    "ahooks": "^3.8.1",
 | 
			
		||||
    "ai": "^3.3.14",
 | 
			
		||||
    "class-variance-authority": "^0.7.0",
 | 
			
		||||
    "clsx": "^2.1.1",
 | 
			
		||||
    "cmdk": "^1.0.0",
 | 
			
		||||
    "dexie": "^4.0.8",
 | 
			
		||||
    "fast-deep-equal": "^3.1.3",
 | 
			
		||||
    "html-to-image": "^1.11.11",
 | 
			
		||||
    "i18next": "^23.14.0",
 | 
			
		||||
    "lucide-react": "^0.441.0",
 | 
			
		||||
    "monaco-editor": "^0.52.0",
 | 
			
		||||
    "nanoid": "^5.0.7",
 | 
			
		||||
    "node-sql-parser": "^5.3.2",
 | 
			
		||||
    "react": "^18.3.1",
 | 
			
		||||
    "react-dom": "^18.3.1",
 | 
			
		||||
    "react-helmet-async": "^2.0.5",
 | 
			
		||||
    "react-hotkeys-hook": "^4.5.0",
 | 
			
		||||
    "react-i18next": "^15.0.1",
 | 
			
		||||
    "react-resizable-panels": "^2.0.22",
 | 
			
		||||
    "react-responsive": "^10.0.0",
 | 
			
		||||
    "react-router-dom": "^6.26.0",
 | 
			
		||||
    "react-use": "^17.5.1",
 | 
			
		||||
    "tailwind-merge": "^2.4.0",
 | 
			
		||||
    "tailwindcss-animate": "^1.0.7",
 | 
			
		||||
    "timeago-react": "^3.0.6",
 | 
			
		||||
    "vaul": "^0.9.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/node": "^22.1.0",
 | 
			
		||||
    "@types/react": "^18.3.3",
 | 
			
		||||
    "@types/react-dom": "^18.3.0",
 | 
			
		||||
    "@typescript-eslint/eslint-plugin": "^7.15.0",
 | 
			
		||||
    "@typescript-eslint/parser": "^7.15.0",
 | 
			
		||||
    "@vitejs/plugin-react": "^4.3.1",
 | 
			
		||||
    "autoprefixer": "^10.4.20",
 | 
			
		||||
    "eslint": "^8.57.0",
 | 
			
		||||
    "eslint-config-prettier": "^9.1.0",
 | 
			
		||||
    "eslint-plugin-css-modules": "^2.12.0",
 | 
			
		||||
    "eslint-plugin-jsx-a11y": "^6.9.0",
 | 
			
		||||
    "eslint-plugin-prettier": "^5.2.1",
 | 
			
		||||
    "eslint-plugin-react": "^7.35.0",
 | 
			
		||||
    "eslint-plugin-react-hooks": "^4.6.2",
 | 
			
		||||
    "eslint-plugin-react-refresh": "^0.4.7",
 | 
			
		||||
    "eslint-plugin-tailwindcss": "^3.17.4",
 | 
			
		||||
    "husky": "^9.1.5",
 | 
			
		||||
    "postcss": "^8.4.40",
 | 
			
		||||
    "prettier": "^3.3.3",
 | 
			
		||||
    "rollup-plugin-visualizer": "^5.12.0",
 | 
			
		||||
    "tailwindcss": "^3.4.7",
 | 
			
		||||
    "typescript": "^5.2.2",
 | 
			
		||||
    "unplugin-inject-preload": "^3.0.0",
 | 
			
		||||
    "vite": "^5.3.4"
 | 
			
		||||
  }
 | 
			
		||||
    "name": "chartdb",
 | 
			
		||||
    "private": true,
 | 
			
		||||
    "version": "1.3.1",
 | 
			
		||||
    "type": "module",
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "dev": "vite",
 | 
			
		||||
        "build": "npm run lint && tsc -b && vite build",
 | 
			
		||||
        "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
 | 
			
		||||
        "lint:fix": "npm run lint -- --fix",
 | 
			
		||||
        "preview": "vite preview",
 | 
			
		||||
        "prepare": "husky"
 | 
			
		||||
    },
 | 
			
		||||
    "dependencies": {
 | 
			
		||||
        "@ai-sdk/openai": "^0.0.51",
 | 
			
		||||
        "@dnd-kit/sortable": "^8.0.0",
 | 
			
		||||
        "@monaco-editor/react": "^4.6.0",
 | 
			
		||||
        "@radix-ui/react-accordion": "^1.2.0",
 | 
			
		||||
        "@radix-ui/react-alert-dialog": "^1.1.1",
 | 
			
		||||
        "@radix-ui/react-avatar": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-checkbox": "^1.1.1",
 | 
			
		||||
        "@radix-ui/react-collapsible": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-context-menu": "^2.2.1",
 | 
			
		||||
        "@radix-ui/react-dialog": "^1.1.1",
 | 
			
		||||
        "@radix-ui/react-dropdown-menu": "^2.1.1",
 | 
			
		||||
        "@radix-ui/react-hover-card": "^1.1.1",
 | 
			
		||||
        "@radix-ui/react-icons": "^1.3.0",
 | 
			
		||||
        "@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-select": "^2.1.1",
 | 
			
		||||
        "@radix-ui/react-separator": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-slot": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-tabs": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-toast": "^1.2.1",
 | 
			
		||||
        "@radix-ui/react-toggle": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-toggle-group": "^1.1.0",
 | 
			
		||||
        "@radix-ui/react-tooltip": "^1.1.2",
 | 
			
		||||
        "@uidotdev/usehooks": "^2.4.1",
 | 
			
		||||
        "@xyflow/react": "^12.3.1",
 | 
			
		||||
        "ahooks": "^3.8.1",
 | 
			
		||||
        "ai": "^3.3.14",
 | 
			
		||||
        "class-variance-authority": "^0.7.0",
 | 
			
		||||
        "clsx": "^2.1.1",
 | 
			
		||||
        "cmdk": "^1.0.0",
 | 
			
		||||
        "dexie": "^4.0.8",
 | 
			
		||||
        "fast-deep-equal": "^3.1.3",
 | 
			
		||||
        "html-to-image": "^1.11.11",
 | 
			
		||||
        "i18next": "^23.14.0",
 | 
			
		||||
        "i18next-browser-languagedetector": "^8.0.0",
 | 
			
		||||
        "lucide-react": "^0.441.0",
 | 
			
		||||
        "monaco-editor": "^0.52.0",
 | 
			
		||||
        "nanoid": "^5.0.7",
 | 
			
		||||
        "node-sql-parser": "^5.3.2",
 | 
			
		||||
        "react": "^18.3.1",
 | 
			
		||||
        "react-dom": "^18.3.1",
 | 
			
		||||
        "react-helmet-async": "^2.0.5",
 | 
			
		||||
        "react-hotkeys-hook": "^4.5.0",
 | 
			
		||||
        "react-i18next": "^15.0.1",
 | 
			
		||||
        "react-resizable-panels": "^2.0.22",
 | 
			
		||||
        "react-responsive": "^10.0.0",
 | 
			
		||||
        "react-router-dom": "^6.26.0",
 | 
			
		||||
        "react-use": "^17.5.1",
 | 
			
		||||
        "tailwind-merge": "^2.4.0",
 | 
			
		||||
        "tailwindcss-animate": "^1.0.7",
 | 
			
		||||
        "timeago-react": "^3.0.6",
 | 
			
		||||
        "vaul": "^0.9.1",
 | 
			
		||||
        "zod": "^3.23.8"
 | 
			
		||||
    },
 | 
			
		||||
    "devDependencies": {
 | 
			
		||||
        "@types/node": "^22.1.0",
 | 
			
		||||
        "@types/react": "^18.3.3",
 | 
			
		||||
        "@types/react-dom": "^18.3.0",
 | 
			
		||||
        "@typescript-eslint/eslint-plugin": "^7.15.0",
 | 
			
		||||
        "@typescript-eslint/parser": "^7.15.0",
 | 
			
		||||
        "@vitejs/plugin-react": "^4.3.1",
 | 
			
		||||
        "autoprefixer": "^10.4.20",
 | 
			
		||||
        "eslint": "^8.57.0",
 | 
			
		||||
        "eslint-config-prettier": "^9.1.0",
 | 
			
		||||
        "eslint-plugin-css-modules": "^2.12.0",
 | 
			
		||||
        "eslint-plugin-jsx-a11y": "^6.9.0",
 | 
			
		||||
        "eslint-plugin-prettier": "^5.2.1",
 | 
			
		||||
        "eslint-plugin-react": "^7.35.0",
 | 
			
		||||
        "eslint-plugin-react-hooks": "^4.6.2",
 | 
			
		||||
        "eslint-plugin-react-refresh": "^0.4.7",
 | 
			
		||||
        "eslint-plugin-tailwindcss": "^3.17.4",
 | 
			
		||||
        "husky": "^9.1.5",
 | 
			
		||||
        "postcss": "^8.4.40",
 | 
			
		||||
        "prettier": "^3.3.3",
 | 
			
		||||
        "rollup-plugin-visualizer": "^5.12.0",
 | 
			
		||||
        "tailwindcss": "^3.4.7",
 | 
			
		||||
        "typescript": "^5.2.2",
 | 
			
		||||
        "unplugin-inject-preload": "^3.0.0",
 | 
			
		||||
        "vite": "^5.3.4"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 882 KiB After Width: | Height: | Size: 882 KiB  | 
							
								
								
									
										0
									
								
								public/config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/adonis-acl-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 290 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/adonis-acl.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 322 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/airbnb-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 388 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/airbnb.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 424 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/akaunting-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 327 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/akaunting.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 345 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/attendize-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 525 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/attendize-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 613 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/bookstack-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 452 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/bookstack-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 578 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/bouncer-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 323 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/bouncer-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 348 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/buddypress-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 417 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/buddypress.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 496 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/cabot-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 420 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/cabot-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 498 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/comfortable-mexican-sofa-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 375 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/comfortable-mexican-sofa-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 409 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/django-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 354 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/django-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 389 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/feedbin-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 498 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/feedbin-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 583 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/flarum-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 412 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/flarum-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 499 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/freescout-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 441 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/freescout-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 502 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/gravity-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 382 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/gravity-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 415 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/hacker-news-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 427 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/hacker-news-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 472 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/koel-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 369 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/koel-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 402 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/laravel-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 168 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/laravel-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 172 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/laravel-permission-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 215 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/laravel-permission-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 216 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/laravel-spark-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 412 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/laravel-spark-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 449 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/lobsters-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 403 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/lobsters-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 450 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/monica-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 746 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/monica-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 846 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/pokemon-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 421 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/pokemon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 444 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/refinerycms-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 414 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/refinerycms-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 434 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/saas-pegasus-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 474 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/saas-pegasus-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 500 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/snipe-it-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 383 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/snipe-it-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 428 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/sylius-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 673 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/sylius-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 804 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/ticketit-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 337 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/ticketit-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 374 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/twitter-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 370 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/twitter-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 404 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/voyager-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 401 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/voyager-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 431 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/wordpress-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 461 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/wordpress-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 489 KiB  | 
							
								
								
									
										62
									
								
								src/components/alert/alert.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,62 @@
 | 
			
		||||
import * as React from 'react';
 | 
			
		||||
import { cva, type VariantProps } from 'class-variance-authority';
 | 
			
		||||
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
const alertVariants = cva(
 | 
			
		||||
    'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
 | 
			
		||||
    {
 | 
			
		||||
        variants: {
 | 
			
		||||
            variant: {
 | 
			
		||||
                default: 'bg-background text-foreground',
 | 
			
		||||
                destructive:
 | 
			
		||||
                    'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        defaultVariants: {
 | 
			
		||||
            variant: 'default',
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const Alert = React.forwardRef<
 | 
			
		||||
    HTMLDivElement,
 | 
			
		||||
    React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
 | 
			
		||||
>(({ className, variant, ...props }, ref) => (
 | 
			
		||||
    <div
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        role="alert"
 | 
			
		||||
        className={cn(alertVariants({ variant }), className)}
 | 
			
		||||
        {...props}
 | 
			
		||||
    />
 | 
			
		||||
));
 | 
			
		||||
Alert.displayName = 'Alert';
 | 
			
		||||
 | 
			
		||||
const AlertTitle = React.forwardRef<
 | 
			
		||||
    HTMLParagraphElement,
 | 
			
		||||
    React.HTMLAttributes<HTMLHeadingElement>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
    <h5
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        className={cn(
 | 
			
		||||
            'mb-1 font-medium leading-none tracking-tight',
 | 
			
		||||
            className
 | 
			
		||||
        )}
 | 
			
		||||
        {...props}
 | 
			
		||||
    />
 | 
			
		||||
));
 | 
			
		||||
AlertTitle.displayName = 'AlertTitle';
 | 
			
		||||
 | 
			
		||||
const AlertDescription = React.forwardRef<
 | 
			
		||||
    HTMLParagraphElement,
 | 
			
		||||
    React.HTMLAttributes<HTMLParagraphElement>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
    <div
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        className={cn('text-sm [&_p]:leading-relaxed', className)}
 | 
			
		||||
        {...props}
 | 
			
		||||
    />
 | 
			
		||||
));
 | 
			
		||||
AlertDescription.displayName = 'AlertDescription';
 | 
			
		||||
 | 
			
		||||
export { Alert, AlertTitle, AlertDescription };
 | 
			
		||||
@@ -9,12 +9,15 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { DarkTheme } from './themes/dark';
 | 
			
		||||
import { LightTheme } from './themes/light';
 | 
			
		||||
import './config.ts';
 | 
			
		||||
 | 
			
		||||
export interface CodeSnippetProps {
 | 
			
		||||
    className?: string;
 | 
			
		||||
    code: string;
 | 
			
		||||
    language?: 'sql' | 'shell';
 | 
			
		||||
    loading?: boolean;
 | 
			
		||||
    autoScroll?: boolean;
 | 
			
		||||
    isComplete?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Editor = lazy(() =>
 | 
			
		||||
@@ -24,7 +27,14 @@ export const Editor = lazy(() =>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
    ({ className, code, loading, language = 'sql' }) => {
 | 
			
		||||
    ({
 | 
			
		||||
        className,
 | 
			
		||||
        code,
 | 
			
		||||
        loading,
 | 
			
		||||
        language = 'sql',
 | 
			
		||||
        autoScroll = false,
 | 
			
		||||
        isComplete = true,
 | 
			
		||||
    }) => {
 | 
			
		||||
        const { t } = useTranslation();
 | 
			
		||||
        const monaco = useMonaco();
 | 
			
		||||
        const { effectiveTheme } = useTheme();
 | 
			
		||||
@@ -46,6 +56,16 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
            }, 1500);
 | 
			
		||||
        }, [isCopied]);
 | 
			
		||||
 | 
			
		||||
        useEffect(() => {
 | 
			
		||||
            if (monaco) {
 | 
			
		||||
                const editor = monaco.editor.getModels()[0];
 | 
			
		||||
                if (editor && autoScroll) {
 | 
			
		||||
                    const lineCount = editor.getLineCount();
 | 
			
		||||
                    monaco.editor.getEditors()[0]?.revealLine(lineCount);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }, [code, monaco, autoScroll]);
 | 
			
		||||
 | 
			
		||||
        const copyToClipboard = useCallback(() => {
 | 
			
		||||
            navigator.clipboard.writeText(code);
 | 
			
		||||
            setIsCopied(true);
 | 
			
		||||
@@ -62,32 +82,38 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
                    <Spinner />
 | 
			
		||||
                ) : (
 | 
			
		||||
                    <Suspense fallback={<Spinner />}>
 | 
			
		||||
                        <Tooltip
 | 
			
		||||
                            onOpenChange={setTooltipOpen}
 | 
			
		||||
                            open={isCopied || tooltipOpen}
 | 
			
		||||
                        >
 | 
			
		||||
                            <TooltipTrigger
 | 
			
		||||
                                asChild
 | 
			
		||||
                                className="absolute right-1 top-1 z-10"
 | 
			
		||||
                        {isComplete ? (
 | 
			
		||||
                            <Tooltip
 | 
			
		||||
                                onOpenChange={setTooltipOpen}
 | 
			
		||||
                                open={isCopied || tooltipOpen}
 | 
			
		||||
                            >
 | 
			
		||||
                                <span>
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                        className=" h-fit p-1.5"
 | 
			
		||||
                                        variant="outline"
 | 
			
		||||
                                        onClick={copyToClipboard}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {isCopied ? (
 | 
			
		||||
                                            <CopyCheck size={16} />
 | 
			
		||||
                                        ) : (
 | 
			
		||||
                                            <Copy size={16} />
 | 
			
		||||
                                        )}
 | 
			
		||||
                                    </Button>
 | 
			
		||||
                                </span>
 | 
			
		||||
                            </TooltipTrigger>
 | 
			
		||||
                            <TooltipContent>
 | 
			
		||||
                                {t(isCopied ? 'copied' : 'copy_to_clipboard')}
 | 
			
		||||
                            </TooltipContent>
 | 
			
		||||
                        </Tooltip>
 | 
			
		||||
                                <TooltipTrigger
 | 
			
		||||
                                    asChild
 | 
			
		||||
                                    className="absolute right-1 top-1 z-10"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <span>
 | 
			
		||||
                                        <Button
 | 
			
		||||
                                            className=" h-fit p-1.5"
 | 
			
		||||
                                            variant="outline"
 | 
			
		||||
                                            onClick={copyToClipboard}
 | 
			
		||||
                                        >
 | 
			
		||||
                                            {isCopied ? (
 | 
			
		||||
                                                <CopyCheck size={16} />
 | 
			
		||||
                                            ) : (
 | 
			
		||||
                                                <Copy size={16} />
 | 
			
		||||
                                            )}
 | 
			
		||||
                                        </Button>
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </TooltipTrigger>
 | 
			
		||||
                                <TooltipContent>
 | 
			
		||||
                                    {t(
 | 
			
		||||
                                        isCopied
 | 
			
		||||
                                            ? 'copied'
 | 
			
		||||
                                            : 'copy_to_clipboard'
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </TooltipContent>
 | 
			
		||||
                            </Tooltip>
 | 
			
		||||
                        ) : null}
 | 
			
		||||
 | 
			
		||||
                        <Editor
 | 
			
		||||
                            value={code}
 | 
			
		||||
@@ -117,6 +143,9 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
 | 
			
		||||
                                contextmenu: false,
 | 
			
		||||
                            }}
 | 
			
		||||
                        />
 | 
			
		||||
                        {!isComplete ? (
 | 
			
		||||
                            <div className="absolute bottom-2 right-2 size-2 animate-blink rounded-full bg-pink-600" />
 | 
			
		||||
                        ) : null}
 | 
			
		||||
                    </Suspense>
 | 
			
		||||
                )}
 | 
			
		||||
            </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import * as DialogPrimitive from '@radix-ui/react-dialog';
 | 
			
		||||
import { Cross2Icon } from '@radix-ui/react-icons';
 | 
			
		||||
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import { ScrollArea } from '../scroll-area/scroll-area';
 | 
			
		||||
 | 
			
		||||
const Dialog = DialogPrimitive.Root;
 | 
			
		||||
 | 
			
		||||
@@ -110,6 +111,18 @@ const DialogDescription = React.forwardRef<
 | 
			
		||||
));
 | 
			
		||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
 | 
			
		||||
 | 
			
		||||
const DialogInternalContent = React.forwardRef<
 | 
			
		||||
    React.ElementRef<typeof ScrollArea>,
 | 
			
		||||
    React.ComponentPropsWithoutRef<typeof ScrollArea>
 | 
			
		||||
>(({ className, ...props }, ref) => (
 | 
			
		||||
    <ScrollArea
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        className={cn('flex max-h-screen flex-col overflow-y-auto', className)}
 | 
			
		||||
        {...props}
 | 
			
		||||
    />
 | 
			
		||||
));
 | 
			
		||||
DialogInternalContent.displayName = 'DialogInternalContent';
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
    Dialog,
 | 
			
		||||
    DialogPortal,
 | 
			
		||||
@@ -121,4 +134,5 @@ export {
 | 
			
		||||
    DialogFooter,
 | 
			
		||||
    DialogTitle,
 | 
			
		||||
    DialogDescription,
 | 
			
		||||
    DialogInternalContent,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										168
									
								
								src/components/file-uploader/file-uploader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,168 @@
 | 
			
		||||
import React, { useCallback, useEffect, useState } from 'react';
 | 
			
		||||
import { Upload, FileIcon, AlertCircle, Trash2 } from 'lucide-react';
 | 
			
		||||
import { Button } from '../button/button';
 | 
			
		||||
 | 
			
		||||
interface FileWithPreview extends File {
 | 
			
		||||
    preview?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface FileUploaderProps {
 | 
			
		||||
    onFilesChange?: (files: File[]) => void;
 | 
			
		||||
    multiple?: boolean;
 | 
			
		||||
    supportedExtensions?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const FileUploader: React.FC<FileUploaderProps> = ({
 | 
			
		||||
    onFilesChange,
 | 
			
		||||
    multiple,
 | 
			
		||||
    supportedExtensions,
 | 
			
		||||
}) => {
 | 
			
		||||
    const [files, setFiles] = useState<FileWithPreview[]>([]);
 | 
			
		||||
    const [isDragging, setIsDragging] = useState(false);
 | 
			
		||||
    const [error, setError] = useState<string | null>(null);
 | 
			
		||||
 | 
			
		||||
    const isFileSupported = useCallback(
 | 
			
		||||
        (file: File) => {
 | 
			
		||||
            if (!supportedExtensions) return true;
 | 
			
		||||
            const fileExtension = file.name.split('.').pop()?.toLowerCase();
 | 
			
		||||
            return fileExtension
 | 
			
		||||
                ? supportedExtensions.includes(`.${fileExtension}`)
 | 
			
		||||
                : false;
 | 
			
		||||
        },
 | 
			
		||||
        [supportedExtensions]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const handleFiles = useCallback(
 | 
			
		||||
        (selectedFiles: FileList) => {
 | 
			
		||||
            const newFiles = Array.from(selectedFiles)
 | 
			
		||||
                .filter((file) => {
 | 
			
		||||
                    if (!isFileSupported(file)) {
 | 
			
		||||
                        setError(
 | 
			
		||||
                            `File type not supported. Supported types: ${supportedExtensions?.join(', ')}`
 | 
			
		||||
                        );
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    return true;
 | 
			
		||||
                })
 | 
			
		||||
                .map((file) =>
 | 
			
		||||
                    Object.assign(file, { preview: URL.createObjectURL(file) })
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
            if (newFiles.length === 0) return;
 | 
			
		||||
 | 
			
		||||
            setError(null);
 | 
			
		||||
            setFiles((prevFiles) => {
 | 
			
		||||
                if (multiple) {
 | 
			
		||||
                    return [...prevFiles, ...newFiles];
 | 
			
		||||
                } else {
 | 
			
		||||
                    return newFiles.slice(0, 1);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [multiple, supportedExtensions, isFileSupported]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const onDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        setIsDragging(true);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const onDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        setIsDragging(false);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const onDrop = useCallback(
 | 
			
		||||
        (e: React.DragEvent<HTMLDivElement>) => {
 | 
			
		||||
            e.preventDefault();
 | 
			
		||||
            setIsDragging(false);
 | 
			
		||||
            if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
 | 
			
		||||
                handleFiles(e.dataTransfer.files);
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        [handleFiles]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (onFilesChange) {
 | 
			
		||||
            onFilesChange(files.length > 0 ? files : []);
 | 
			
		||||
        }
 | 
			
		||||
    }, [files, onFilesChange]);
 | 
			
		||||
 | 
			
		||||
    const removeFile = useCallback((fileToRemove: File) => {
 | 
			
		||||
        setFiles((prevFiles) =>
 | 
			
		||||
            prevFiles.filter((file) => file !== fileToRemove)
 | 
			
		||||
        );
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="mx-auto w-full max-w-md">
 | 
			
		||||
            <div
 | 
			
		||||
                onDragOver={onDragOver}
 | 
			
		||||
                onDragLeave={onDragLeave}
 | 
			
		||||
                onDrop={onDrop}
 | 
			
		||||
                className={`cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors ${
 | 
			
		||||
                    isDragging
 | 
			
		||||
                        ? 'border-primary bg-primary/10 dark:bg-primary/20'
 | 
			
		||||
                        : 'border-gray-300 hover:border-primary dark:border-gray-600 dark:hover:border-primary'
 | 
			
		||||
                }`}
 | 
			
		||||
            >
 | 
			
		||||
                <input
 | 
			
		||||
                    type="file"
 | 
			
		||||
                    multiple={multiple}
 | 
			
		||||
                    onChange={(e) =>
 | 
			
		||||
                        e.target.files && handleFiles(e.target.files)
 | 
			
		||||
                    }
 | 
			
		||||
                    className="hidden"
 | 
			
		||||
                    id="fileInput"
 | 
			
		||||
                    accept={supportedExtensions?.join(',')}
 | 
			
		||||
                />
 | 
			
		||||
                <label htmlFor="fileInput" className="cursor-pointer">
 | 
			
		||||
                    <Upload className="mx-auto size-12 text-gray-400 dark:text-gray-500" />
 | 
			
		||||
                    <p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
 | 
			
		||||
                        {multiple
 | 
			
		||||
                            ? 'Drag and drop files here or click to select'
 | 
			
		||||
                            : 'Drag and drop a file here or click to select'}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    {supportedExtensions ? (
 | 
			
		||||
                        <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
 | 
			
		||||
                            Supported types: {supportedExtensions.join(', ')}
 | 
			
		||||
                        </p>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                </label>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            {error ? (
 | 
			
		||||
                <div className="mt-4 flex items-center rounded-lg bg-red-100 p-3 text-red-700 dark:bg-red-900 dark:text-red-200">
 | 
			
		||||
                    <AlertCircle className="mr-2 size-5" />
 | 
			
		||||
                    <span className="text-sm">{error}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
            ) : null}
 | 
			
		||||
 | 
			
		||||
            {files.length > 0 ? (
 | 
			
		||||
                <ul className="mt-4 space-y-4">
 | 
			
		||||
                    {files.map((file) => (
 | 
			
		||||
                        <li
 | 
			
		||||
                            key={file.name}
 | 
			
		||||
                            className="flex items-center justify-between rounded-lg bg-gray-100 p-3 dark:bg-gray-800"
 | 
			
		||||
                        >
 | 
			
		||||
                            <div className="flex min-w-0 flex-1 items-center space-x-2">
 | 
			
		||||
                                <FileIcon className="size-5 text-primary" />
 | 
			
		||||
                                <span className="truncate text-sm font-medium text-gray-700 dark:text-gray-300">
 | 
			
		||||
                                    {file.name}
 | 
			
		||||
                                </span>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant="ghost"
 | 
			
		||||
                                className="size-5 p-0 hover:bg-primary-foreground"
 | 
			
		||||
                                onClick={() => removeFile(file)}
 | 
			
		||||
                            >
 | 
			
		||||
                                <Trash2 className="size-3.5 text-red-700" />
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </li>
 | 
			
		||||
                    ))}
 | 
			
		||||
                </ul>
 | 
			
		||||
            ) : null}
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -34,7 +34,7 @@ export const ListMenu = React.forwardRef<HTMLDivElement, ListMenuProps>(
 | 
			
		||||
                                strokeWidth={item.selected ? 2.4 : 2}
 | 
			
		||||
                            />
 | 
			
		||||
                        ) : null}
 | 
			
		||||
                        {item.title}
 | 
			
		||||
                        <span className="min-w-0 truncate">{item.title}</span>
 | 
			
		||||
                    </Link>
 | 
			
		||||
                ))}
 | 
			
		||||
            </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ const ScrollArea = React.forwardRef<
 | 
			
		||||
        className={cn('relative overflow-hidden', className)}
 | 
			
		||||
        {...props}
 | 
			
		||||
    >
 | 
			
		||||
        <ScrollAreaPrimitive.Viewport className="scrollable-flex size-full rounded-[inherit]">
 | 
			
		||||
        <ScrollAreaPrimitive.Viewport className="size-full rounded-[inherit]">
 | 
			
		||||
            {children}
 | 
			
		||||
        </ScrollAreaPrimitive.Viewport>
 | 
			
		||||
        <ScrollBar />
 | 
			
		||||
@@ -40,7 +40,7 @@ const ScrollBar = React.forwardRef<
 | 
			
		||||
        )}
 | 
			
		||||
        {...props}
 | 
			
		||||
    >
 | 
			
		||||
        <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
 | 
			
		||||
        <ScrollAreaPrimitive.ScrollAreaThumb className="relative z-20 flex-1 rounded-full bg-border" />
 | 
			
		||||
    </ScrollAreaPrimitive.ScrollAreaScrollbar>
 | 
			
		||||
));
 | 
			
		||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ export interface SelectBoxOption {
 | 
			
		||||
    description?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface SelectBoxProps {
 | 
			
		||||
export interface SelectBoxProps {
 | 
			
		||||
    options: SelectBoxOption[];
 | 
			
		||||
    value?: string[] | string;
 | 
			
		||||
    onChange?: (values: string[] | string) => void;
 | 
			
		||||
@@ -156,9 +156,19 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
            [options, value, multiple]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const handleKeyDown = React.useCallback(
 | 
			
		||||
            (e: React.KeyboardEvent) => {
 | 
			
		||||
                if (!isOpen && e.code.toLowerCase() === 'space') {
 | 
			
		||||
                    e.preventDefault();
 | 
			
		||||
                    onOpenChange(true);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            [isOpen, onOpenChange]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
 | 
			
		||||
                <PopoverTrigger asChild>
 | 
			
		||||
                <PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
 | 
			
		||||
                    <div
 | 
			
		||||
                        className={cn(
 | 
			
		||||
                            `flex min-h-[36px] cursor-pointer items-center justify-between rounded-md border px-3 py-1 data-[state=open]:border-ring ${disabled ? 'bg-muted pointer-events-none' : ''}`,
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,6 @@ import type { DBRelationship } from '@/lib/domain/db-relationship';
 | 
			
		||||
import { useStorage } from '@/hooks/use-storage';
 | 
			
		||||
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import { useConfig } from '@/hooks/use-config';
 | 
			
		||||
import type { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
			
		||||
import type { DBSchema } from '@/lib/domain/db-schema';
 | 
			
		||||
import {
 | 
			
		||||
@@ -34,13 +32,11 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
> = ({ children, diagram, readonly }) => {
 | 
			
		||||
    let db = useStorage();
 | 
			
		||||
    const events = useEventEmitter<ChartDBEvent>();
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
    const { setSchemasFilter, schemasFilter } = useLocalConfig();
 | 
			
		||||
    const { addUndoAction, resetRedoStack, resetUndoStack } =
 | 
			
		||||
        useRedoUndoStack();
 | 
			
		||||
    const [diagramId, setDiagramId] = useState('');
 | 
			
		||||
    const [diagramName, setDiagramName] = useState('');
 | 
			
		||||
    const { updateConfig } = useConfig();
 | 
			
		||||
    const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
 | 
			
		||||
    const [diagramUpdatedAt, setDiagramUpdatedAt] = useState<Date>(new Date());
 | 
			
		||||
    const [databaseType, setDatabaseType] = useState<DatabaseType>(
 | 
			
		||||
@@ -173,34 +169,13 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            resetRedoStack();
 | 
			
		||||
            resetUndoStack();
 | 
			
		||||
 | 
			
		||||
            const [config] = await Promise.all([
 | 
			
		||||
                db.getConfig(),
 | 
			
		||||
            await Promise.all([
 | 
			
		||||
                db.deleteDiagramTables(diagramId),
 | 
			
		||||
                db.deleteDiagramRelationships(diagramId),
 | 
			
		||||
                db.deleteDiagram(diagramId),
 | 
			
		||||
                db.deleteDiagramDependencies(diagramId),
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
            if (config?.defaultDiagramId === diagramId) {
 | 
			
		||||
                const diagrams = await db.listDiagrams();
 | 
			
		||||
 | 
			
		||||
                if (diagrams.length > 0) {
 | 
			
		||||
                    const defaultDiagramId = diagrams[0].id;
 | 
			
		||||
                    await updateConfig({ defaultDiagramId });
 | 
			
		||||
                    navigate(`/diagrams/${defaultDiagramId}`);
 | 
			
		||||
                } else {
 | 
			
		||||
                    await updateConfig({ defaultDiagramId: '' });
 | 
			
		||||
                    navigate('/');
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }, [
 | 
			
		||||
            db,
 | 
			
		||||
            diagramId,
 | 
			
		||||
            navigate,
 | 
			
		||||
            resetRedoStack,
 | 
			
		||||
            resetUndoStack,
 | 
			
		||||
            updateConfig,
 | 
			
		||||
        ]);
 | 
			
		||||
        }, [db, diagramId, resetRedoStack, resetUndoStack]);
 | 
			
		||||
 | 
			
		||||
    const updateDiagramUpdatedAt: ChartDBContext['updateDiagramUpdatedAt'] =
 | 
			
		||||
        useCallback(async () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@ import type { TableSchemaDialogProps } from '@/dialogs/table-schema-dialog/table
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
export interface DialogContext {
 | 
			
		||||
    // Create diagram dialog
 | 
			
		||||
@@ -48,6 +50,18 @@ export interface DialogContext {
 | 
			
		||||
        params: Omit<ExportImageDialogProps, 'dialog'>
 | 
			
		||||
    ) => void;
 | 
			
		||||
    closeExportImageDialog: () => void;
 | 
			
		||||
 | 
			
		||||
    // Export diagram dialog
 | 
			
		||||
    openExportDiagramDialog: (
 | 
			
		||||
        params: Omit<ExportDiagramDialogProps, 'dialog'>
 | 
			
		||||
    ) => void;
 | 
			
		||||
    closeExportDiagramDialog: () => void;
 | 
			
		||||
 | 
			
		||||
    // Import diagram dialog
 | 
			
		||||
    openImportDiagramDialog: (
 | 
			
		||||
        params: Omit<ImportDiagramDialogProps, 'dialog'>
 | 
			
		||||
    ) => void;
 | 
			
		||||
    closeImportDiagramDialog: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const dialogContext = createContext<DialogContext>({
 | 
			
		||||
@@ -69,4 +83,8 @@ export const dialogContext = createContext<DialogContext>({
 | 
			
		||||
    closeStarUsDialog: emptyFn,
 | 
			
		||||
    openExportImageDialog: emptyFn,
 | 
			
		||||
    closeExportImageDialog: emptyFn,
 | 
			
		||||
    openExportDiagramDialog: emptyFn,
 | 
			
		||||
    closeExportDiagramDialog: emptyFn,
 | 
			
		||||
    openImportDiagramDialog: emptyFn,
 | 
			
		||||
    closeImportDiagramDialog: emptyFn,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,8 @@ import { emptyFn } from '@/lib/utils';
 | 
			
		||||
import { StarUsDialog } from '@/dialogs/star-us-dialog/star-us-dialog';
 | 
			
		||||
import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog';
 | 
			
		||||
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
 | 
			
		||||
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
 | 
			
		||||
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
 | 
			
		||||
 | 
			
		||||
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
@@ -86,6 +88,14 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            [setOpenTableSchemaDialog]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    // Export image dialog
 | 
			
		||||
    const [openExportDiagramDialog, setOpenExportDiagramDialog] =
 | 
			
		||||
        useState(false);
 | 
			
		||||
 | 
			
		||||
    // Import diagram dialog
 | 
			
		||||
    const [openImportDiagramDialog, setOpenImportDiagramDialog] =
 | 
			
		||||
        useState(false);
 | 
			
		||||
 | 
			
		||||
    // Alert dialog
 | 
			
		||||
    const [showAlert, setShowAlert] = useState(false);
 | 
			
		||||
    const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({
 | 
			
		||||
@@ -126,6 +136,12 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                closeStarUsDialog: () => setOpenStarUsDialog(false),
 | 
			
		||||
                closeExportImageDialog: () => setOpenExportImageDialog(false),
 | 
			
		||||
                openExportImageDialog: openExportImageDialogHandler,
 | 
			
		||||
                openExportDiagramDialog: () => setOpenExportDiagramDialog(true),
 | 
			
		||||
                closeExportDiagramDialog: () =>
 | 
			
		||||
                    setOpenExportDiagramDialog(false),
 | 
			
		||||
                openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
 | 
			
		||||
                closeImportDiagramDialog: () =>
 | 
			
		||||
                    setOpenImportDiagramDialog(false),
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
@@ -152,6 +168,8 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                dialog={{ open: openExportImageDialog }}
 | 
			
		||||
                {...exportImageDialogParams}
 | 
			
		||||
            />
 | 
			
		||||
            <ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
 | 
			
		||||
            <ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
 | 
			
		||||
        </dialogContext.Provider>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -5,12 +5,14 @@ import { toJpeg, toPng, toSvg } from 'html-to-image';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { hideLoader, showLoader } = useFullScreenLoader();
 | 
			
		||||
    const { setNodes, getViewport } = useReactFlow();
 | 
			
		||||
    const { effectiveTheme } = useTheme();
 | 
			
		||||
    const { diagramName } = useChartDB();
 | 
			
		||||
 | 
			
		||||
    const downloadImage = useCallback(
 | 
			
		||||
@@ -59,13 +61,101 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            const imageCreateFn = imageCreatorMap[type];
 | 
			
		||||
 | 
			
		||||
            setTimeout(async () => {
 | 
			
		||||
                const dataUrl = await imageCreateFn(
 | 
			
		||||
                    window.document.querySelector(
 | 
			
		||||
                        '.react-flow__viewport'
 | 
			
		||||
                    ) as HTMLElement,
 | 
			
		||||
                    {
 | 
			
		||||
                const viewportElement = window.document.querySelector(
 | 
			
		||||
                    '.react-flow__viewport'
 | 
			
		||||
                ) as HTMLElement;
 | 
			
		||||
 | 
			
		||||
                const markerDefs = document.querySelector(
 | 
			
		||||
                    '.marker-definitions defs'
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                const tempSvg = document.createElementNS(
 | 
			
		||||
                    'http://www.w3.org/2000/svg',
 | 
			
		||||
                    'svg'
 | 
			
		||||
                );
 | 
			
		||||
                tempSvg.style.position = 'absolute';
 | 
			
		||||
                tempSvg.style.top = '0';
 | 
			
		||||
                tempSvg.style.left = '0';
 | 
			
		||||
                tempSvg.style.width = '100%';
 | 
			
		||||
                tempSvg.style.height = '100%';
 | 
			
		||||
                tempSvg.style.overflow = 'visible';
 | 
			
		||||
                tempSvg.style.zIndex = '-50';
 | 
			
		||||
                tempSvg.setAttribute(
 | 
			
		||||
                    'viewBox',
 | 
			
		||||
                    `0 0 ${reactFlowBounds.width} ${reactFlowBounds.height}`
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                const defs = document.createElementNS(
 | 
			
		||||
                    'http://www.w3.org/2000/svg',
 | 
			
		||||
                    'defs'
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                if (markerDefs) {
 | 
			
		||||
                    defs.innerHTML = markerDefs.innerHTML;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const pattern = document.createElementNS(
 | 
			
		||||
                    'http://www.w3.org/2000/svg',
 | 
			
		||||
                    'pattern'
 | 
			
		||||
                );
 | 
			
		||||
                pattern.setAttribute('id', 'background-pattern');
 | 
			
		||||
                pattern.setAttribute('width', String(16 * viewport.zoom));
 | 
			
		||||
                pattern.setAttribute('height', String(16 * viewport.zoom));
 | 
			
		||||
                pattern.setAttribute('patternUnits', 'userSpaceOnUse');
 | 
			
		||||
                pattern.setAttribute(
 | 
			
		||||
                    'patternTransform',
 | 
			
		||||
                    `translate(${viewport.x % (16 * viewport.zoom)} ${viewport.y % (16 * viewport.zoom)})`
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                const dot = document.createElementNS(
 | 
			
		||||
                    'http://www.w3.org/2000/svg',
 | 
			
		||||
                    'circle'
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                const dotSize = viewport.zoom * 0.5;
 | 
			
		||||
                dot.setAttribute('cx', String(viewport.zoom));
 | 
			
		||||
                dot.setAttribute('cy', String(viewport.zoom));
 | 
			
		||||
                dot.setAttribute('r', String(dotSize));
 | 
			
		||||
                const dotColor =
 | 
			
		||||
                    effectiveTheme === 'light' ? '#92939C' : '#777777';
 | 
			
		||||
                dot.setAttribute('fill', dotColor);
 | 
			
		||||
 | 
			
		||||
                pattern.appendChild(dot);
 | 
			
		||||
                defs.appendChild(pattern);
 | 
			
		||||
                tempSvg.appendChild(defs);
 | 
			
		||||
 | 
			
		||||
                const backgroundRect = document.createElementNS(
 | 
			
		||||
                    'http://www.w3.org/2000/svg',
 | 
			
		||||
                    'rect'
 | 
			
		||||
                );
 | 
			
		||||
                const padding = 2000;
 | 
			
		||||
                backgroundRect.setAttribute('x', String(-viewport.x - padding));
 | 
			
		||||
                backgroundRect.setAttribute('y', String(-viewport.y - padding));
 | 
			
		||||
                backgroundRect.setAttribute(
 | 
			
		||||
                    'width',
 | 
			
		||||
                    String(reactFlowBounds.width + 2 * padding)
 | 
			
		||||
                );
 | 
			
		||||
                backgroundRect.setAttribute(
 | 
			
		||||
                    'height',
 | 
			
		||||
                    String(reactFlowBounds.height + 2 * padding)
 | 
			
		||||
                );
 | 
			
		||||
                backgroundRect.setAttribute('fill', 'url(#background-pattern)');
 | 
			
		||||
                tempSvg.appendChild(backgroundRect);
 | 
			
		||||
 | 
			
		||||
                viewportElement.insertBefore(
 | 
			
		||||
                    tempSvg,
 | 
			
		||||
                    viewportElement.firstChild
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
                try {
 | 
			
		||||
                    const dataUrl = await imageCreateFn(viewportElement, {
 | 
			
		||||
                        ...(type === 'jpeg' || type === 'png'
 | 
			
		||||
                            ? { backgroundColor: '#ffffff' }
 | 
			
		||||
                            ? {
 | 
			
		||||
                                  backgroundColor:
 | 
			
		||||
                                      effectiveTheme === 'light'
 | 
			
		||||
                                          ? '#ffffff'
 | 
			
		||||
                                          : '#141414',
 | 
			
		||||
                              }
 | 
			
		||||
                            : {}),
 | 
			
		||||
                        width: reactFlowBounds.width,
 | 
			
		||||
                        height: reactFlowBounds.height,
 | 
			
		||||
@@ -76,11 +166,13 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                        },
 | 
			
		||||
                        quality: 1,
 | 
			
		||||
                        pixelRatio: scale,
 | 
			
		||||
                    }
 | 
			
		||||
                );
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                downloadImage(dataUrl, type);
 | 
			
		||||
                hideLoader();
 | 
			
		||||
                    downloadImage(dataUrl, type);
 | 
			
		||||
                } finally {
 | 
			
		||||
                    viewportElement.removeChild(tempSvg);
 | 
			
		||||
                    hideLoader();
 | 
			
		||||
                }
 | 
			
		||||
            }, 0);
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
@@ -90,6 +182,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            imageCreatorMap,
 | 
			
		||||
            setNodes,
 | 
			
		||||
            showLoader,
 | 
			
		||||
            effectiveTheme,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import {
 | 
			
		||||
import { useHistory } from '@/hooks/use-history';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useLayout } from '@/hooks/use-layout';
 | 
			
		||||
 | 
			
		||||
export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
@@ -15,6 +16,8 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    const { redo, undo } = useHistory();
 | 
			
		||||
    const { openOpenDiagramDialog } = useDialog();
 | 
			
		||||
    const { updateDiagramUpdatedAt } = useChartDB();
 | 
			
		||||
    const { toggleSidePanel } = useLayout();
 | 
			
		||||
 | 
			
		||||
    useHotkeys(
 | 
			
		||||
        keyboardShortcutsForOS[KeyboardShortcutAction.REDO].keyCombination,
 | 
			
		||||
        redo,
 | 
			
		||||
@@ -49,6 +52,15 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        },
 | 
			
		||||
        [updateDiagramUpdatedAt]
 | 
			
		||||
    );
 | 
			
		||||
    useHotkeys(
 | 
			
		||||
        keyboardShortcutsForOS[KeyboardShortcutAction.TOGGLE_SIDE_PANEL]
 | 
			
		||||
            .keyCombination,
 | 
			
		||||
        toggleSidePanel,
 | 
			
		||||
        {
 | 
			
		||||
            preventDefault: true,
 | 
			
		||||
        },
 | 
			
		||||
        [toggleSidePanel]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <keyboardShortcutsContext.Provider value={{}}>
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ export enum KeyboardShortcutAction {
 | 
			
		||||
    UNDO = 'undo',
 | 
			
		||||
    OPEN_DIAGRAM = 'open_diagram',
 | 
			
		||||
    SAVE_DIAGRAM = 'save_diagram',
 | 
			
		||||
    TOGGLE_SIDE_PANEL = 'toggle_side_panel',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface KeyboardShortcut {
 | 
			
		||||
@@ -47,6 +48,13 @@ export const keyboardShortcuts: Record<
 | 
			
		||||
        keyCombinationMac: 'meta+s',
 | 
			
		||||
        keyCombinationWin: 'ctrl+s',
 | 
			
		||||
    },
 | 
			
		||||
    [KeyboardShortcutAction.TOGGLE_SIDE_PANEL]: {
 | 
			
		||||
        action: KeyboardShortcutAction.TOGGLE_SIDE_PANEL,
 | 
			
		||||
        keyCombinationLabelMac: '⌘B',
 | 
			
		||||
        keyCombinationLabelWin: 'Ctrl+B',
 | 
			
		||||
        keyCombinationMac: 'meta+b',
 | 
			
		||||
        keyCombinationWin: 'ctrl+b',
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface KeyboardShortcutForOS {
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ export interface LayoutContext {
 | 
			
		||||
    isSidePanelShowed: boolean;
 | 
			
		||||
    hideSidePanel: () => void;
 | 
			
		||||
    showSidePanel: () => void;
 | 
			
		||||
    toggleSidePanel: () => void;
 | 
			
		||||
 | 
			
		||||
    isSelectSchemaOpen: boolean;
 | 
			
		||||
    openSelectSchema: () => void;
 | 
			
		||||
@@ -47,6 +48,7 @@ export const layoutContext = createContext<LayoutContext>({
 | 
			
		||||
    isSidePanelShowed: false,
 | 
			
		||||
    hideSidePanel: emptyFn,
 | 
			
		||||
    showSidePanel: emptyFn,
 | 
			
		||||
    toggleSidePanel: emptyFn,
 | 
			
		||||
 | 
			
		||||
    isSelectSchemaOpen: false,
 | 
			
		||||
    openSelectSchema: emptyFn,
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,10 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    const showSidePanel: LayoutContext['showSidePanel'] = () =>
 | 
			
		||||
        setIsSidePanelShowed(true);
 | 
			
		||||
 | 
			
		||||
    const toggleSidePanel: LayoutContext['toggleSidePanel'] = () => {
 | 
			
		||||
        setIsSidePanelShowed((prevIsSidePanelShowed) => !prevIsSidePanelShowed);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const openTableFromSidebar: LayoutContext['openTableFromSidebar'] = (
 | 
			
		||||
        tableId
 | 
			
		||||
    ) => {
 | 
			
		||||
@@ -77,6 +81,7 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                isSidePanelShowed,
 | 
			
		||||
                hideSidePanel,
 | 
			
		||||
                showSidePanel,
 | 
			
		||||
                toggleSidePanel,
 | 
			
		||||
                isSelectSchemaOpen,
 | 
			
		||||
                openSelectSchema,
 | 
			
		||||
                closeSelectSchema,
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import {
 | 
			
		||||
    DialogDescription,
 | 
			
		||||
    DialogFooter,
 | 
			
		||||
    DialogHeader,
 | 
			
		||||
    DialogInternalContent,
 | 
			
		||||
    DialogTitle,
 | 
			
		||||
} from '@/components/dialog/dialog';
 | 
			
		||||
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
 | 
			
		||||
@@ -139,6 +140,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
            setScriptResult(fixedJson);
 | 
			
		||||
            setErrorMessage('');
 | 
			
		||||
        } else {
 | 
			
		||||
            setScriptResult(fixedJson);
 | 
			
		||||
            setErrorMessage(errorScriptOutputMessage);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -157,188 +159,201 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
 | 
			
		||||
    const renderContent = useCallback(() => {
 | 
			
		||||
        return (
 | 
			
		||||
            <div className="flex w-full flex-1 flex-col gap-6">
 | 
			
		||||
                {databaseTypeToEditionMap[databaseType].length > 0 ? (
 | 
			
		||||
                    <div className="flex flex-col gap-1 md:flex-row">
 | 
			
		||||
                        <p className="text-sm leading-6 text-muted-foreground">
 | 
			
		||||
                            {t(
 | 
			
		||||
                                'new_diagram_dialog.import_database.database_edition'
 | 
			
		||||
                            )}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <ToggleGroup
 | 
			
		||||
                            type="single"
 | 
			
		||||
                            className="ml-1 flex-wrap gap-2"
 | 
			
		||||
                            value={
 | 
			
		||||
                                !databaseEdition ? 'regular' : databaseEdition
 | 
			
		||||
                            }
 | 
			
		||||
                            onValueChange={(value) => {
 | 
			
		||||
                                setDatabaseEdition(
 | 
			
		||||
                                    value === 'regular'
 | 
			
		||||
                                        ? undefined
 | 
			
		||||
                                        : (value as DatabaseEdition)
 | 
			
		||||
                                );
 | 
			
		||||
                            }}
 | 
			
		||||
                        >
 | 
			
		||||
                            <ToggleGroupItem
 | 
			
		||||
                                value="regular"
 | 
			
		||||
                                variant="outline"
 | 
			
		||||
                                className="h-6 gap-1 p-0 px-2 shadow-none"
 | 
			
		||||
            <DialogInternalContent>
 | 
			
		||||
                <div className="flex w-full flex-1 flex-col gap-6">
 | 
			
		||||
                    {databaseTypeToEditionMap[databaseType].length > 0 ? (
 | 
			
		||||
                        <div className="flex flex-col gap-1 md:flex-row">
 | 
			
		||||
                            <p className="text-sm leading-6 text-muted-foreground">
 | 
			
		||||
                                {t(
 | 
			
		||||
                                    'new_diagram_dialog.import_database.database_edition'
 | 
			
		||||
                                )}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <ToggleGroup
 | 
			
		||||
                                type="single"
 | 
			
		||||
                                className="ml-1 flex-wrap gap-2"
 | 
			
		||||
                                value={
 | 
			
		||||
                                    !databaseEdition
 | 
			
		||||
                                        ? 'regular'
 | 
			
		||||
                                        : databaseEdition
 | 
			
		||||
                                }
 | 
			
		||||
                                onValueChange={(value) => {
 | 
			
		||||
                                    setDatabaseEdition(
 | 
			
		||||
                                        value === 'regular'
 | 
			
		||||
                                            ? undefined
 | 
			
		||||
                                            : (value as DatabaseEdition)
 | 
			
		||||
                                    );
 | 
			
		||||
                                }}
 | 
			
		||||
                            >
 | 
			
		||||
                                <Avatar className="size-4 rounded-none">
 | 
			
		||||
                                    <AvatarImage
 | 
			
		||||
                                        src={
 | 
			
		||||
                                            databaseSecondaryLogoMap[
 | 
			
		||||
                                                databaseType
 | 
			
		||||
                                            ]
 | 
			
		||||
                                        }
 | 
			
		||||
                                        alt="Regular"
 | 
			
		||||
                                    />
 | 
			
		||||
                                    <AvatarFallback>Regular</AvatarFallback>
 | 
			
		||||
                                </Avatar>
 | 
			
		||||
                                Regular
 | 
			
		||||
                            </ToggleGroupItem>
 | 
			
		||||
                            {databaseTypeToEditionMap[databaseType].map(
 | 
			
		||||
                                (edition) => (
 | 
			
		||||
                                    <ToggleGroupItem
 | 
			
		||||
                                        value={edition}
 | 
			
		||||
                                        key={edition}
 | 
			
		||||
                                        variant="outline"
 | 
			
		||||
                                        className="h-6 gap-1 p-0 px-2 shadow-none"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <Avatar className="size-4">
 | 
			
		||||
                                            <AvatarImage
 | 
			
		||||
                                                src={
 | 
			
		||||
                                                    databaseEditionToImageMap[
 | 
			
		||||
                                                        edition
 | 
			
		||||
                                                    ]
 | 
			
		||||
                                                }
 | 
			
		||||
                                                alt={
 | 
			
		||||
                                                    databaseEditionToLabelMap[
 | 
			
		||||
                                                        edition
 | 
			
		||||
                                                    ]
 | 
			
		||||
                                                }
 | 
			
		||||
                                            />
 | 
			
		||||
                                            <AvatarFallback>
 | 
			
		||||
                                                {
 | 
			
		||||
                                                    databaseEditionToLabelMap[
 | 
			
		||||
                                                        edition
 | 
			
		||||
                                                    ]
 | 
			
		||||
                                                }
 | 
			
		||||
                                            </AvatarFallback>
 | 
			
		||||
                                        </Avatar>
 | 
			
		||||
                                        {databaseEditionToLabelMap[edition]}
 | 
			
		||||
                                    </ToggleGroupItem>
 | 
			
		||||
                                )
 | 
			
		||||
                            )}
 | 
			
		||||
                        </ToggleGroup>
 | 
			
		||||
                    </div>
 | 
			
		||||
                ) : null}
 | 
			
		||||
                <div className="flex flex-col gap-1">
 | 
			
		||||
                    <div className="flex flex-col gap-1 text-sm text-muted-foreground md:flex-row md:justify-between">
 | 
			
		||||
                        <div>
 | 
			
		||||
                            1. {t('new_diagram_dialog.import_database.step_1')}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {databaseType === DatabaseType.SQL_SERVER && (
 | 
			
		||||
                            <SSMSInfo />
 | 
			
		||||
                        )}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {databaseTypeToClientsMap[databaseType].length > 0 ? (
 | 
			
		||||
                        <Tabs
 | 
			
		||||
                            value={
 | 
			
		||||
                                !databaseClient ? 'dbclient' : databaseClient
 | 
			
		||||
                            }
 | 
			
		||||
                            onValueChange={(value) => {
 | 
			
		||||
                                setDatabaseClient(
 | 
			
		||||
                                    value === 'dbclient'
 | 
			
		||||
                                        ? undefined
 | 
			
		||||
                                        : (value as DatabaseClient)
 | 
			
		||||
                                );
 | 
			
		||||
                            }}
 | 
			
		||||
                        >
 | 
			
		||||
                            <div className="flex flex-1">
 | 
			
		||||
                                <TabsList className="h-8 justify-start rounded-none rounded-t-sm ">
 | 
			
		||||
                                    <TabsTrigger
 | 
			
		||||
                                        value="dbclient"
 | 
			
		||||
                                        className="h-6 w-20"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        DB Client
 | 
			
		||||
                                    </TabsTrigger>
 | 
			
		||||
 | 
			
		||||
                                    {databaseClients?.map((client) => (
 | 
			
		||||
                                        <TabsTrigger
 | 
			
		||||
                                            key={client}
 | 
			
		||||
                                            value={client}
 | 
			
		||||
                                            className="h-6 !w-20"
 | 
			
		||||
                                <ToggleGroupItem
 | 
			
		||||
                                    value="regular"
 | 
			
		||||
                                    variant="outline"
 | 
			
		||||
                                    className="h-6 gap-1 p-0 px-2 shadow-none"
 | 
			
		||||
                                >
 | 
			
		||||
                                    <Avatar className="size-4 rounded-none">
 | 
			
		||||
                                        <AvatarImage
 | 
			
		||||
                                            src={
 | 
			
		||||
                                                databaseSecondaryLogoMap[
 | 
			
		||||
                                                    databaseType
 | 
			
		||||
                                                ]
 | 
			
		||||
                                            }
 | 
			
		||||
                                            alt="Regular"
 | 
			
		||||
                                        />
 | 
			
		||||
                                        <AvatarFallback>Regular</AvatarFallback>
 | 
			
		||||
                                    </Avatar>
 | 
			
		||||
                                    Regular
 | 
			
		||||
                                </ToggleGroupItem>
 | 
			
		||||
                                {databaseTypeToEditionMap[databaseType].map(
 | 
			
		||||
                                    (edition) => (
 | 
			
		||||
                                        <ToggleGroupItem
 | 
			
		||||
                                            value={edition}
 | 
			
		||||
                                            key={edition}
 | 
			
		||||
                                            variant="outline"
 | 
			
		||||
                                            className="h-6 gap-1 p-0 px-2 shadow-none"
 | 
			
		||||
                                        >
 | 
			
		||||
                                            {databaseClientToLabelMap[client]}
 | 
			
		||||
                                        </TabsTrigger>
 | 
			
		||||
                                    )) ?? []}
 | 
			
		||||
                                </TabsList>
 | 
			
		||||
                                            <Avatar className="size-4">
 | 
			
		||||
                                                <AvatarImage
 | 
			
		||||
                                                    src={
 | 
			
		||||
                                                        databaseEditionToImageMap[
 | 
			
		||||
                                                            edition
 | 
			
		||||
                                                        ]
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                    alt={
 | 
			
		||||
                                                        databaseEditionToLabelMap[
 | 
			
		||||
                                                            edition
 | 
			
		||||
                                                        ]
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                />
 | 
			
		||||
                                                <AvatarFallback>
 | 
			
		||||
                                                    {
 | 
			
		||||
                                                        databaseEditionToLabelMap[
 | 
			
		||||
                                                            edition
 | 
			
		||||
                                                        ]
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                </AvatarFallback>
 | 
			
		||||
                                            </Avatar>
 | 
			
		||||
                                            {databaseEditionToLabelMap[edition]}
 | 
			
		||||
                                        </ToggleGroupItem>
 | 
			
		||||
                                    )
 | 
			
		||||
                                )}
 | 
			
		||||
                            </ToggleGroup>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                    <div className="flex flex-col gap-1">
 | 
			
		||||
                        <div className="flex flex-col gap-1 text-sm text-muted-foreground md:flex-row md:justify-between">
 | 
			
		||||
                            <div>
 | 
			
		||||
                                1.{' '}
 | 
			
		||||
                                {t('new_diagram_dialog.import_database.step_1')}
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {databaseType === DatabaseType.SQL_SERVER && (
 | 
			
		||||
                                <SSMSInfo />
 | 
			
		||||
                            )}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {databaseTypeToClientsMap[databaseType].length > 0 ? (
 | 
			
		||||
                            <Tabs
 | 
			
		||||
                                value={
 | 
			
		||||
                                    !databaseClient
 | 
			
		||||
                                        ? 'dbclient'
 | 
			
		||||
                                        : databaseClient
 | 
			
		||||
                                }
 | 
			
		||||
                                onValueChange={(value) => {
 | 
			
		||||
                                    setDatabaseClient(
 | 
			
		||||
                                        value === 'dbclient'
 | 
			
		||||
                                            ? undefined
 | 
			
		||||
                                            : (value as DatabaseClient)
 | 
			
		||||
                                    );
 | 
			
		||||
                                }}
 | 
			
		||||
                            >
 | 
			
		||||
                                <div className="flex flex-1">
 | 
			
		||||
                                    <TabsList className="h-8 justify-start rounded-none rounded-t-sm ">
 | 
			
		||||
                                        <TabsTrigger
 | 
			
		||||
                                            value="dbclient"
 | 
			
		||||
                                            className="h-6 w-20"
 | 
			
		||||
                                        >
 | 
			
		||||
                                            DB Client
 | 
			
		||||
                                        </TabsTrigger>
 | 
			
		||||
 | 
			
		||||
                                        {databaseClients?.map((client) => (
 | 
			
		||||
                                            <TabsTrigger
 | 
			
		||||
                                                key={client}
 | 
			
		||||
                                                value={client}
 | 
			
		||||
                                                className="h-6 !w-20"
 | 
			
		||||
                                            >
 | 
			
		||||
                                                {
 | 
			
		||||
                                                    databaseClientToLabelMap[
 | 
			
		||||
                                                        client
 | 
			
		||||
                                                    ]
 | 
			
		||||
                                                }
 | 
			
		||||
                                            </TabsTrigger>
 | 
			
		||||
                                        )) ?? []}
 | 
			
		||||
                                    </TabsList>
 | 
			
		||||
                                </div>
 | 
			
		||||
                                <CodeSnippet
 | 
			
		||||
                                    className="h-40 w-full"
 | 
			
		||||
                                    loading={!importMetadataScripts}
 | 
			
		||||
                                    code={
 | 
			
		||||
                                        importMetadataScripts?.[databaseType]?.(
 | 
			
		||||
                                            {
 | 
			
		||||
                                                databaseEdition,
 | 
			
		||||
                                                databaseClient,
 | 
			
		||||
                                            }
 | 
			
		||||
                                        ) ?? ''
 | 
			
		||||
                                    }
 | 
			
		||||
                                    language={databaseClient ? 'shell' : 'sql'}
 | 
			
		||||
                                />
 | 
			
		||||
                            </Tabs>
 | 
			
		||||
                        ) : (
 | 
			
		||||
                            <CodeSnippet
 | 
			
		||||
                                className="h-40 w-full"
 | 
			
		||||
                                className="h-40 w-full flex-auto"
 | 
			
		||||
                                loading={!importMetadataScripts}
 | 
			
		||||
                                code={
 | 
			
		||||
                                    importMetadataScripts?.[databaseType]?.({
 | 
			
		||||
                                        databaseEdition,
 | 
			
		||||
                                        databaseClient,
 | 
			
		||||
                                    }) ?? ''
 | 
			
		||||
                                }
 | 
			
		||||
                                language={databaseClient ? 'shell' : 'sql'}
 | 
			
		||||
                                language="sql"
 | 
			
		||||
                            />
 | 
			
		||||
                        </Tabs>
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        <CodeSnippet
 | 
			
		||||
                            className="h-40 w-full flex-auto"
 | 
			
		||||
                            loading={!importMetadataScripts}
 | 
			
		||||
                            code={
 | 
			
		||||
                                importMetadataScripts?.[databaseType]?.({
 | 
			
		||||
                                    databaseEdition,
 | 
			
		||||
                                }) ?? ''
 | 
			
		||||
                            }
 | 
			
		||||
                            language="sql"
 | 
			
		||||
                        />
 | 
			
		||||
                    )}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="flex h-48 flex-col gap-1">
 | 
			
		||||
                    <p className="text-sm text-muted-foreground">
 | 
			
		||||
                        2. {t('new_diagram_dialog.import_database.step_2')}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <Textarea
 | 
			
		||||
                        className="w-full flex-1 rounded-md bg-muted p-2 text-sm"
 | 
			
		||||
                        placeholder={t(
 | 
			
		||||
                            'new_diagram_dialog.import_database.script_results_placeholder'
 | 
			
		||||
                        )}
 | 
			
		||||
                        value={scriptResult}
 | 
			
		||||
                        onChange={handleInputChange}
 | 
			
		||||
                    />
 | 
			
		||||
                    {showCheckJsonButton || errorMessage ? (
 | 
			
		||||
                        <div className="mt-2 flex items-center gap-2">
 | 
			
		||||
                            {showCheckJsonButton ? (
 | 
			
		||||
                                <Button
 | 
			
		||||
                                    type="button"
 | 
			
		||||
                                    variant="outline"
 | 
			
		||||
                                    size="sm"
 | 
			
		||||
                                    onClick={handleCheckJson}
 | 
			
		||||
                                    disabled={isCheckingJson}
 | 
			
		||||
                                >
 | 
			
		||||
                                    {isCheckingJson ? (
 | 
			
		||||
                                        <Spinner size="small" />
 | 
			
		||||
                                    ) : (
 | 
			
		||||
                                        t(
 | 
			
		||||
                                            'new_diagram_dialog.import_database.check_script_result'
 | 
			
		||||
                                        )
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                <p className="text-sm text-red-700">
 | 
			
		||||
                                    {errorMessage}
 | 
			
		||||
                                </p>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="flex h-48 flex-col gap-1">
 | 
			
		||||
                        <p className="text-sm text-muted-foreground">
 | 
			
		||||
                            2. {t('new_diagram_dialog.import_database.step_2')}
 | 
			
		||||
                        </p>
 | 
			
		||||
                        <Textarea
 | 
			
		||||
                            className="w-full flex-1 rounded-md bg-muted p-2 text-sm"
 | 
			
		||||
                            placeholder={t(
 | 
			
		||||
                                'new_diagram_dialog.import_database.script_results_placeholder'
 | 
			
		||||
                            )}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                            value={scriptResult}
 | 
			
		||||
                            onChange={handleInputChange}
 | 
			
		||||
                        />
 | 
			
		||||
                        {showCheckJsonButton || errorMessage ? (
 | 
			
		||||
                            <div className="mt-2 flex items-center gap-2">
 | 
			
		||||
                                {showCheckJsonButton ? (
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                        type="button"
 | 
			
		||||
                                        variant="outline"
 | 
			
		||||
                                        size="sm"
 | 
			
		||||
                                        onClick={handleCheckJson}
 | 
			
		||||
                                        disabled={isCheckingJson}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {isCheckingJson ? (
 | 
			
		||||
                                            <Spinner size="small" />
 | 
			
		||||
                                        ) : (
 | 
			
		||||
                                            t(
 | 
			
		||||
                                                'new_diagram_dialog.import_database.check_script_result'
 | 
			
		||||
                                            )
 | 
			
		||||
                                        )}
 | 
			
		||||
                                    </Button>
 | 
			
		||||
                                ) : (
 | 
			
		||||
                                    <p className="text-sm text-red-700">
 | 
			
		||||
                                        {errorMessage}
 | 
			
		||||
                                    </p>
 | 
			
		||||
                                )}
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ) : null}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            </DialogInternalContent>
 | 
			
		||||
        );
 | 
			
		||||
    }, [
 | 
			
		||||
        databaseEdition,
 | 
			
		||||
 
 | 
			
		||||
@@ -128,7 +128,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent
 | 
			
		||||
                className="flex w-[90vw] max-w-[90vw] flex-col overflow-y-auto md:overflow-visible lg:max-w-[60vw] xl:lg:max-w-lg xl:min-w-[45vw]"
 | 
			
		||||
                className="flex max-h-screen w-[90vw] max-w-[90vw] flex-col overflow-y-auto md:overflow-visible lg:max-w-[60vw] xl:lg:max-w-lg xl:min-w-[45vw]"
 | 
			
		||||
                showClose={hasExistingDiagram}
 | 
			
		||||
            >
 | 
			
		||||
                {step === CreateDiagramDialogStep.SELECT_DATABASE ? (
 | 
			
		||||
 
 | 
			
		||||
@@ -5,11 +5,13 @@ import {
 | 
			
		||||
    DialogDescription,
 | 
			
		||||
    DialogFooter,
 | 
			
		||||
    DialogHeader,
 | 
			
		||||
    DialogInternalContent,
 | 
			
		||||
    DialogTitle,
 | 
			
		||||
} from '@/components/dialog/dialog';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { SelectDatabaseContent } from './select-database-content';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
 | 
			
		||||
export interface SelectDatabaseProps {
 | 
			
		||||
    onContinue: () => void;
 | 
			
		||||
@@ -27,6 +29,7 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({
 | 
			
		||||
    createNewDiagram,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { openImportDiagramDialog } = useDialog();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
@@ -38,11 +41,13 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({
 | 
			
		||||
                    {t('new_diagram_dialog.database_selection.description')}
 | 
			
		||||
                </DialogDescription>
 | 
			
		||||
            </DialogHeader>
 | 
			
		||||
            <SelectDatabaseContent
 | 
			
		||||
                databaseType={databaseType}
 | 
			
		||||
                onContinue={onContinue}
 | 
			
		||||
                setDatabaseType={setDatabaseType}
 | 
			
		||||
            />
 | 
			
		||||
            <DialogInternalContent>
 | 
			
		||||
                <SelectDatabaseContent
 | 
			
		||||
                    databaseType={databaseType}
 | 
			
		||||
                    onContinue={onContinue}
 | 
			
		||||
                    setDatabaseType={setDatabaseType}
 | 
			
		||||
                />
 | 
			
		||||
            </DialogInternalContent>
 | 
			
		||||
            <DialogFooter className="mt-4 flex !justify-between gap-2">
 | 
			
		||||
                {hasExistingDiagram ? (
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
@@ -51,7 +56,13 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </DialogClose>
 | 
			
		||||
                ) : (
 | 
			
		||||
                    <div></div>
 | 
			
		||||
                    <Button
 | 
			
		||||
                        type="button"
 | 
			
		||||
                        variant="ghost"
 | 
			
		||||
                        onClick={openImportDiagramDialog}
 | 
			
		||||
                    >
 | 
			
		||||
                        {t('new_diagram_dialog.import_from_file')}
 | 
			
		||||
                    </Button>
 | 
			
		||||
                )}
 | 
			
		||||
                <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2">
 | 
			
		||||
                    <Button
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										132
									
								
								src/dialogs/export-diagram-dialog/export-diagram-dialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,132 @@
 | 
			
		||||
import React, { useCallback, useEffect, useMemo, useState } 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 { SelectBoxOption } from '@/components/select-box/select-box';
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
export interface ExportDiagramDialogProps extends BaseDialogProps {}
 | 
			
		||||
 | 
			
		||||
export const ExportDiagramDialog: React.FC<ExportDiagramDialogProps> = ({
 | 
			
		||||
    dialog,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { diagramName, currentDiagram } = useChartDB();
 | 
			
		||||
    const [isLoading, setIsLoading] = useState(false);
 | 
			
		||||
    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 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);
 | 
			
		||||
            closeExportDiagramDialog();
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            setError(true);
 | 
			
		||||
            setIsLoading(false);
 | 
			
		||||
 | 
			
		||||
            throw e;
 | 
			
		||||
        }
 | 
			
		||||
    }, [downloadOutput, currentDiagram, closeExportDiagramDialog]);
 | 
			
		||||
 | 
			
		||||
    const outputTypeOptions: SelectBoxOption[] = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
            ['json'].map((format) => ({
 | 
			
		||||
                value: format,
 | 
			
		||||
                label: t(`export_diagram_dialog.format_${format}`),
 | 
			
		||||
            })),
 | 
			
		||||
        [t]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Dialog
 | 
			
		||||
            {...dialog}
 | 
			
		||||
            onOpenChange={(open) => {
 | 
			
		||||
                if (!open) {
 | 
			
		||||
                    closeExportDiagramDialog();
 | 
			
		||||
                }
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent className="flex flex-col" showClose>
 | 
			
		||||
                <DialogHeader>
 | 
			
		||||
                    <DialogTitle>
 | 
			
		||||
                        {t('export_diagram_dialog.title')}
 | 
			
		||||
                    </DialogTitle>
 | 
			
		||||
                    <DialogDescription>
 | 
			
		||||
                        {t('export_diagram_dialog.description')}
 | 
			
		||||
                    </DialogDescription>
 | 
			
		||||
                </DialogHeader>
 | 
			
		||||
                <div className="grid gap-4 py-1">
 | 
			
		||||
                    <div className="grid w-full items-center gap-4">
 | 
			
		||||
                        <SelectBox
 | 
			
		||||
                            options={outputTypeOptions}
 | 
			
		||||
                            multiple={false}
 | 
			
		||||
                            value="json"
 | 
			
		||||
                        />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {error ? (
 | 
			
		||||
                        <Alert variant="destructive">
 | 
			
		||||
                            <AlertCircle className="size-4" />
 | 
			
		||||
                            <AlertTitle>
 | 
			
		||||
                                {t('export_diagram_dialog.error.title')}
 | 
			
		||||
                            </AlertTitle>
 | 
			
		||||
                            <AlertDescription>
 | 
			
		||||
                                {t('export_diagram_dialog.error.description')}
 | 
			
		||||
                            </AlertDescription>
 | 
			
		||||
                        </Alert>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                </div>
 | 
			
		||||
                <DialogFooter className="flex gap-1 md:justify-between">
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
                        <Button variant="secondary">
 | 
			
		||||
                            {t('export_diagram_dialog.cancel')}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </DialogClose>
 | 
			
		||||
                    <Button onClick={handleExport} disabled={isLoading}>
 | 
			
		||||
                        {isLoading ? (
 | 
			
		||||
                            <Spinner className="mr-1 size-5 text-primary-foreground" />
 | 
			
		||||
                        ) : null}
 | 
			
		||||
                        {t('export_diagram_dialog.export')}
 | 
			
		||||
                    </Button>
 | 
			
		||||
                </DialogFooter>
 | 
			
		||||
            </DialogContent>
 | 
			
		||||
        </Dialog>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -7,6 +7,7 @@ import {
 | 
			
		||||
    DialogDescription,
 | 
			
		||||
    DialogFooter,
 | 
			
		||||
    DialogHeader,
 | 
			
		||||
    DialogInternalContent,
 | 
			
		||||
    DialogTitle,
 | 
			
		||||
} from '@/components/dialog/dialog';
 | 
			
		||||
import { Label } from '@/components/label/label';
 | 
			
		||||
@@ -20,7 +21,7 @@ import {
 | 
			
		||||
import { databaseTypeToLabelMap } from '@/lib/databases';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import { Annoyed, Sparkles } from 'lucide-react';
 | 
			
		||||
import React, { useCallback, useEffect } from 'react';
 | 
			
		||||
import React, { useCallback, useEffect, useRef } from 'react';
 | 
			
		||||
import { Trans, useTranslation } from 'react-i18next';
 | 
			
		||||
import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
			
		||||
 | 
			
		||||
@@ -37,28 +38,47 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const [script, setScript] = React.useState<string>();
 | 
			
		||||
    const [error, setError] = React.useState<boolean>(false);
 | 
			
		||||
    const [isScriptLoading, setIsScriptLoading] =
 | 
			
		||||
        React.useState<boolean>(false);
 | 
			
		||||
    const abortControllerRef = useRef<AbortController | null>(null);
 | 
			
		||||
 | 
			
		||||
    const exportSQLScript = useCallback(async () => {
 | 
			
		||||
        if (targetDatabaseType === DatabaseType.GENERIC) {
 | 
			
		||||
            return Promise.resolve(exportBaseSQL(currentDiagram));
 | 
			
		||||
        } else {
 | 
			
		||||
            return exportSQL(currentDiagram, targetDatabaseType);
 | 
			
		||||
            return exportSQL(currentDiagram, targetDatabaseType, {
 | 
			
		||||
                stream: true,
 | 
			
		||||
                onResultStream: (text) =>
 | 
			
		||||
                    setScript((prev) => (prev ? prev + text : text)),
 | 
			
		||||
                signal: abortControllerRef.current?.signal,
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
    }, [targetDatabaseType, currentDiagram]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!dialog.open) return;
 | 
			
		||||
        if (!dialog.open) {
 | 
			
		||||
            abortControllerRef.current?.abort();
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        abortControllerRef.current = new AbortController();
 | 
			
		||||
        setScript(undefined);
 | 
			
		||||
        setError(false);
 | 
			
		||||
        const fetchScript = async () => {
 | 
			
		||||
            try {
 | 
			
		||||
                setIsScriptLoading(true);
 | 
			
		||||
                const script = await exportSQLScript();
 | 
			
		||||
                setScript(script);
 | 
			
		||||
                setIsScriptLoading(false);
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                setError(true);
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        fetchScript();
 | 
			
		||||
 | 
			
		||||
        return () => {
 | 
			
		||||
            abortControllerRef.current?.abort();
 | 
			
		||||
        };
 | 
			
		||||
    }, [dialog.open, setScript, exportSQLScript, setError]);
 | 
			
		||||
 | 
			
		||||
    const renderError = useCallback(
 | 
			
		||||
@@ -132,7 +152,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent
 | 
			
		||||
                className="flex max-h-[80vh] flex-col overflow-y-auto xl:min-w-[75vw]"
 | 
			
		||||
                className="flex max-h-screen flex-col overflow-y-auto xl:min-w-[75vw]"
 | 
			
		||||
                showClose
 | 
			
		||||
            >
 | 
			
		||||
                <DialogHeader>
 | 
			
		||||
@@ -148,18 +168,24 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
 | 
			
		||||
                        })}
 | 
			
		||||
                    </DialogDescription>
 | 
			
		||||
                </DialogHeader>
 | 
			
		||||
                <div className="flex flex-1 items-center justify-center">
 | 
			
		||||
                    {error ? (
 | 
			
		||||
                        renderError()
 | 
			
		||||
                    ) : script === undefined ? (
 | 
			
		||||
                        renderLoader()
 | 
			
		||||
                    ) : script.length === 0 ? (
 | 
			
		||||
                        renderError()
 | 
			
		||||
                    ) : (
 | 
			
		||||
                        <CodeSnippet className="h-96 w-full" code={script!} />
 | 
			
		||||
                    )}
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <DialogInternalContent>
 | 
			
		||||
                    <div className="flex flex-1 items-center justify-center">
 | 
			
		||||
                        {error ? (
 | 
			
		||||
                            renderError()
 | 
			
		||||
                        ) : script === undefined ? (
 | 
			
		||||
                            renderLoader()
 | 
			
		||||
                        ) : script.length === 0 ? (
 | 
			
		||||
                            renderError()
 | 
			
		||||
                        ) : (
 | 
			
		||||
                            <CodeSnippet
 | 
			
		||||
                                className="h-96 w-full"
 | 
			
		||||
                                code={script!}
 | 
			
		||||
                                autoScroll={true}
 | 
			
		||||
                                isComplete={!isScriptLoading}
 | 
			
		||||
                            />
 | 
			
		||||
                        )}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </DialogInternalContent>
 | 
			
		||||
                <DialogFooter className="flex !justify-between gap-2">
 | 
			
		||||
                    <div />
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
 
 | 
			
		||||
@@ -323,7 +323,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent
 | 
			
		||||
                className="flex w-[90vw] flex-col overflow-y-auto md:overflow-visible xl:min-w-[45vw]"
 | 
			
		||||
                className="flex max-h-screen w-[90vw] flex-col overflow-y-auto md:overflow-visible xl:min-w-[45vw]"
 | 
			
		||||
                showClose
 | 
			
		||||
            >
 | 
			
		||||
                <ImportDatabase
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										134
									
								
								src/dialogs/import-diagram-dialog/import-diagram-dialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,134 @@
 | 
			
		||||
import React, { useCallback, useEffect, useState } from 'react';
 | 
			
		||||
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 { FileUploader } from '@/components/file-uploader/file-uploader';
 | 
			
		||||
import { useStorage } from '@/hooks/use-storage';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import { diagramFromJSONInput } from '@/lib/export-import-utils';
 | 
			
		||||
import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert';
 | 
			
		||||
import { AlertCircle } from 'lucide-react';
 | 
			
		||||
 | 
			
		||||
export interface ImportDiagramDialogProps extends BaseDialogProps {}
 | 
			
		||||
 | 
			
		||||
export const ImportDiagramDialog: React.FC<ImportDiagramDialogProps> = ({
 | 
			
		||||
    dialog,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const [file, setFile] = useState<File | null>(null);
 | 
			
		||||
    const { addDiagram } = useStorage();
 | 
			
		||||
    const navigate = useNavigate();
 | 
			
		||||
    const [error, setError] = useState(false);
 | 
			
		||||
 | 
			
		||||
    const onFileChange = useCallback((files: File[]) => {
 | 
			
		||||
        if (files.length === 0) {
 | 
			
		||||
            setFile(null);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setFile(files[0]);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!dialog.open) return;
 | 
			
		||||
        setError(false);
 | 
			
		||||
        setFile(null);
 | 
			
		||||
    }, [dialog.open]);
 | 
			
		||||
    const { closeImportDiagramDialog, closeCreateDiagramDialog } = useDialog();
 | 
			
		||||
 | 
			
		||||
    const handleImport = useCallback(() => {
 | 
			
		||||
        if (!file) return;
 | 
			
		||||
 | 
			
		||||
        const reader = new FileReader();
 | 
			
		||||
        reader.onload = async (e) => {
 | 
			
		||||
            const json = e.target?.result;
 | 
			
		||||
            if (typeof json !== 'string') return;
 | 
			
		||||
 | 
			
		||||
            try {
 | 
			
		||||
                const diagram = diagramFromJSONInput(json);
 | 
			
		||||
 | 
			
		||||
                await addDiagram({ diagram });
 | 
			
		||||
 | 
			
		||||
                closeImportDiagramDialog();
 | 
			
		||||
                closeCreateDiagramDialog();
 | 
			
		||||
 | 
			
		||||
                navigate(`/diagrams/${diagram.id}`);
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                setError(true);
 | 
			
		||||
 | 
			
		||||
                throw e;
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        reader.readAsText(file);
 | 
			
		||||
    }, [
 | 
			
		||||
        file,
 | 
			
		||||
        addDiagram,
 | 
			
		||||
        navigate,
 | 
			
		||||
        closeImportDiagramDialog,
 | 
			
		||||
        closeCreateDiagramDialog,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Dialog
 | 
			
		||||
            {...dialog}
 | 
			
		||||
            onOpenChange={(open) => {
 | 
			
		||||
                if (!open) {
 | 
			
		||||
                    closeImportDiagramDialog();
 | 
			
		||||
                }
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent className="flex max-h-screen flex-col" showClose>
 | 
			
		||||
                <DialogHeader>
 | 
			
		||||
                    <DialogTitle>
 | 
			
		||||
                        {t('import_diagram_dialog.title')}
 | 
			
		||||
                    </DialogTitle>
 | 
			
		||||
                    <DialogDescription>
 | 
			
		||||
                        {t('import_diagram_dialog.description')}
 | 
			
		||||
                    </DialogDescription>
 | 
			
		||||
                </DialogHeader>
 | 
			
		||||
                <DialogInternalContent>
 | 
			
		||||
                    <div className="flex flex-col p-1">
 | 
			
		||||
                        <FileUploader
 | 
			
		||||
                            supportedExtensions={['.json']}
 | 
			
		||||
                            onFilesChange={onFileChange}
 | 
			
		||||
                        />
 | 
			
		||||
                        {error ? (
 | 
			
		||||
                            <Alert variant="destructive" className="mt-2">
 | 
			
		||||
                                <AlertCircle className="size-4" />
 | 
			
		||||
                                <AlertTitle>
 | 
			
		||||
                                    {t('import_diagram_dialog.error.title')}
 | 
			
		||||
                                </AlertTitle>
 | 
			
		||||
                                <AlertDescription>
 | 
			
		||||
                                    {t(
 | 
			
		||||
                                        'import_diagram_dialog.error.description'
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </AlertDescription>
 | 
			
		||||
                            </Alert>
 | 
			
		||||
                        ) : null}
 | 
			
		||||
                    </div>
 | 
			
		||||
                </DialogInternalContent>
 | 
			
		||||
                <DialogFooter className="flex gap-1 md:justify-between">
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
                        <Button variant="secondary">
 | 
			
		||||
                            {t('import_diagram_dialog.cancel')}
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </DialogClose>
 | 
			
		||||
                    <Button onClick={handleImport} disabled={file === null}>
 | 
			
		||||
                        {t('import_diagram_dialog.import')}
 | 
			
		||||
                    </Button>
 | 
			
		||||
                </DialogFooter>
 | 
			
		||||
            </DialogContent>
 | 
			
		||||
        </Dialog>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -7,9 +7,9 @@ import {
 | 
			
		||||
    DialogDescription,
 | 
			
		||||
    DialogFooter,
 | 
			
		||||
    DialogHeader,
 | 
			
		||||
    DialogInternalContent,
 | 
			
		||||
    DialogTitle,
 | 
			
		||||
} from '@/components/dialog/dialog';
 | 
			
		||||
import { ScrollArea } from '@/components/scroll-area/scroll-area';
 | 
			
		||||
import {
 | 
			
		||||
    Table,
 | 
			
		||||
    TableBody,
 | 
			
		||||
@@ -74,7 +74,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent
 | 
			
		||||
                className="flex w-[90vw] flex-col overflow-y-auto md:w-screen xl:min-w-[55vw]"
 | 
			
		||||
                className="flex h-[30rem] max-h-screen w-[90vw] flex-col overflow-y-auto md:w-screen xl:min-w-[55vw]"
 | 
			
		||||
                showClose
 | 
			
		||||
            >
 | 
			
		||||
                <DialogHeader>
 | 
			
		||||
@@ -83,9 +83,9 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
 | 
			
		||||
                        {t('open_diagram_dialog.description')}
 | 
			
		||||
                    </DialogDescription>
 | 
			
		||||
                </DialogHeader>
 | 
			
		||||
                <div className="flex flex-1 items-center justify-center">
 | 
			
		||||
                    <ScrollArea className="h-80 w-full">
 | 
			
		||||
                        <Table className="h-fit">
 | 
			
		||||
                <DialogInternalContent>
 | 
			
		||||
                    <div className="flex flex-1 items-center justify-center">
 | 
			
		||||
                        <Table>
 | 
			
		||||
                            <TableHeader className="sticky top-0 bg-background">
 | 
			
		||||
                                <TableRow>
 | 
			
		||||
                                    <TableHead />
 | 
			
		||||
@@ -155,8 +155,8 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
 | 
			
		||||
                                ))}
 | 
			
		||||
                            </TableBody>
 | 
			
		||||
                        </Table>
 | 
			
		||||
                    </ScrollArea>
 | 
			
		||||
                </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </DialogInternalContent>
 | 
			
		||||
 | 
			
		||||
                <DialogFooter className="flex !justify-between gap-2">
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										128
									
								
								src/globals.css
									
									
									
									
									
								
							
							
						
						@@ -3,69 +3,73 @@
 | 
			
		||||
@tailwind utilities;
 | 
			
		||||
 | 
			
		||||
@layer base {
 | 
			
		||||
  :root {
 | 
			
		||||
    --background: 0 0% 100%;
 | 
			
		||||
    --foreground: 222.2 84% 4.9%;
 | 
			
		||||
    --card: 0 0% 100%;
 | 
			
		||||
    --card-foreground: 222.2 84% 4.9%;
 | 
			
		||||
    --popover: 0 0% 100%;
 | 
			
		||||
    --popover-foreground: 222.2 84% 4.9%;
 | 
			
		||||
    --primary: 222.2 47.4% 11.2%;
 | 
			
		||||
    --primary-foreground: 210 40% 98%;
 | 
			
		||||
    --secondary: 210 40% 96.1%;
 | 
			
		||||
    --secondary-foreground: 222.2 47.4% 11.2%;
 | 
			
		||||
    --muted: 210 40% 96.1%;
 | 
			
		||||
    --muted-foreground: 215.4 16.3% 46.9%;
 | 
			
		||||
    --accent: 210 40% 96.1%;
 | 
			
		||||
    --accent-foreground: 222.2 47.4% 11.2%;
 | 
			
		||||
    --destructive: 0 84.2% 60.2%;
 | 
			
		||||
    --destructive-foreground: 210 40% 98%;
 | 
			
		||||
    --border: 214.3 31.8% 91.4%;
 | 
			
		||||
    --input: 214.3 31.8% 91.4%;
 | 
			
		||||
    --ring: 222.2 84% 4.9%;
 | 
			
		||||
    --radius: 0.5rem;
 | 
			
		||||
    --chart-1: 12 76% 61%;
 | 
			
		||||
    --chart-2: 173 58% 39%;
 | 
			
		||||
    --chart-3: 197 37% 24%;
 | 
			
		||||
    --chart-4: 43 74% 66%;
 | 
			
		||||
    --chart-5: 27 87% 67%;
 | 
			
		||||
    --subtitle: 215.3 19.3% 34.5%;
 | 
			
		||||
  }
 | 
			
		||||
    :root {
 | 
			
		||||
        --background: 0 0% 100%;
 | 
			
		||||
        --foreground: 222.2 84% 4.9%;
 | 
			
		||||
        --card: 0 0% 100%;
 | 
			
		||||
        --card-foreground: 222.2 84% 4.9%;
 | 
			
		||||
        --popover: 0 0% 100%;
 | 
			
		||||
        --popover-foreground: 222.2 84% 4.9%;
 | 
			
		||||
        --primary: 222.2 47.4% 11.2%;
 | 
			
		||||
        --primary-foreground: 210 40% 98%;
 | 
			
		||||
        --secondary: 210 40% 96.1%;
 | 
			
		||||
        --secondary-foreground: 222.2 47.4% 11.2%;
 | 
			
		||||
        --muted: 210 40% 96.1%;
 | 
			
		||||
        --muted-foreground: 215.4 16.3% 46.9%;
 | 
			
		||||
        --accent: 210 40% 96.1%;
 | 
			
		||||
        --accent-foreground: 222.2 47.4% 11.2%;
 | 
			
		||||
        --destructive: 0 84.2% 60.2%;
 | 
			
		||||
        --destructive-foreground: 210 40% 98%;
 | 
			
		||||
        --border: 214.3 31.8% 91.4%;
 | 
			
		||||
        --input: 214.3 31.8% 91.4%;
 | 
			
		||||
        --ring: 222.2 84% 4.9%;
 | 
			
		||||
        --radius: 0.5rem;
 | 
			
		||||
        --chart-1: 12 76% 61%;
 | 
			
		||||
        --chart-2: 173 58% 39%;
 | 
			
		||||
        --chart-3: 197 37% 24%;
 | 
			
		||||
        --chart-4: 43 74% 66%;
 | 
			
		||||
        --chart-5: 27 87% 67%;
 | 
			
		||||
        --subtitle: 215.3 19.3% 34.5%;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  .dark {
 | 
			
		||||
    --background: 222.2 84% 4.9%;
 | 
			
		||||
    --foreground: 210 40% 98%;
 | 
			
		||||
    --card: 222.2 84% 4.9%;
 | 
			
		||||
    --card-foreground: 210 40% 98%;
 | 
			
		||||
    --popover: 222.2 84% 4.9%;
 | 
			
		||||
    --popover-foreground: 210 40% 98%;
 | 
			
		||||
    --primary: 210 40% 98%;
 | 
			
		||||
    --primary-foreground: 222.2 47.4% 11.2%;
 | 
			
		||||
    --secondary: 217.2 32.6% 17.5%;
 | 
			
		||||
    --secondary-foreground: 210 40% 98%;
 | 
			
		||||
    --muted: 217.2 32.6% 17.5%;
 | 
			
		||||
    --muted-foreground: 215 20.2% 65.1%;
 | 
			
		||||
    --accent: 217.2 32.6% 17.5%;
 | 
			
		||||
    --accent-foreground: 210 40% 98%;
 | 
			
		||||
    --destructive: 0 62.8% 30.6%;
 | 
			
		||||
    --destructive-foreground: 210 40% 98%;
 | 
			
		||||
    --border: 217.2 32.6% 17.5%;
 | 
			
		||||
    --input: 217.2 32.6% 17.5%;
 | 
			
		||||
    --ring: 212.7 26.8% 83.9%;
 | 
			
		||||
    --chart-1: 220 70% 50%;
 | 
			
		||||
    --chart-2: 160 60% 45%;
 | 
			
		||||
    --chart-3: 30 80% 55%;
 | 
			
		||||
    --chart-4: 280 65% 60%;
 | 
			
		||||
    --chart-5: 340 75% 55%;
 | 
			
		||||
    --subtitle: 212.7 26.8% 83.9%;
 | 
			
		||||
  }
 | 
			
		||||
    .dark {
 | 
			
		||||
        --background: 222.2 84% 4.9%;
 | 
			
		||||
        --foreground: 210 40% 98%;
 | 
			
		||||
        --card: 222.2 84% 4.9%;
 | 
			
		||||
        --card-foreground: 210 40% 98%;
 | 
			
		||||
        --popover: 222.2 84% 4.9%;
 | 
			
		||||
        --popover-foreground: 210 40% 98%;
 | 
			
		||||
        --primary: 210 40% 98%;
 | 
			
		||||
        --primary-foreground: 222.2 47.4% 11.2%;
 | 
			
		||||
        --secondary: 217.2 32.6% 17.5%;
 | 
			
		||||
        --secondary-foreground: 210 40% 98%;
 | 
			
		||||
        --muted: 217.2 32.6% 17.5%;
 | 
			
		||||
        --muted-foreground: 215 20.2% 65.1%;
 | 
			
		||||
        --accent: 217.2 32.6% 17.5%;
 | 
			
		||||
        --accent-foreground: 210 40% 98%;
 | 
			
		||||
        --destructive: 0 62.8% 30.6%;
 | 
			
		||||
        --destructive-foreground: 210 40% 98%;
 | 
			
		||||
        --border: 217.2 32.6% 17.5%;
 | 
			
		||||
        --input: 217.2 32.6% 17.5%;
 | 
			
		||||
        --ring: 212.7 26.8% 83.9%;
 | 
			
		||||
        --chart-1: 220 70% 50%;
 | 
			
		||||
        --chart-2: 160 60% 45%;
 | 
			
		||||
        --chart-3: 30 80% 55%;
 | 
			
		||||
        --chart-4: 280 65% 60%;
 | 
			
		||||
        --chart-5: 340 75% 55%;
 | 
			
		||||
        --subtitle: 212.7 26.8% 83.9%;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@layer base {
 | 
			
		||||
  * {
 | 
			
		||||
    @apply border-border;
 | 
			
		||||
  }
 | 
			
		||||
  body {
 | 
			
		||||
    @apply bg-background text-foreground;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
    * {
 | 
			
		||||
        @apply border-border;
 | 
			
		||||
    }
 | 
			
		||||
    body {
 | 
			
		||||
        @apply bg-background text-foreground;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .text-editable {
 | 
			
		||||
        @apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] rounded-md cursor-pointer;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ export const HelmetData: React.FC = () => (
 | 
			
		||||
        />
 | 
			
		||||
        <meta
 | 
			
		||||
            property="og:image"
 | 
			
		||||
            content="https://app.chartdb.io/ChartDB.png"
 | 
			
		||||
            content="https://app.chartdb.io/chartdb.png"
 | 
			
		||||
        />
 | 
			
		||||
        <meta property="og:url" content="https://app.chartdb.io" />
 | 
			
		||||
        <meta name="twitter:card" content="summary_large_image" />
 | 
			
		||||
@@ -32,7 +32,7 @@ export const HelmetData: React.FC = () => (
 | 
			
		||||
        />
 | 
			
		||||
        <meta
 | 
			
		||||
            name="twitter:image"
 | 
			
		||||
            content="https://github.com/chartdb/chartdb/raw/main/public/ChartDB.png"
 | 
			
		||||
            content="https://github.com/chartdb/chartdb/raw/main/public/chartdb.png"
 | 
			
		||||
        />
 | 
			
		||||
        <title>ChartDB - Database schema diagrams visualizer</title>
 | 
			
		||||
    </Helmet>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,48 @@
 | 
			
		||||
import i18n from 'i18next';
 | 
			
		||||
import { initReactI18next } from 'react-i18next';
 | 
			
		||||
import LanguageDetector from 'i18next-browser-languagedetector';
 | 
			
		||||
import type { LanguageMetadata } from './types';
 | 
			
		||||
import { en, enMetadata } from './locales/en';
 | 
			
		||||
import { es } from './locales/es';
 | 
			
		||||
import { fr } from './locales/fr';
 | 
			
		||||
import { de } from './locales/de';
 | 
			
		||||
import { hi } from './locales/hi';
 | 
			
		||||
import { ja } from './locales/ja';
 | 
			
		||||
import { pt_BR } from './locales/pt_BR';
 | 
			
		||||
import { es, esMetadata } from './locales/es';
 | 
			
		||||
import { fr, frMetadata } from './locales/fr';
 | 
			
		||||
import { de, deMetadata } from './locales/de';
 | 
			
		||||
import { hi, hiMetadata } from './locales/hi';
 | 
			
		||||
import { ja, jaMetadata } from './locales/ja';
 | 
			
		||||
import { ko_KR, ko_KRMetadata } from './locales/ko_KR';
 | 
			
		||||
import { pt_BR, pt_BRMetadata } from './locales/pt_BR';
 | 
			
		||||
import { uk, ukMetadata } from './locales/uk';
 | 
			
		||||
import { ru, ruMetadata } from './locales/ru';
 | 
			
		||||
import { zh_CN, zh_CNMetadata } from './locales/zh_CN';
 | 
			
		||||
import { zh_TW, zh_TWMetadata } from './locales/zh_TW';
 | 
			
		||||
import { ne, neMetadata } from './locales/ne';
 | 
			
		||||
import { mr, mrMetadata } from './locales/mr';
 | 
			
		||||
import { tr, trMetadata } from './locales/tr';
 | 
			
		||||
import { id_ID, id_IDMetadata } from './locales/id_ID';
 | 
			
		||||
import { te, teMetadata } from './locales/te';
 | 
			
		||||
import { gu, guMetadata } from './locales/gu';
 | 
			
		||||
import { vi, viMetadata } from './locales/vi';
 | 
			
		||||
 | 
			
		||||
export const languages: LanguageMetadata[] = [
 | 
			
		||||
    enMetadata,
 | 
			
		||||
    esMetadata,
 | 
			
		||||
    frMetadata,
 | 
			
		||||
    deMetadata,
 | 
			
		||||
    hiMetadata,
 | 
			
		||||
    jaMetadata,
 | 
			
		||||
    ko_KRMetadata,
 | 
			
		||||
    pt_BRMetadata,
 | 
			
		||||
    ukMetadata,
 | 
			
		||||
    ruMetadata,
 | 
			
		||||
    zh_CNMetadata,
 | 
			
		||||
    zh_TWMetadata,
 | 
			
		||||
    neMetadata,
 | 
			
		||||
    mrMetadata,
 | 
			
		||||
    trMetadata,
 | 
			
		||||
    id_IDMetadata,
 | 
			
		||||
    teMetadata,
 | 
			
		||||
    guMetadata,
 | 
			
		||||
    viMetadata,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const resources = {
 | 
			
		||||
    en,
 | 
			
		||||
@@ -15,17 +51,30 @@ const resources = {
 | 
			
		||||
    de,
 | 
			
		||||
    hi,
 | 
			
		||||
    ja,
 | 
			
		||||
    ko_KR,
 | 
			
		||||
    pt_BR,
 | 
			
		||||
    uk,
 | 
			
		||||
    ru,
 | 
			
		||||
    zh_CN,
 | 
			
		||||
    zh_TW,
 | 
			
		||||
    ne,
 | 
			
		||||
    mr,
 | 
			
		||||
    tr,
 | 
			
		||||
    id_ID,
 | 
			
		||||
    te,
 | 
			
		||||
    gu,
 | 
			
		||||
    vi,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
i18n.use(initReactI18next).init({
 | 
			
		||||
    resources,
 | 
			
		||||
    lng: enMetadata.code,
 | 
			
		||||
    interpolation: {
 | 
			
		||||
        escapeValue: false,
 | 
			
		||||
    },
 | 
			
		||||
    fallbackLng: enMetadata.code,
 | 
			
		||||
    debug: false,
 | 
			
		||||
});
 | 
			
		||||
i18n.use(LanguageDetector)
 | 
			
		||||
    .use(initReactI18next)
 | 
			
		||||
    .init({
 | 
			
		||||
        resources,
 | 
			
		||||
        interpolation: {
 | 
			
		||||
            escapeValue: false,
 | 
			
		||||
        },
 | 
			
		||||
        fallbackLng: enMetadata.code,
 | 
			
		||||
        debug: false,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
export { i18n };
 | 
			
		||||
 
 | 
			
		||||
@@ -28,10 +28,15 @@ export const de: LanguageTranslation = {
 | 
			
		||||
                show_cardinality: 'Kardinalität anzeigen',
 | 
			
		||||
                zoom_on_scroll: 'Zoom beim Scrollen',
 | 
			
		||||
                theme: 'Stil',
 | 
			
		||||
                change_language: 'Sprache',
 | 
			
		||||
                show_dependencies: 'Abhängigkeiten anzeigen',
 | 
			
		||||
                hide_dependencies: 'Abhängigkeiten ausblenden',
 | 
			
		||||
            },
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Share',
 | 
			
		||||
                export_diagram: 'Export Diagram',
 | 
			
		||||
                import_diagram: 'Import Diagram',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'Hilfe',
 | 
			
		||||
                visit_website: 'ChartDB Webseite',
 | 
			
		||||
@@ -139,6 +144,7 @@ export const de: LanguageTranslation = {
 | 
			
		||||
                        change_schema: 'Schema ändern',
 | 
			
		||||
                        add_field: 'Feld hinzufügen',
 | 
			
		||||
                        add_index: 'Index hinzufügen',
 | 
			
		||||
                        duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
                        delete_table: 'Tabelle löschen',
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
@@ -226,6 +232,8 @@ export const de: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
            cancel: 'Abbrechen',
 | 
			
		||||
            back: 'Zurück',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            import_from_file: 'Import from File',
 | 
			
		||||
            empty_diagram: 'Leeres Diagramm',
 | 
			
		||||
            continue: 'Weiter',
 | 
			
		||||
            import: 'Importieren',
 | 
			
		||||
@@ -329,7 +337,31 @@ export const de: LanguageTranslation = {
 | 
			
		||||
            close: 'Nicht jetzt',
 | 
			
		||||
            confirm: 'Natürlich!',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        export_diagram_dialog: {
 | 
			
		||||
            title: 'Export Diagram',
 | 
			
		||||
            description: 'Choose the format for export:',
 | 
			
		||||
            format_json: 'JSON',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            export: 'Export',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error exporting diagram',
 | 
			
		||||
                description:
 | 
			
		||||
                    'Something went wrong. Need help? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_diagram_dialog: {
 | 
			
		||||
            title: 'Import Diagram',
 | 
			
		||||
            description: 'Paste the diagram JSON below:',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error importing diagram',
 | 
			
		||||
                description:
 | 
			
		||||
                    'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'Ein zu Eins (1:1)',
 | 
			
		||||
            one_to_many: 'Ein zu Viele (1:n)',
 | 
			
		||||
@@ -344,12 +376,25 @@ export const de: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        table_node_context_menu: {
 | 
			
		||||
            edit_table: 'Tabelle bearbeiten',
 | 
			
		||||
            duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
            delete_table: 'Tabelle löschen',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // TODO: Add translations
 | 
			
		||||
        snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
 | 
			
		||||
 | 
			
		||||
        tool_tips: {
 | 
			
		||||
            double_click_to_edit: 'Doppelklicken zum Bearbeiten',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        language_select: {
 | 
			
		||||
            change_language: 'Sprache',
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const deMetadata: LanguageMetadata = {
 | 
			
		||||
    name: 'Deutsch',
 | 
			
		||||
    name: 'German',
 | 
			
		||||
    nativeName: 'Deutsch',
 | 
			
		||||
    code: 'de',
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -28,10 +28,14 @@ export const en = {
 | 
			
		||||
                show_cardinality: 'Show Cardinality',
 | 
			
		||||
                zoom_on_scroll: 'Zoom on Scroll',
 | 
			
		||||
                theme: 'Theme',
 | 
			
		||||
                change_language: 'Language',
 | 
			
		||||
                show_dependencies: 'Show Dependencies',
 | 
			
		||||
                hide_dependencies: 'Hide Dependencies',
 | 
			
		||||
            },
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Share',
 | 
			
		||||
                export_diagram: 'Export Diagram',
 | 
			
		||||
                import_diagram: 'Import Diagram',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'Help',
 | 
			
		||||
                visit_website: 'Visit ChartDB',
 | 
			
		||||
@@ -139,6 +143,7 @@ export const en = {
 | 
			
		||||
                        change_schema: 'Change Schema',
 | 
			
		||||
                        add_field: 'Add Field',
 | 
			
		||||
                        add_index: 'Add Index',
 | 
			
		||||
                        duplicate_table: 'Duplicate Table',
 | 
			
		||||
                        delete_table: 'Delete Table',
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
@@ -224,6 +229,7 @@ export const en = {
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            import_from_file: 'Import from File',
 | 
			
		||||
            back: 'Back',
 | 
			
		||||
            empty_diagram: 'Empty diagram',
 | 
			
		||||
            continue: 'Continue',
 | 
			
		||||
@@ -328,7 +334,30 @@ export const en = {
 | 
			
		||||
            close: 'Not now',
 | 
			
		||||
            confirm: 'Of course!',
 | 
			
		||||
        },
 | 
			
		||||
        export_diagram_dialog: {
 | 
			
		||||
            title: 'Export Diagram',
 | 
			
		||||
            description: 'Choose the format for export:',
 | 
			
		||||
            format_json: 'JSON',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            export: 'Export',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error exporting diagram',
 | 
			
		||||
                description:
 | 
			
		||||
                    'Something went wrong. Need help? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        import_diagram_dialog: {
 | 
			
		||||
            title: 'Import Diagram',
 | 
			
		||||
            description: 'Paste the diagram JSON below:',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error importing diagram',
 | 
			
		||||
                description:
 | 
			
		||||
                    'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'One to One',
 | 
			
		||||
            one_to_many: 'One to Many',
 | 
			
		||||
@@ -343,12 +372,24 @@ export const en = {
 | 
			
		||||
 | 
			
		||||
        table_node_context_menu: {
 | 
			
		||||
            edit_table: 'Edit Table',
 | 
			
		||||
            duplicate_table: 'Duplicate Table',
 | 
			
		||||
            delete_table: 'Delete Table',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
 | 
			
		||||
 | 
			
		||||
        tool_tips: {
 | 
			
		||||
            double_click_to_edit: 'Double-click to edit',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        language_select: {
 | 
			
		||||
            change_language: 'Language',
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const enMetadata: LanguageMetadata = {
 | 
			
		||||
    name: 'English',
 | 
			
		||||
    nativeName: 'English',
 | 
			
		||||
    code: 'en',
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -28,10 +28,14 @@ export const es: LanguageTranslation = {
 | 
			
		||||
                hide_sidebar: 'Ocultar Barra Lateral',
 | 
			
		||||
                zoom_on_scroll: 'Zoom al Desplazarse',
 | 
			
		||||
                theme: 'Tema',
 | 
			
		||||
                change_language: 'Idioma',
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                show_dependencies: 'Show Dependencies',
 | 
			
		||||
                hide_dependencies: 'Hide Dependencies',
 | 
			
		||||
                show_dependencies: 'Mostrar dependencias',
 | 
			
		||||
                hide_dependencies: 'Ocultar dependencias',
 | 
			
		||||
            },
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            share: {
 | 
			
		||||
                share: 'Share',
 | 
			
		||||
                export_diagram: 'Export Diagram',
 | 
			
		||||
                import_diagram: 'Import Diagram',
 | 
			
		||||
            },
 | 
			
		||||
            help: {
 | 
			
		||||
                help: 'Ayuda',
 | 
			
		||||
@@ -80,20 +84,19 @@ export const es: LanguageTranslation = {
 | 
			
		||||
        saved: 'Guardado',
 | 
			
		||||
        diagrams: 'Diagramas',
 | 
			
		||||
        loading_diagram: 'Cargando diagrama...',
 | 
			
		||||
        deselect_all: 'Deselect All', // TODO: Translate
 | 
			
		||||
        select_all: 'Select All', // TODO: Translate
 | 
			
		||||
        clear: 'Clear', // TODO: Translate
 | 
			
		||||
        show_more: 'Show More', // TODO: Translate
 | 
			
		||||
        show_less: 'Show Less', // TODO: Translate
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        deselect_all: 'Deseleccionar todo',
 | 
			
		||||
        select_all: 'Seleccionar todo',
 | 
			
		||||
        clear: 'Limpiar',
 | 
			
		||||
        show_more: 'Mostrar más',
 | 
			
		||||
        show_less: 'Mostrar menos',
 | 
			
		||||
        copy_to_clipboard: 'Copy to Clipboard',
 | 
			
		||||
        copied: 'Copied!',
 | 
			
		||||
 | 
			
		||||
        side_panel: {
 | 
			
		||||
            schema: 'Schema:', // TODO: Translate
 | 
			
		||||
            filter_by_schema: 'Filter by schema', // TODO: Translate
 | 
			
		||||
            search_schema: 'Search schema...', // TODO: Translate
 | 
			
		||||
            no_schemas_found: 'No schemas found.', // TODO: Translate
 | 
			
		||||
            schema: 'Esquema:',
 | 
			
		||||
            filter_by_schema: 'Filtrar por esquema',
 | 
			
		||||
            search_schema: 'Buscar esquema...',
 | 
			
		||||
            no_schemas_found: 'No se encontraron esquemas.',
 | 
			
		||||
            view_all_options: 'Ver todas las opciones...',
 | 
			
		||||
            tables_section: {
 | 
			
		||||
                tables: 'Tablas',
 | 
			
		||||
@@ -113,7 +116,7 @@ export const es: LanguageTranslation = {
 | 
			
		||||
                    index_select_fields: 'Seleccionar campos',
 | 
			
		||||
                    field_name: 'Nombre',
 | 
			
		||||
                    field_type: 'Tipo',
 | 
			
		||||
                    no_types_found: 'No types found', // TODO: Translate
 | 
			
		||||
                    no_types_found: 'No se encontraron tipos',
 | 
			
		||||
                    field_actions: {
 | 
			
		||||
                        title: 'Atributos del Campo',
 | 
			
		||||
                        unique: 'Único',
 | 
			
		||||
@@ -132,6 +135,7 @@ export const es: LanguageTranslation = {
 | 
			
		||||
                        change_schema: 'Cambiar Esquema',
 | 
			
		||||
                        add_field: 'Agregar Campo',
 | 
			
		||||
                        add_index: 'Agregar Índice',
 | 
			
		||||
                        duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
                        delete_table: 'Eliminar Tabla',
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
@@ -160,23 +164,22 @@ export const es: LanguageTranslation = {
 | 
			
		||||
                    description: 'Crea una relación para conectar tablas',
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            dependencies_section: {
 | 
			
		||||
                dependencies: 'Dependencies',
 | 
			
		||||
                filter: 'Filter',
 | 
			
		||||
                collapse: 'Collapse All',
 | 
			
		||||
                dependencies: 'Dependencias',
 | 
			
		||||
                filter: 'Filtro',
 | 
			
		||||
                collapse: 'Colapsar todo',
 | 
			
		||||
                dependency: {
 | 
			
		||||
                    table: 'Table',
 | 
			
		||||
                    dependent_table: 'Dependent View',
 | 
			
		||||
                    delete_dependency: 'Delete',
 | 
			
		||||
                    table: 'Tabla',
 | 
			
		||||
                    dependent_table: 'Vista dependiente',
 | 
			
		||||
                    delete_dependency: 'Eliminar',
 | 
			
		||||
                    dependency_actions: {
 | 
			
		||||
                        title: 'Actions',
 | 
			
		||||
                        delete_dependency: 'Delete',
 | 
			
		||||
                        title: 'Acciones',
 | 
			
		||||
                        delete_dependency: 'Eliminar',
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
                empty_state: {
 | 
			
		||||
                    title: 'No dependencies',
 | 
			
		||||
                    description: 'Create a view to get started',
 | 
			
		||||
                    title: 'Sin dependencias',
 | 
			
		||||
                    description: 'Crea una vista para comenzar',
 | 
			
		||||
                },
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
@@ -189,8 +192,7 @@ export const es: LanguageTranslation = {
 | 
			
		||||
            undo: 'Deshacer',
 | 
			
		||||
            redo: 'Rehacer',
 | 
			
		||||
            reorder_diagram: 'Reordenar Diagrama',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            highlight_overlapping_tables: 'Highlight Overlapping Tables',
 | 
			
		||||
            highlight_overlapping_tables: 'Resaltar tablas superpuestas',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_diagram_dialog: {
 | 
			
		||||
@@ -214,20 +216,20 @@ export const es: LanguageTranslation = {
 | 
			
		||||
                    step_1: 'Ve a Herramientas > Opciones > Resultados de Consulta > SQL Server.',
 | 
			
		||||
                    step_2: 'Si estás usando "Resultados en Cuadrícula", cambia el Máximo de Caracteres Recuperados para Datos No XML (configúralo en 9999999).',
 | 
			
		||||
                },
 | 
			
		||||
                // TODO: Translate
 | 
			
		||||
                instructions_link: 'Need help? Watch how',
 | 
			
		||||
                check_script_result: 'Check Script Result',
 | 
			
		||||
                instructions_link: '¿Necesitas ayuda? mira cómo',
 | 
			
		||||
                check_script_result: 'Revisa el resultado del script',
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            cancel: 'Cancelar',
 | 
			
		||||
            back: 'Atrás',
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            import_from_file: 'Import from File',
 | 
			
		||||
            empty_diagram: 'Diagrama vacío',
 | 
			
		||||
            continue: 'Continuar',
 | 
			
		||||
            import: 'Importar',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        open_diagram_dialog: {
 | 
			
		||||
            // TODO: Translate
 | 
			
		||||
            title: 'Abrir Diagrama',
 | 
			
		||||
            description:
 | 
			
		||||
                'Selecciona un diagrama para abrir de la lista a continuación.',
 | 
			
		||||
@@ -293,16 +295,15 @@ export const es: LanguageTranslation = {
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        export_image_dialog: {
 | 
			
		||||
            title: 'Export Image',
 | 
			
		||||
            description: 'Choose the scale factor for export:',
 | 
			
		||||
            scale_1x: '1x Regular',
 | 
			
		||||
            scale_2x: '2x (Recommended)',
 | 
			
		||||
            title: 'Exportar imagen',
 | 
			
		||||
            description: 'Escoge el factor de escalamiento para exportar:',
 | 
			
		||||
            scale_1x: '1x regular',
 | 
			
		||||
            scale_2x: '2x (recomendado)',
 | 
			
		||||
            scale_3x: '3x',
 | 
			
		||||
            scale_4x: '4x',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            export: 'Export',
 | 
			
		||||
            cancel: 'Cancelar',
 | 
			
		||||
            export: 'Exportar',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        new_table_schema_dialog: {
 | 
			
		||||
@@ -336,7 +337,31 @@ export const es: LanguageTranslation = {
 | 
			
		||||
            change_schema: 'Cambiar',
 | 
			
		||||
            none: 'nada',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        export_diagram_dialog: {
 | 
			
		||||
            title: 'Export Diagram',
 | 
			
		||||
            description: 'Choose the format for export:',
 | 
			
		||||
            format_json: 'JSON',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            export: 'Export',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error exporting diagram',
 | 
			
		||||
                description:
 | 
			
		||||
                    'Something went wrong. Need help? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        // TODO: Translate
 | 
			
		||||
        import_diagram_dialog: {
 | 
			
		||||
            title: 'Import Diagram',
 | 
			
		||||
            description: 'Paste the diagram JSON below:',
 | 
			
		||||
            cancel: 'Cancel',
 | 
			
		||||
            import: 'Import',
 | 
			
		||||
            error: {
 | 
			
		||||
                title: 'Error importing diagram',
 | 
			
		||||
                description:
 | 
			
		||||
                    'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        relationship_type: {
 | 
			
		||||
            one_to_one: 'Uno a Uno',
 | 
			
		||||
            one_to_many: 'Uno a Muchos',
 | 
			
		||||
@@ -351,12 +376,25 @@ export const es: LanguageTranslation = {
 | 
			
		||||
 | 
			
		||||
        table_node_context_menu: {
 | 
			
		||||
            edit_table: 'Editar Tabla',
 | 
			
		||||
            duplicate_table: 'Duplicate Table', // TODO: Translate
 | 
			
		||||
            delete_table: 'Eliminar Tabla',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // TODO: Add translations
 | 
			
		||||
        snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
 | 
			
		||||
 | 
			
		||||
        tool_tips: {
 | 
			
		||||
            double_click_to_edit: 'Doble clic para editar',
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        language_select: {
 | 
			
		||||
            change_language: 'Idioma',
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const esMetadata: LanguageMetadata = {
 | 
			
		||||
    name: 'Español',
 | 
			
		||||
    name: 'Spanish',
 | 
			
		||||
    nativeName: 'Español',
 | 
			
		||||
    code: 'es',
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||