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', | ||||
| }; | ||||
|   | ||||