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 | # 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) | ## 1.0.0 (2024-11-04) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ To get started: | |||||||
|  |  | ||||||
| ### License | ### 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? | ## Questions? | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| FROM node:22-alpine AS builder | FROM node:22-alpine AS builder | ||||||
|  |  | ||||||
|  | ARG VITE_OPENAI_API_KEY | ||||||
|  |  | ||||||
| WORKDIR /usr/src/app | WORKDIR /usr/src/app | ||||||
|  |  | ||||||
| COPY package.json package-lock.json ./ | COPY package.json package-lock.json ./ | ||||||
| @@ -14,8 +16,11 @@ RUN npm run build | |||||||
| FROM nginx:stable-alpine AS production | FROM nginx:stable-alpine AS production | ||||||
|  |  | ||||||
| COPY --from=builder /usr/src/app/dist /usr/share/nginx/html | COPY --from=builder /usr/src/app/dist /usr/share/nginx/html | ||||||
|  | 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 the default port for the Nginx web server | ||||||
| EXPOSE 80 | EXPOSE 80 | ||||||
|  |  | ||||||
| CMD ["nginx", "-g", "daemon off;"] | ENTRYPOINT ["/entrypoint.sh"] | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -15,8 +15,9 @@ | |||||||
|  |  | ||||||
| <h3 align="center"> | <h3 align="center"> | ||||||
|   <a href="https://discord.gg/QeFwyWSKwC">Community</a>  • |   <a href="https://discord.gg/QeFwyWSKwC">Community</a>  • | ||||||
|   <a href="https://www.chartdb.io">Website</a>  • |   <a href="https://www.chartdb.io?ref=github_readme">Website</a>  • | ||||||
|   <a href="https://app.chartdb.io/examples">Demo</a> |   <a href="https://chartdb.io/templates?ref=github_readme">Examples</a>  • | ||||||
|  |   <a href="https://app.chartdb.io?ref=github_readme">Demo</a> | ||||||
| </h3> | </h3> | ||||||
|  |  | ||||||
| <h4 align="center"> | <h4 align="center"> | ||||||
| @@ -38,7 +39,7 @@ | |||||||
| --- | --- | ||||||
|  |  | ||||||
| <p align="center"> | <p align="center"> | ||||||
|   <img width='700px' src="./public/ChartDB.png"> |   <img width='700px' src="./public/chartdb.png"> | ||||||
| </p> | </p> | ||||||
|  |  | ||||||
| ### 🎉 ChartDB | ### 🎉 ChartDB | ||||||
| @@ -71,7 +72,7 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif | |||||||
|  |  | ||||||
| ## Getting Started | ## 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 | ### How To Use | ||||||
|  |  | ||||||
| @@ -95,9 +96,13 @@ VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> npm run build | |||||||
| ``` | ``` | ||||||
|  |  | ||||||
| ### Running the Docker Container | ### Running the Docker Container | ||||||
|  |  | ||||||
| ```bash | ```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 | docker run -p 8080:80 chartdb | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| @@ -105,7 +110,7 @@ Open your browser and navigate to `http://localhost:8080`. | |||||||
|  |  | ||||||
| ## Try it on our website | ## 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" | 2. Click "Go to app" | ||||||
| 3. Choose the database that you are using. | 3. Choose the database that you are using. | ||||||
| 4. Take the magic query and run it in your database. | 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" |             href="https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100..900;1,100..900&display=swap" | ||||||
|             rel="stylesheet" |             rel="stylesheet" | ||||||
|         /> |         /> | ||||||
|  |         <script src="/config.js"></script> | ||||||
|         <script |         <script | ||||||
|             src="https://cdn.usefathom.com/script.js" |             src="https://cdn.usefathom.com/script.js" | ||||||
|             data-site="PRHIVBNN" |             data-site="PRHIVBNN" | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						| @@ -1,12 +1,12 @@ | |||||||
| { | { | ||||||
|   "name": "chartdb", |   "name": "chartdb", | ||||||
|   "version": "1.0.0", |   "version": "1.3.1", | ||||||
|   "lockfileVersion": 3, |   "lockfileVersion": 3, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "packages": { |   "packages": { | ||||||
|     "": { |     "": { | ||||||
|       "name": "chartdb", |       "name": "chartdb", | ||||||
|       "version": "1.0.0", |       "version": "1.3.1", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@ai-sdk/openai": "^0.0.51", |         "@ai-sdk/openai": "^0.0.51", | ||||||
|         "@dnd-kit/sortable": "^8.0.0", |         "@dnd-kit/sortable": "^8.0.0", | ||||||
| @@ -44,6 +44,7 @@ | |||||||
|         "fast-deep-equal": "^3.1.3", |         "fast-deep-equal": "^3.1.3", | ||||||
|         "html-to-image": "^1.11.11", |         "html-to-image": "^1.11.11", | ||||||
|         "i18next": "^23.14.0", |         "i18next": "^23.14.0", | ||||||
|  |         "i18next-browser-languagedetector": "^8.0.0", | ||||||
|         "lucide-react": "^0.441.0", |         "lucide-react": "^0.441.0", | ||||||
|         "monaco-editor": "^0.52.0", |         "monaco-editor": "^0.52.0", | ||||||
|         "nanoid": "^5.0.7", |         "nanoid": "^5.0.7", | ||||||
| @@ -60,7 +61,8 @@ | |||||||
|         "tailwind-merge": "^2.4.0", |         "tailwind-merge": "^2.4.0", | ||||||
|         "tailwindcss-animate": "^1.0.7", |         "tailwindcss-animate": "^1.0.7", | ||||||
|         "timeago-react": "^3.0.6", |         "timeago-react": "^3.0.6", | ||||||
|         "vaul": "^0.9.1" |         "vaul": "^0.9.1", | ||||||
|  |         "zod": "^3.23.8" | ||||||
|       }, |       }, | ||||||
|       "devDependencies": { |       "devDependencies": { | ||||||
|         "@types/node": "^22.1.0", |         "@types/node": "^22.1.0", | ||||||
| @@ -6754,6 +6756,15 @@ | |||||||
|         "@babel/runtime": "^7.23.2" |         "@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": { |     "node_modules/ignore": { | ||||||
|       "version": "5.3.2", |       "version": "5.3.2", | ||||||
|       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", |       "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", |       "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", | ||||||
|       "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", |       "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", | ||||||
|       "license": "MIT", |       "license": "MIT", | ||||||
|       "peer": true, |  | ||||||
|       "funding": { |       "funding": { | ||||||
|         "url": "https://github.com/sponsors/colinhacks" |         "url": "https://github.com/sponsors/colinhacks" | ||||||
|       } |       } | ||||||
|   | |||||||
							
								
								
									
										188
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,95 +1,97 @@ | |||||||
| { | { | ||||||
|   "name": "chartdb", |     "name": "chartdb", | ||||||
|   "private": true, |     "private": true, | ||||||
|   "version": "1.0.0", |     "version": "1.3.1", | ||||||
|   "type": "module", |     "type": "module", | ||||||
|   "scripts": { |     "scripts": { | ||||||
|     "dev": "vite", |         "dev": "vite", | ||||||
|     "build": "npm run lint && tsc -b && vite build", |         "build": "npm run lint && tsc -b && vite build", | ||||||
|     "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", |         "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", | ||||||
|     "lint:fix": "npm run lint -- --fix", |         "lint:fix": "npm run lint -- --fix", | ||||||
|     "preview": "vite preview", |         "preview": "vite preview", | ||||||
|     "prepare": "husky" |         "prepare": "husky" | ||||||
|   }, |     }, | ||||||
|   "dependencies": { |     "dependencies": { | ||||||
|     "@ai-sdk/openai": "^0.0.51", |         "@ai-sdk/openai": "^0.0.51", | ||||||
|     "@dnd-kit/sortable": "^8.0.0", |         "@dnd-kit/sortable": "^8.0.0", | ||||||
|     "@monaco-editor/react": "^4.6.0", |         "@monaco-editor/react": "^4.6.0", | ||||||
|     "@radix-ui/react-accordion": "^1.2.0", |         "@radix-ui/react-accordion": "^1.2.0", | ||||||
|     "@radix-ui/react-alert-dialog": "^1.1.1", |         "@radix-ui/react-alert-dialog": "^1.1.1", | ||||||
|     "@radix-ui/react-avatar": "^1.1.0", |         "@radix-ui/react-avatar": "^1.1.0", | ||||||
|     "@radix-ui/react-checkbox": "^1.1.1", |         "@radix-ui/react-checkbox": "^1.1.1", | ||||||
|     "@radix-ui/react-collapsible": "^1.1.0", |         "@radix-ui/react-collapsible": "^1.1.0", | ||||||
|     "@radix-ui/react-context-menu": "^2.2.1", |         "@radix-ui/react-context-menu": "^2.2.1", | ||||||
|     "@radix-ui/react-dialog": "^1.1.1", |         "@radix-ui/react-dialog": "^1.1.1", | ||||||
|     "@radix-ui/react-dropdown-menu": "^2.1.1", |         "@radix-ui/react-dropdown-menu": "^2.1.1", | ||||||
|     "@radix-ui/react-hover-card": "^1.1.1", |         "@radix-ui/react-hover-card": "^1.1.1", | ||||||
|     "@radix-ui/react-icons": "^1.3.0", |         "@radix-ui/react-icons": "^1.3.0", | ||||||
|     "@radix-ui/react-label": "^2.1.0", |         "@radix-ui/react-label": "^2.1.0", | ||||||
|     "@radix-ui/react-menubar": "^1.1.1", |         "@radix-ui/react-menubar": "^1.1.1", | ||||||
|     "@radix-ui/react-popover": "^1.1.1", |         "@radix-ui/react-popover": "^1.1.1", | ||||||
|     "@radix-ui/react-scroll-area": "^1.1.0", |         "@radix-ui/react-scroll-area": "^1.1.0", | ||||||
|     "@radix-ui/react-select": "^2.1.1", |         "@radix-ui/react-select": "^2.1.1", | ||||||
|     "@radix-ui/react-separator": "^1.1.0", |         "@radix-ui/react-separator": "^1.1.0", | ||||||
|     "@radix-ui/react-slot": "^1.1.0", |         "@radix-ui/react-slot": "^1.1.0", | ||||||
|     "@radix-ui/react-tabs": "^1.1.0", |         "@radix-ui/react-tabs": "^1.1.0", | ||||||
|     "@radix-ui/react-toast": "^1.2.1", |         "@radix-ui/react-toast": "^1.2.1", | ||||||
|     "@radix-ui/react-toggle": "^1.1.0", |         "@radix-ui/react-toggle": "^1.1.0", | ||||||
|     "@radix-ui/react-toggle-group": "^1.1.0", |         "@radix-ui/react-toggle-group": "^1.1.0", | ||||||
|     "@radix-ui/react-tooltip": "^1.1.2", |         "@radix-ui/react-tooltip": "^1.1.2", | ||||||
|     "@uidotdev/usehooks": "^2.4.1", |         "@uidotdev/usehooks": "^2.4.1", | ||||||
|     "@xyflow/react": "^12.3.1", |         "@xyflow/react": "^12.3.1", | ||||||
|     "ahooks": "^3.8.1", |         "ahooks": "^3.8.1", | ||||||
|     "ai": "^3.3.14", |         "ai": "^3.3.14", | ||||||
|     "class-variance-authority": "^0.7.0", |         "class-variance-authority": "^0.7.0", | ||||||
|     "clsx": "^2.1.1", |         "clsx": "^2.1.1", | ||||||
|     "cmdk": "^1.0.0", |         "cmdk": "^1.0.0", | ||||||
|     "dexie": "^4.0.8", |         "dexie": "^4.0.8", | ||||||
|     "fast-deep-equal": "^3.1.3", |         "fast-deep-equal": "^3.1.3", | ||||||
|     "html-to-image": "^1.11.11", |         "html-to-image": "^1.11.11", | ||||||
|     "i18next": "^23.14.0", |         "i18next": "^23.14.0", | ||||||
|     "lucide-react": "^0.441.0", |         "i18next-browser-languagedetector": "^8.0.0", | ||||||
|     "monaco-editor": "^0.52.0", |         "lucide-react": "^0.441.0", | ||||||
|     "nanoid": "^5.0.7", |         "monaco-editor": "^0.52.0", | ||||||
|     "node-sql-parser": "^5.3.2", |         "nanoid": "^5.0.7", | ||||||
|     "react": "^18.3.1", |         "node-sql-parser": "^5.3.2", | ||||||
|     "react-dom": "^18.3.1", |         "react": "^18.3.1", | ||||||
|     "react-helmet-async": "^2.0.5", |         "react-dom": "^18.3.1", | ||||||
|     "react-hotkeys-hook": "^4.5.0", |         "react-helmet-async": "^2.0.5", | ||||||
|     "react-i18next": "^15.0.1", |         "react-hotkeys-hook": "^4.5.0", | ||||||
|     "react-resizable-panels": "^2.0.22", |         "react-i18next": "^15.0.1", | ||||||
|     "react-responsive": "^10.0.0", |         "react-resizable-panels": "^2.0.22", | ||||||
|     "react-router-dom": "^6.26.0", |         "react-responsive": "^10.0.0", | ||||||
|     "react-use": "^17.5.1", |         "react-router-dom": "^6.26.0", | ||||||
|     "tailwind-merge": "^2.4.0", |         "react-use": "^17.5.1", | ||||||
|     "tailwindcss-animate": "^1.0.7", |         "tailwind-merge": "^2.4.0", | ||||||
|     "timeago-react": "^3.0.6", |         "tailwindcss-animate": "^1.0.7", | ||||||
|     "vaul": "^0.9.1" |         "timeago-react": "^3.0.6", | ||||||
|   }, |         "vaul": "^0.9.1", | ||||||
|   "devDependencies": { |         "zod": "^3.23.8" | ||||||
|     "@types/node": "^22.1.0", |     }, | ||||||
|     "@types/react": "^18.3.3", |     "devDependencies": { | ||||||
|     "@types/react-dom": "^18.3.0", |         "@types/node": "^22.1.0", | ||||||
|     "@typescript-eslint/eslint-plugin": "^7.15.0", |         "@types/react": "^18.3.3", | ||||||
|     "@typescript-eslint/parser": "^7.15.0", |         "@types/react-dom": "^18.3.0", | ||||||
|     "@vitejs/plugin-react": "^4.3.1", |         "@typescript-eslint/eslint-plugin": "^7.15.0", | ||||||
|     "autoprefixer": "^10.4.20", |         "@typescript-eslint/parser": "^7.15.0", | ||||||
|     "eslint": "^8.57.0", |         "@vitejs/plugin-react": "^4.3.1", | ||||||
|     "eslint-config-prettier": "^9.1.0", |         "autoprefixer": "^10.4.20", | ||||||
|     "eslint-plugin-css-modules": "^2.12.0", |         "eslint": "^8.57.0", | ||||||
|     "eslint-plugin-jsx-a11y": "^6.9.0", |         "eslint-config-prettier": "^9.1.0", | ||||||
|     "eslint-plugin-prettier": "^5.2.1", |         "eslint-plugin-css-modules": "^2.12.0", | ||||||
|     "eslint-plugin-react": "^7.35.0", |         "eslint-plugin-jsx-a11y": "^6.9.0", | ||||||
|     "eslint-plugin-react-hooks": "^4.6.2", |         "eslint-plugin-prettier": "^5.2.1", | ||||||
|     "eslint-plugin-react-refresh": "^0.4.7", |         "eslint-plugin-react": "^7.35.0", | ||||||
|     "eslint-plugin-tailwindcss": "^3.17.4", |         "eslint-plugin-react-hooks": "^4.6.2", | ||||||
|     "husky": "^9.1.5", |         "eslint-plugin-react-refresh": "^0.4.7", | ||||||
|     "postcss": "^8.4.40", |         "eslint-plugin-tailwindcss": "^3.17.4", | ||||||
|     "prettier": "^3.3.3", |         "husky": "^9.1.5", | ||||||
|     "rollup-plugin-visualizer": "^5.12.0", |         "postcss": "^8.4.40", | ||||||
|     "tailwindcss": "^3.4.7", |         "prettier": "^3.3.3", | ||||||
|     "typescript": "^5.2.2", |         "rollup-plugin-visualizer": "^5.12.0", | ||||||
|     "unplugin-inject-preload": "^3.0.0", |         "tailwindcss": "^3.4.7", | ||||||
|     "vite": "^5.3.4" |         "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 { useTranslation } from 'react-i18next'; | ||||||
| import { DarkTheme } from './themes/dark'; | import { DarkTheme } from './themes/dark'; | ||||||
| import { LightTheme } from './themes/light'; | import { LightTheme } from './themes/light'; | ||||||
|  | import './config.ts'; | ||||||
|  |  | ||||||
| export interface CodeSnippetProps { | export interface CodeSnippetProps { | ||||||
|     className?: string; |     className?: string; | ||||||
|     code: string; |     code: string; | ||||||
|     language?: 'sql' | 'shell'; |     language?: 'sql' | 'shell'; | ||||||
|     loading?: boolean; |     loading?: boolean; | ||||||
|  |     autoScroll?: boolean; | ||||||
|  |     isComplete?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export const Editor = lazy(() => | export const Editor = lazy(() => | ||||||
| @@ -24,7 +27,14 @@ export const Editor = lazy(() => | |||||||
| ); | ); | ||||||
|  |  | ||||||
| export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | 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 { t } = useTranslation(); | ||||||
|         const monaco = useMonaco(); |         const monaco = useMonaco(); | ||||||
|         const { effectiveTheme } = useTheme(); |         const { effectiveTheme } = useTheme(); | ||||||
| @@ -46,6 +56,16 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | |||||||
|             }, 1500); |             }, 1500); | ||||||
|         }, [isCopied]); |         }, [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(() => { |         const copyToClipboard = useCallback(() => { | ||||||
|             navigator.clipboard.writeText(code); |             navigator.clipboard.writeText(code); | ||||||
|             setIsCopied(true); |             setIsCopied(true); | ||||||
| @@ -62,32 +82,38 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | |||||||
|                     <Spinner /> |                     <Spinner /> | ||||||
|                 ) : ( |                 ) : ( | ||||||
|                     <Suspense fallback={<Spinner />}> |                     <Suspense fallback={<Spinner />}> | ||||||
|                         <Tooltip |                         {isComplete ? ( | ||||||
|                             onOpenChange={setTooltipOpen} |                             <Tooltip | ||||||
|                             open={isCopied || tooltipOpen} |                                 onOpenChange={setTooltipOpen} | ||||||
|                         > |                                 open={isCopied || tooltipOpen} | ||||||
|                             <TooltipTrigger |  | ||||||
|                                 asChild |  | ||||||
|                                 className="absolute right-1 top-1 z-10" |  | ||||||
|                             > |                             > | ||||||
|                                 <span> |                                 <TooltipTrigger | ||||||
|                                     <Button |                                     asChild | ||||||
|                                         className=" h-fit p-1.5" |                                     className="absolute right-1 top-1 z-10" | ||||||
|                                         variant="outline" |                                 > | ||||||
|                                         onClick={copyToClipboard} |                                     <span> | ||||||
|                                     > |                                         <Button | ||||||
|                                         {isCopied ? ( |                                             className=" h-fit p-1.5" | ||||||
|                                             <CopyCheck size={16} /> |                                             variant="outline" | ||||||
|                                         ) : ( |                                             onClick={copyToClipboard} | ||||||
|                                             <Copy size={16} /> |                                         > | ||||||
|                                         )} |                                             {isCopied ? ( | ||||||
|                                     </Button> |                                                 <CopyCheck size={16} /> | ||||||
|                                 </span> |                                             ) : ( | ||||||
|                             </TooltipTrigger> |                                                 <Copy size={16} /> | ||||||
|                             <TooltipContent> |                                             )} | ||||||
|                                 {t(isCopied ? 'copied' : 'copy_to_clipboard')} |                                         </Button> | ||||||
|                             </TooltipContent> |                                     </span> | ||||||
|                         </Tooltip> |                                 </TooltipTrigger> | ||||||
|  |                                 <TooltipContent> | ||||||
|  |                                     {t( | ||||||
|  |                                         isCopied | ||||||
|  |                                             ? 'copied' | ||||||
|  |                                             : 'copy_to_clipboard' | ||||||
|  |                                     )} | ||||||
|  |                                 </TooltipContent> | ||||||
|  |                             </Tooltip> | ||||||
|  |                         ) : null} | ||||||
|  |  | ||||||
|                         <Editor |                         <Editor | ||||||
|                             value={code} |                             value={code} | ||||||
| @@ -117,6 +143,9 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | |||||||
|                                 contextmenu: false, |                                 contextmenu: false, | ||||||
|                             }} |                             }} | ||||||
|                         /> |                         /> | ||||||
|  |                         {!isComplete ? ( | ||||||
|  |                             <div className="absolute bottom-2 right-2 size-2 animate-blink rounded-full bg-pink-600" /> | ||||||
|  |                         ) : null} | ||||||
|                     </Suspense> |                     </Suspense> | ||||||
|                 )} |                 )} | ||||||
|             </div> |             </div> | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import * as DialogPrimitive from '@radix-ui/react-dialog'; | |||||||
| import { Cross2Icon } from '@radix-ui/react-icons'; | import { Cross2Icon } from '@radix-ui/react-icons'; | ||||||
|  |  | ||||||
| import { cn } from '@/lib/utils'; | import { cn } from '@/lib/utils'; | ||||||
|  | import { ScrollArea } from '../scroll-area/scroll-area'; | ||||||
|  |  | ||||||
| const Dialog = DialogPrimitive.Root; | const Dialog = DialogPrimitive.Root; | ||||||
|  |  | ||||||
| @@ -110,6 +111,18 @@ const DialogDescription = React.forwardRef< | |||||||
| )); | )); | ||||||
| DialogDescription.displayName = DialogPrimitive.Description.displayName; | 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 { | export { | ||||||
|     Dialog, |     Dialog, | ||||||
|     DialogPortal, |     DialogPortal, | ||||||
| @@ -121,4 +134,5 @@ export { | |||||||
|     DialogFooter, |     DialogFooter, | ||||||
|     DialogTitle, |     DialogTitle, | ||||||
|     DialogDescription, |     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} |                                 strokeWidth={item.selected ? 2.4 : 2} | ||||||
|                             /> |                             /> | ||||||
|                         ) : null} |                         ) : null} | ||||||
|                         {item.title} |                         <span className="min-w-0 truncate">{item.title}</span> | ||||||
|                     </Link> |                     </Link> | ||||||
|                 ))} |                 ))} | ||||||
|             </div> |             </div> | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ const ScrollArea = React.forwardRef< | |||||||
|         className={cn('relative overflow-hidden', className)} |         className={cn('relative overflow-hidden', className)} | ||||||
|         {...props} |         {...props} | ||||||
|     > |     > | ||||||
|         <ScrollAreaPrimitive.Viewport className="scrollable-flex size-full rounded-[inherit]"> |         <ScrollAreaPrimitive.Viewport className="size-full rounded-[inherit]"> | ||||||
|             {children} |             {children} | ||||||
|         </ScrollAreaPrimitive.Viewport> |         </ScrollAreaPrimitive.Viewport> | ||||||
|         <ScrollBar /> |         <ScrollBar /> | ||||||
| @@ -40,7 +40,7 @@ const ScrollBar = React.forwardRef< | |||||||
|         )} |         )} | ||||||
|         {...props} |         {...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> |     </ScrollAreaPrimitive.ScrollAreaScrollbar> | ||||||
| )); | )); | ||||||
| ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ export interface SelectBoxOption { | |||||||
|     description?: string; |     description?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| interface SelectBoxProps { | export interface SelectBoxProps { | ||||||
|     options: SelectBoxOption[]; |     options: SelectBoxOption[]; | ||||||
|     value?: string[] | string; |     value?: string[] | string; | ||||||
|     onChange?: (values: string[] | string) => void; |     onChange?: (values: string[] | string) => void; | ||||||
| @@ -156,9 +156,19 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | |||||||
|             [options, value, multiple] |             [options, value, multiple] | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|  |         const handleKeyDown = React.useCallback( | ||||||
|  |             (e: React.KeyboardEvent) => { | ||||||
|  |                 if (!isOpen && e.code.toLowerCase() === 'space') { | ||||||
|  |                     e.preventDefault(); | ||||||
|  |                     onOpenChange(true); | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             [isOpen, onOpenChange] | ||||||
|  |         ); | ||||||
|  |  | ||||||
|         return ( |         return ( | ||||||
|             <Popover open={isOpen} onOpenChange={onOpenChange} modal={true}> |             <Popover open={isOpen} onOpenChange={onOpenChange} modal={true}> | ||||||
|                 <PopoverTrigger asChild> |                 <PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}> | ||||||
|                     <div |                     <div | ||||||
|                         className={cn( |                         className={cn( | ||||||
|                             `flex min-h-[36px] cursor-pointer items-center justify-between rounded-md border px-3 py-1 data-[state=open]:border-ring ${disabled ? 'bg-muted pointer-events-none' : ''}`, |                             `flex min-h-[36px] cursor-pointer items-center justify-between rounded-md border px-3 py-1 data-[state=open]:border-ring ${disabled ? 'bg-muted pointer-events-none' : ''}`, | ||||||
|   | |||||||
| @@ -11,8 +11,6 @@ import type { DBRelationship } from '@/lib/domain/db-relationship'; | |||||||
| import { useStorage } from '@/hooks/use-storage'; | import { useStorage } from '@/hooks/use-storage'; | ||||||
| import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack'; | import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack'; | ||||||
| import type { Diagram } from '@/lib/domain/diagram'; | 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 { DatabaseEdition } from '@/lib/domain/database-edition'; | ||||||
| import type { DBSchema } from '@/lib/domain/db-schema'; | import type { DBSchema } from '@/lib/domain/db-schema'; | ||||||
| import { | import { | ||||||
| @@ -34,13 +32,11 @@ export const ChartDBProvider: React.FC< | |||||||
| > = ({ children, diagram, readonly }) => { | > = ({ children, diagram, readonly }) => { | ||||||
|     let db = useStorage(); |     let db = useStorage(); | ||||||
|     const events = useEventEmitter<ChartDBEvent>(); |     const events = useEventEmitter<ChartDBEvent>(); | ||||||
|     const navigate = useNavigate(); |  | ||||||
|     const { setSchemasFilter, schemasFilter } = useLocalConfig(); |     const { setSchemasFilter, schemasFilter } = useLocalConfig(); | ||||||
|     const { addUndoAction, resetRedoStack, resetUndoStack } = |     const { addUndoAction, resetRedoStack, resetUndoStack } = | ||||||
|         useRedoUndoStack(); |         useRedoUndoStack(); | ||||||
|     const [diagramId, setDiagramId] = useState(''); |     const [diagramId, setDiagramId] = useState(''); | ||||||
|     const [diagramName, setDiagramName] = useState(''); |     const [diagramName, setDiagramName] = useState(''); | ||||||
|     const { updateConfig } = useConfig(); |  | ||||||
|     const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date()); |     const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date()); | ||||||
|     const [diagramUpdatedAt, setDiagramUpdatedAt] = useState<Date>(new Date()); |     const [diagramUpdatedAt, setDiagramUpdatedAt] = useState<Date>(new Date()); | ||||||
|     const [databaseType, setDatabaseType] = useState<DatabaseType>( |     const [databaseType, setDatabaseType] = useState<DatabaseType>( | ||||||
| @@ -173,34 +169,13 @@ export const ChartDBProvider: React.FC< | |||||||
|             resetRedoStack(); |             resetRedoStack(); | ||||||
|             resetUndoStack(); |             resetUndoStack(); | ||||||
|  |  | ||||||
|             const [config] = await Promise.all([ |             await Promise.all([ | ||||||
|                 db.getConfig(), |  | ||||||
|                 db.deleteDiagramTables(diagramId), |                 db.deleteDiagramTables(diagramId), | ||||||
|                 db.deleteDiagramRelationships(diagramId), |                 db.deleteDiagramRelationships(diagramId), | ||||||
|                 db.deleteDiagram(diagramId), |                 db.deleteDiagram(diagramId), | ||||||
|                 db.deleteDiagramDependencies(diagramId), |                 db.deleteDiagramDependencies(diagramId), | ||||||
|             ]); |             ]); | ||||||
|  |         }, [db, diagramId, resetRedoStack, resetUndoStack]); | ||||||
|             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, |  | ||||||
|         ]); |  | ||||||
|  |  | ||||||
|     const updateDiagramUpdatedAt: ChartDBContext['updateDiagramUpdatedAt'] = |     const updateDiagramUpdatedAt: ChartDBContext['updateDiagramUpdatedAt'] = | ||||||
|         useCallback(async () => { |         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 { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog'; | ||||||
| import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-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 { 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 { | export interface DialogContext { | ||||||
|     // Create diagram dialog |     // Create diagram dialog | ||||||
| @@ -48,6 +50,18 @@ export interface DialogContext { | |||||||
|         params: Omit<ExportImageDialogProps, 'dialog'> |         params: Omit<ExportImageDialogProps, 'dialog'> | ||||||
|     ) => void; |     ) => void; | ||||||
|     closeExportImageDialog: () => 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>({ | export const dialogContext = createContext<DialogContext>({ | ||||||
| @@ -69,4 +83,8 @@ export const dialogContext = createContext<DialogContext>({ | |||||||
|     closeStarUsDialog: emptyFn, |     closeStarUsDialog: emptyFn, | ||||||
|     openExportImageDialog: emptyFn, |     openExportImageDialog: emptyFn, | ||||||
|     closeExportImageDialog: 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 { StarUsDialog } from '@/dialogs/star-us-dialog/star-us-dialog'; | ||||||
| import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog'; | import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog'; | ||||||
| import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog'; | import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog'; | ||||||
|  | import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog'; | ||||||
|  | import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog'; | ||||||
|  |  | ||||||
| export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | ||||||
|     children, |     children, | ||||||
| @@ -86,6 +88,14 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             [setOpenTableSchemaDialog] |             [setOpenTableSchemaDialog] | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|  |     // Export image dialog | ||||||
|  |     const [openExportDiagramDialog, setOpenExportDiagramDialog] = | ||||||
|  |         useState(false); | ||||||
|  |  | ||||||
|  |     // Import diagram dialog | ||||||
|  |     const [openImportDiagramDialog, setOpenImportDiagramDialog] = | ||||||
|  |         useState(false); | ||||||
|  |  | ||||||
|     // Alert dialog |     // Alert dialog | ||||||
|     const [showAlert, setShowAlert] = useState(false); |     const [showAlert, setShowAlert] = useState(false); | ||||||
|     const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({ |     const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({ | ||||||
| @@ -126,6 +136,12 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|                 closeStarUsDialog: () => setOpenStarUsDialog(false), |                 closeStarUsDialog: () => setOpenStarUsDialog(false), | ||||||
|                 closeExportImageDialog: () => setOpenExportImageDialog(false), |                 closeExportImageDialog: () => setOpenExportImageDialog(false), | ||||||
|                 openExportImageDialog: openExportImageDialogHandler, |                 openExportImageDialog: openExportImageDialogHandler, | ||||||
|  |                 openExportDiagramDialog: () => setOpenExportDiagramDialog(true), | ||||||
|  |                 closeExportDiagramDialog: () => | ||||||
|  |                     setOpenExportDiagramDialog(false), | ||||||
|  |                 openImportDiagramDialog: () => setOpenImportDiagramDialog(true), | ||||||
|  |                 closeImportDiagramDialog: () => | ||||||
|  |                     setOpenImportDiagramDialog(false), | ||||||
|             }} |             }} | ||||||
|         > |         > | ||||||
|             {children} |             {children} | ||||||
| @@ -152,6 +168,8 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|                 dialog={{ open: openExportImageDialog }} |                 dialog={{ open: openExportImageDialog }} | ||||||
|                 {...exportImageDialogParams} |                 {...exportImageDialogParams} | ||||||
|             /> |             /> | ||||||
|  |             <ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} /> | ||||||
|  |             <ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} /> | ||||||
|         </dialogContext.Provider> |         </dialogContext.Provider> | ||||||
|     ); |     ); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -5,12 +5,14 @@ import { toJpeg, toPng, toSvg } from 'html-to-image'; | |||||||
| import { useReactFlow } from '@xyflow/react'; | import { useReactFlow } from '@xyflow/react'; | ||||||
| import { useChartDB } from '@/hooks/use-chartdb'; | import { useChartDB } from '@/hooks/use-chartdb'; | ||||||
| import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner'; | import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner'; | ||||||
|  | import { useTheme } from '@/hooks/use-theme'; | ||||||
|  |  | ||||||
| export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | ||||||
|     children, |     children, | ||||||
| }) => { | }) => { | ||||||
|     const { hideLoader, showLoader } = useFullScreenLoader(); |     const { hideLoader, showLoader } = useFullScreenLoader(); | ||||||
|     const { setNodes, getViewport } = useReactFlow(); |     const { setNodes, getViewport } = useReactFlow(); | ||||||
|  |     const { effectiveTheme } = useTheme(); | ||||||
|     const { diagramName } = useChartDB(); |     const { diagramName } = useChartDB(); | ||||||
|  |  | ||||||
|     const downloadImage = useCallback( |     const downloadImage = useCallback( | ||||||
| @@ -59,13 +61,101 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             const imageCreateFn = imageCreatorMap[type]; |             const imageCreateFn = imageCreatorMap[type]; | ||||||
|  |  | ||||||
|             setTimeout(async () => { |             setTimeout(async () => { | ||||||
|                 const dataUrl = await imageCreateFn( |                 const viewportElement = window.document.querySelector( | ||||||
|                     window.document.querySelector( |                     '.react-flow__viewport' | ||||||
|                         '.react-flow__viewport' |                 ) as HTMLElement; | ||||||
|                     ) 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' |                         ...(type === 'jpeg' || type === 'png' | ||||||
|                             ? { backgroundColor: '#ffffff' } |                             ? { | ||||||
|  |                                   backgroundColor: | ||||||
|  |                                       effectiveTheme === 'light' | ||||||
|  |                                           ? '#ffffff' | ||||||
|  |                                           : '#141414', | ||||||
|  |                               } | ||||||
|                             : {}), |                             : {}), | ||||||
|                         width: reactFlowBounds.width, |                         width: reactFlowBounds.width, | ||||||
|                         height: reactFlowBounds.height, |                         height: reactFlowBounds.height, | ||||||
| @@ -76,11 +166,13 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|                         }, |                         }, | ||||||
|                         quality: 1, |                         quality: 1, | ||||||
|                         pixelRatio: scale, |                         pixelRatio: scale, | ||||||
|                     } |                     }); | ||||||
|                 ); |  | ||||||
|  |  | ||||||
|                 downloadImage(dataUrl, type); |                     downloadImage(dataUrl, type); | ||||||
|                 hideLoader(); |                 } finally { | ||||||
|  |                     viewportElement.removeChild(tempSvg); | ||||||
|  |                     hideLoader(); | ||||||
|  |                 } | ||||||
|             }, 0); |             }, 0); | ||||||
|         }, |         }, | ||||||
|         [ |         [ | ||||||
| @@ -90,6 +182,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|             imageCreatorMap, |             imageCreatorMap, | ||||||
|             setNodes, |             setNodes, | ||||||
|             showLoader, |             showLoader, | ||||||
|  |             effectiveTheme, | ||||||
|         ] |         ] | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ import { | |||||||
| import { useHistory } from '@/hooks/use-history'; | import { useHistory } from '@/hooks/use-history'; | ||||||
| import { useDialog } from '@/hooks/use-dialog'; | import { useDialog } from '@/hooks/use-dialog'; | ||||||
| import { useChartDB } from '@/hooks/use-chartdb'; | import { useChartDB } from '@/hooks/use-chartdb'; | ||||||
|  | import { useLayout } from '@/hooks/use-layout'; | ||||||
|  |  | ||||||
| export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({ | export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({ | ||||||
|     children, |     children, | ||||||
| @@ -15,6 +16,8 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|     const { redo, undo } = useHistory(); |     const { redo, undo } = useHistory(); | ||||||
|     const { openOpenDiagramDialog } = useDialog(); |     const { openOpenDiagramDialog } = useDialog(); | ||||||
|     const { updateDiagramUpdatedAt } = useChartDB(); |     const { updateDiagramUpdatedAt } = useChartDB(); | ||||||
|  |     const { toggleSidePanel } = useLayout(); | ||||||
|  |  | ||||||
|     useHotkeys( |     useHotkeys( | ||||||
|         keyboardShortcutsForOS[KeyboardShortcutAction.REDO].keyCombination, |         keyboardShortcutsForOS[KeyboardShortcutAction.REDO].keyCombination, | ||||||
|         redo, |         redo, | ||||||
| @@ -49,6 +52,15 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|         }, |         }, | ||||||
|         [updateDiagramUpdatedAt] |         [updateDiagramUpdatedAt] | ||||||
|     ); |     ); | ||||||
|  |     useHotkeys( | ||||||
|  |         keyboardShortcutsForOS[KeyboardShortcutAction.TOGGLE_SIDE_PANEL] | ||||||
|  |             .keyCombination, | ||||||
|  |         toggleSidePanel, | ||||||
|  |         { | ||||||
|  |             preventDefault: true, | ||||||
|  |         }, | ||||||
|  |         [toggleSidePanel] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <keyboardShortcutsContext.Provider value={{}}> |         <keyboardShortcutsContext.Provider value={{}}> | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ export enum KeyboardShortcutAction { | |||||||
|     UNDO = 'undo', |     UNDO = 'undo', | ||||||
|     OPEN_DIAGRAM = 'open_diagram', |     OPEN_DIAGRAM = 'open_diagram', | ||||||
|     SAVE_DIAGRAM = 'save_diagram', |     SAVE_DIAGRAM = 'save_diagram', | ||||||
|  |     TOGGLE_SIDE_PANEL = 'toggle_side_panel', | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface KeyboardShortcut { | export interface KeyboardShortcut { | ||||||
| @@ -47,6 +48,13 @@ export const keyboardShortcuts: Record< | |||||||
|         keyCombinationMac: 'meta+s', |         keyCombinationMac: 'meta+s', | ||||||
|         keyCombinationWin: 'ctrl+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 { | export interface KeyboardShortcutForOS { | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ export interface LayoutContext { | |||||||
|     isSidePanelShowed: boolean; |     isSidePanelShowed: boolean; | ||||||
|     hideSidePanel: () => void; |     hideSidePanel: () => void; | ||||||
|     showSidePanel: () => void; |     showSidePanel: () => void; | ||||||
|  |     toggleSidePanel: () => void; | ||||||
|  |  | ||||||
|     isSelectSchemaOpen: boolean; |     isSelectSchemaOpen: boolean; | ||||||
|     openSelectSchema: () => void; |     openSelectSchema: () => void; | ||||||
| @@ -47,6 +48,7 @@ export const layoutContext = createContext<LayoutContext>({ | |||||||
|     isSidePanelShowed: false, |     isSidePanelShowed: false, | ||||||
|     hideSidePanel: emptyFn, |     hideSidePanel: emptyFn, | ||||||
|     showSidePanel: emptyFn, |     showSidePanel: emptyFn, | ||||||
|  |     toggleSidePanel: emptyFn, | ||||||
|  |  | ||||||
|     isSelectSchemaOpen: false, |     isSelectSchemaOpen: false, | ||||||
|     openSelectSchema: emptyFn, |     openSelectSchema: emptyFn, | ||||||
|   | |||||||
| @@ -36,6 +36,10 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|     const showSidePanel: LayoutContext['showSidePanel'] = () => |     const showSidePanel: LayoutContext['showSidePanel'] = () => | ||||||
|         setIsSidePanelShowed(true); |         setIsSidePanelShowed(true); | ||||||
|  |  | ||||||
|  |     const toggleSidePanel: LayoutContext['toggleSidePanel'] = () => { | ||||||
|  |         setIsSidePanelShowed((prevIsSidePanelShowed) => !prevIsSidePanelShowed); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     const openTableFromSidebar: LayoutContext['openTableFromSidebar'] = ( |     const openTableFromSidebar: LayoutContext['openTableFromSidebar'] = ( | ||||||
|         tableId |         tableId | ||||||
|     ) => { |     ) => { | ||||||
| @@ -77,6 +81,7 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | |||||||
|                 isSidePanelShowed, |                 isSidePanelShowed, | ||||||
|                 hideSidePanel, |                 hideSidePanel, | ||||||
|                 showSidePanel, |                 showSidePanel, | ||||||
|  |                 toggleSidePanel, | ||||||
|                 isSelectSchemaOpen, |                 isSelectSchemaOpen, | ||||||
|                 openSelectSchema, |                 openSelectSchema, | ||||||
|                 closeSelectSchema, |                 closeSelectSchema, | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import { | |||||||
|     DialogDescription, |     DialogDescription, | ||||||
|     DialogFooter, |     DialogFooter, | ||||||
|     DialogHeader, |     DialogHeader, | ||||||
|  |     DialogInternalContent, | ||||||
|     DialogTitle, |     DialogTitle, | ||||||
| } from '@/components/dialog/dialog'; | } from '@/components/dialog/dialog'; | ||||||
| import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group'; | import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group'; | ||||||
| @@ -139,6 +140,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | |||||||
|             setScriptResult(fixedJson); |             setScriptResult(fixedJson); | ||||||
|             setErrorMessage(''); |             setErrorMessage(''); | ||||||
|         } else { |         } else { | ||||||
|  |             setScriptResult(fixedJson); | ||||||
|             setErrorMessage(errorScriptOutputMessage); |             setErrorMessage(errorScriptOutputMessage); | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -157,188 +159,201 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | |||||||
|  |  | ||||||
|     const renderContent = useCallback(() => { |     const renderContent = useCallback(() => { | ||||||
|         return ( |         return ( | ||||||
|             <div className="flex w-full flex-1 flex-col gap-6"> |             <DialogInternalContent> | ||||||
|                 {databaseTypeToEditionMap[databaseType].length > 0 ? ( |                 <div className="flex w-full flex-1 flex-col gap-6"> | ||||||
|                     <div className="flex flex-col gap-1 md:flex-row"> |                     {databaseTypeToEditionMap[databaseType].length > 0 ? ( | ||||||
|                         <p className="text-sm leading-6 text-muted-foreground"> |                         <div className="flex flex-col gap-1 md:flex-row"> | ||||||
|                             {t( |                             <p className="text-sm leading-6 text-muted-foreground"> | ||||||
|                                 'new_diagram_dialog.import_database.database_edition' |                                 {t( | ||||||
|                             )} |                                     'new_diagram_dialog.import_database.database_edition' | ||||||
|                         </p> |                                 )} | ||||||
|                         <ToggleGroup |                             </p> | ||||||
|                             type="single" |                             <ToggleGroup | ||||||
|                             className="ml-1 flex-wrap gap-2" |                                 type="single" | ||||||
|                             value={ |                                 className="ml-1 flex-wrap gap-2" | ||||||
|                                 !databaseEdition ? 'regular' : databaseEdition |                                 value={ | ||||||
|                             } |                                     !databaseEdition | ||||||
|                             onValueChange={(value) => { |                                         ? 'regular' | ||||||
|                                 setDatabaseEdition( |                                         : databaseEdition | ||||||
|                                     value === 'regular' |                                 } | ||||||
|                                         ? undefined |                                 onValueChange={(value) => { | ||||||
|                                         : (value as DatabaseEdition) |                                     setDatabaseEdition( | ||||||
|                                 ); |                                         value === 'regular' | ||||||
|                             }} |                                             ? undefined | ||||||
|                         > |                                             : (value as DatabaseEdition) | ||||||
|                             <ToggleGroupItem |                                     ); | ||||||
|                                 value="regular" |                                 }} | ||||||
|                                 variant="outline" |  | ||||||
|                                 className="h-6 gap-1 p-0 px-2 shadow-none" |  | ||||||
|                             > |                             > | ||||||
|                                 <Avatar className="size-4 rounded-none"> |                                 <ToggleGroupItem | ||||||
|                                     <AvatarImage |                                     value="regular" | ||||||
|                                         src={ |                                     variant="outline" | ||||||
|                                             databaseSecondaryLogoMap[ |                                     className="h-6 gap-1 p-0 px-2 shadow-none" | ||||||
|                                                 databaseType |                                 > | ||||||
|                                             ] |                                     <Avatar className="size-4 rounded-none"> | ||||||
|                                         } |                                         <AvatarImage | ||||||
|                                         alt="Regular" |                                             src={ | ||||||
|                                     /> |                                                 databaseSecondaryLogoMap[ | ||||||
|                                     <AvatarFallback>Regular</AvatarFallback> |                                                     databaseType | ||||||
|                                 </Avatar> |                                                 ] | ||||||
|                                 Regular |                                             } | ||||||
|                             </ToggleGroupItem> |                                             alt="Regular" | ||||||
|                             {databaseTypeToEditionMap[databaseType].map( |                                         /> | ||||||
|                                 (edition) => ( |                                         <AvatarFallback>Regular</AvatarFallback> | ||||||
|                                     <ToggleGroupItem |                                     </Avatar> | ||||||
|                                         value={edition} |                                     Regular | ||||||
|                                         key={edition} |                                 </ToggleGroupItem> | ||||||
|                                         variant="outline" |                                 {databaseTypeToEditionMap[databaseType].map( | ||||||
|                                         className="h-6 gap-1 p-0 px-2 shadow-none" |                                     (edition) => ( | ||||||
|                                     > |                                         <ToggleGroupItem | ||||||
|                                         <Avatar className="size-4"> |                                             value={edition} | ||||||
|                                             <AvatarImage |                                             key={edition} | ||||||
|                                                 src={ |                                             variant="outline" | ||||||
|                                                     databaseEditionToImageMap[ |                                             className="h-6 gap-1 p-0 px-2 shadow-none" | ||||||
|                                                         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]} |                                             <Avatar className="size-4"> | ||||||
|                                         </TabsTrigger> |                                                 <AvatarImage | ||||||
|                                     )) ?? []} |                                                     src={ | ||||||
|                                 </TabsList> |                                                         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> |                             </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 |                             <CodeSnippet | ||||||
|                                 className="h-40 w-full" |                                 className="h-40 w-full flex-auto" | ||||||
|                                 loading={!importMetadataScripts} |                                 loading={!importMetadataScripts} | ||||||
|                                 code={ |                                 code={ | ||||||
|                                     importMetadataScripts?.[databaseType]?.({ |                                     importMetadataScripts?.[databaseType]?.({ | ||||||
|                                         databaseEdition, |                                         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} |                     </div> | ||||||
|                         onChange={handleInputChange} |                     <div className="flex h-48 flex-col gap-1"> | ||||||
|                     /> |                         <p className="text-sm text-muted-foreground"> | ||||||
|                     {showCheckJsonButton || errorMessage ? ( |                             2. {t('new_diagram_dialog.import_database.step_2')} | ||||||
|                         <div className="mt-2 flex items-center gap-2"> |                         </p> | ||||||
|                             {showCheckJsonButton ? ( |                         <Textarea | ||||||
|                                 <Button |                             className="w-full flex-1 rounded-md bg-muted p-2 text-sm" | ||||||
|                                     type="button" |                             placeholder={t( | ||||||
|                                     variant="outline" |                                 'new_diagram_dialog.import_database.script_results_placeholder' | ||||||
|                                     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> |                             value={scriptResult} | ||||||
|                     ) : null} |                             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> | ||||||
|             </div> |             </DialogInternalContent> | ||||||
|         ); |         ); | ||||||
|     }, [ |     }, [ | ||||||
|         databaseEdition, |         databaseEdition, | ||||||
|   | |||||||
| @@ -128,7 +128,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({ | |||||||
|             }} |             }} | ||||||
|         > |         > | ||||||
|             <DialogContent |             <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} |                 showClose={hasExistingDiagram} | ||||||
|             > |             > | ||||||
|                 {step === CreateDiagramDialogStep.SELECT_DATABASE ? ( |                 {step === CreateDiagramDialogStep.SELECT_DATABASE ? ( | ||||||
|   | |||||||
| @@ -5,11 +5,13 @@ import { | |||||||
|     DialogDescription, |     DialogDescription, | ||||||
|     DialogFooter, |     DialogFooter, | ||||||
|     DialogHeader, |     DialogHeader, | ||||||
|  |     DialogInternalContent, | ||||||
|     DialogTitle, |     DialogTitle, | ||||||
| } from '@/components/dialog/dialog'; | } from '@/components/dialog/dialog'; | ||||||
| import { DatabaseType } from '@/lib/domain/database-type'; | import { DatabaseType } from '@/lib/domain/database-type'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { SelectDatabaseContent } from './select-database-content'; | import { SelectDatabaseContent } from './select-database-content'; | ||||||
|  | import { useDialog } from '@/hooks/use-dialog'; | ||||||
|  |  | ||||||
| export interface SelectDatabaseProps { | export interface SelectDatabaseProps { | ||||||
|     onContinue: () => void; |     onContinue: () => void; | ||||||
| @@ -27,6 +29,7 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({ | |||||||
|     createNewDiagram, |     createNewDiagram, | ||||||
| }) => { | }) => { | ||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
|  |     const { openImportDiagramDialog } = useDialog(); | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <> |         <> | ||||||
| @@ -38,11 +41,13 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({ | |||||||
|                     {t('new_diagram_dialog.database_selection.description')} |                     {t('new_diagram_dialog.database_selection.description')} | ||||||
|                 </DialogDescription> |                 </DialogDescription> | ||||||
|             </DialogHeader> |             </DialogHeader> | ||||||
|             <SelectDatabaseContent |             <DialogInternalContent> | ||||||
|                 databaseType={databaseType} |                 <SelectDatabaseContent | ||||||
|                 onContinue={onContinue} |                     databaseType={databaseType} | ||||||
|                 setDatabaseType={setDatabaseType} |                     onContinue={onContinue} | ||||||
|             /> |                     setDatabaseType={setDatabaseType} | ||||||
|  |                 /> | ||||||
|  |             </DialogInternalContent> | ||||||
|             <DialogFooter className="mt-4 flex !justify-between gap-2"> |             <DialogFooter className="mt-4 flex !justify-between gap-2"> | ||||||
|                 {hasExistingDiagram ? ( |                 {hasExistingDiagram ? ( | ||||||
|                     <DialogClose asChild> |                     <DialogClose asChild> | ||||||
| @@ -51,7 +56,13 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({ | |||||||
|                         </Button> |                         </Button> | ||||||
|                     </DialogClose> |                     </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"> |                 <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2"> | ||||||
|                     <Button |                     <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, |     DialogDescription, | ||||||
|     DialogFooter, |     DialogFooter, | ||||||
|     DialogHeader, |     DialogHeader, | ||||||
|  |     DialogInternalContent, | ||||||
|     DialogTitle, |     DialogTitle, | ||||||
| } from '@/components/dialog/dialog'; | } from '@/components/dialog/dialog'; | ||||||
| import { Label } from '@/components/label/label'; | import { Label } from '@/components/label/label'; | ||||||
| @@ -20,7 +21,7 @@ import { | |||||||
| import { databaseTypeToLabelMap } from '@/lib/databases'; | import { databaseTypeToLabelMap } from '@/lib/databases'; | ||||||
| import { DatabaseType } from '@/lib/domain/database-type'; | import { DatabaseType } from '@/lib/domain/database-type'; | ||||||
| import { Annoyed, Sparkles } from 'lucide-react'; | 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 { Trans, useTranslation } from 'react-i18next'; | ||||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||||
|  |  | ||||||
| @@ -37,28 +38,47 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | |||||||
|     const { t } = useTranslation(); |     const { t } = useTranslation(); | ||||||
|     const [script, setScript] = React.useState<string>(); |     const [script, setScript] = React.useState<string>(); | ||||||
|     const [error, setError] = React.useState<boolean>(false); |     const [error, setError] = React.useState<boolean>(false); | ||||||
|  |     const [isScriptLoading, setIsScriptLoading] = | ||||||
|  |         React.useState<boolean>(false); | ||||||
|  |     const abortControllerRef = useRef<AbortController | null>(null); | ||||||
|  |  | ||||||
|     const exportSQLScript = useCallback(async () => { |     const exportSQLScript = useCallback(async () => { | ||||||
|         if (targetDatabaseType === DatabaseType.GENERIC) { |         if (targetDatabaseType === DatabaseType.GENERIC) { | ||||||
|             return Promise.resolve(exportBaseSQL(currentDiagram)); |             return Promise.resolve(exportBaseSQL(currentDiagram)); | ||||||
|         } else { |         } 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]); |     }, [targetDatabaseType, currentDiagram]); | ||||||
|  |  | ||||||
|     useEffect(() => { |     useEffect(() => { | ||||||
|         if (!dialog.open) return; |         if (!dialog.open) { | ||||||
|  |             abortControllerRef.current?.abort(); | ||||||
|  |  | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         abortControllerRef.current = new AbortController(); | ||||||
|         setScript(undefined); |         setScript(undefined); | ||||||
|         setError(false); |         setError(false); | ||||||
|         const fetchScript = async () => { |         const fetchScript = async () => { | ||||||
|             try { |             try { | ||||||
|  |                 setIsScriptLoading(true); | ||||||
|                 const script = await exportSQLScript(); |                 const script = await exportSQLScript(); | ||||||
|                 setScript(script); |                 setScript(script); | ||||||
|  |                 setIsScriptLoading(false); | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|                 setError(true); |                 setError(true); | ||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
|         fetchScript(); |         fetchScript(); | ||||||
|  |  | ||||||
|  |         return () => { | ||||||
|  |             abortControllerRef.current?.abort(); | ||||||
|  |         }; | ||||||
|     }, [dialog.open, setScript, exportSQLScript, setError]); |     }, [dialog.open, setScript, exportSQLScript, setError]); | ||||||
|  |  | ||||||
|     const renderError = useCallback( |     const renderError = useCallback( | ||||||
| @@ -132,7 +152,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | |||||||
|             }} |             }} | ||||||
|         > |         > | ||||||
|             <DialogContent |             <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 |                 showClose | ||||||
|             > |             > | ||||||
|                 <DialogHeader> |                 <DialogHeader> | ||||||
| @@ -148,18 +168,24 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | |||||||
|                         })} |                         })} | ||||||
|                     </DialogDescription> |                     </DialogDescription> | ||||||
|                 </DialogHeader> |                 </DialogHeader> | ||||||
|                 <div className="flex flex-1 items-center justify-center"> |                 <DialogInternalContent> | ||||||
|                     {error ? ( |                     <div className="flex flex-1 items-center justify-center"> | ||||||
|                         renderError() |                         {error ? ( | ||||||
|                     ) : script === undefined ? ( |                             renderError() | ||||||
|                         renderLoader() |                         ) : script === undefined ? ( | ||||||
|                     ) : script.length === 0 ? ( |                             renderLoader() | ||||||
|                         renderError() |                         ) : script.length === 0 ? ( | ||||||
|                     ) : ( |                             renderError() | ||||||
|                         <CodeSnippet className="h-96 w-full" code={script!} /> |                         ) : ( | ||||||
|                     )} |                             <CodeSnippet | ||||||
|                 </div> |                                 className="h-96 w-full" | ||||||
|  |                                 code={script!} | ||||||
|  |                                 autoScroll={true} | ||||||
|  |                                 isComplete={!isScriptLoading} | ||||||
|  |                             /> | ||||||
|  |                         )} | ||||||
|  |                     </div> | ||||||
|  |                 </DialogInternalContent> | ||||||
|                 <DialogFooter className="flex !justify-between gap-2"> |                 <DialogFooter className="flex !justify-between gap-2"> | ||||||
|                     <div /> |                     <div /> | ||||||
|                     <DialogClose asChild> |                     <DialogClose asChild> | ||||||
|   | |||||||
| @@ -323,7 +323,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({ | |||||||
|             }} |             }} | ||||||
|         > |         > | ||||||
|             <DialogContent |             <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 |                 showClose | ||||||
|             > |             > | ||||||
|                 <ImportDatabase |                 <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, |     DialogDescription, | ||||||
|     DialogFooter, |     DialogFooter, | ||||||
|     DialogHeader, |     DialogHeader, | ||||||
|  |     DialogInternalContent, | ||||||
|     DialogTitle, |     DialogTitle, | ||||||
| } from '@/components/dialog/dialog'; | } from '@/components/dialog/dialog'; | ||||||
| import { ScrollArea } from '@/components/scroll-area/scroll-area'; |  | ||||||
| import { | import { | ||||||
|     Table, |     Table, | ||||||
|     TableBody, |     TableBody, | ||||||
| @@ -74,7 +74,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | |||||||
|             }} |             }} | ||||||
|         > |         > | ||||||
|             <DialogContent |             <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 |                 showClose | ||||||
|             > |             > | ||||||
|                 <DialogHeader> |                 <DialogHeader> | ||||||
| @@ -83,9 +83,9 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | |||||||
|                         {t('open_diagram_dialog.description')} |                         {t('open_diagram_dialog.description')} | ||||||
|                     </DialogDescription> |                     </DialogDescription> | ||||||
|                 </DialogHeader> |                 </DialogHeader> | ||||||
|                 <div className="flex flex-1 items-center justify-center"> |                 <DialogInternalContent> | ||||||
|                     <ScrollArea className="h-80 w-full"> |                     <div className="flex flex-1 items-center justify-center"> | ||||||
|                         <Table className="h-fit"> |                         <Table> | ||||||
|                             <TableHeader className="sticky top-0 bg-background"> |                             <TableHeader className="sticky top-0 bg-background"> | ||||||
|                                 <TableRow> |                                 <TableRow> | ||||||
|                                     <TableHead /> |                                     <TableHead /> | ||||||
| @@ -155,8 +155,8 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | |||||||
|                                 ))} |                                 ))} | ||||||
|                             </TableBody> |                             </TableBody> | ||||||
|                         </Table> |                         </Table> | ||||||
|                     </ScrollArea> |                     </div> | ||||||
|                 </div> |                 </DialogInternalContent> | ||||||
|  |  | ||||||
|                 <DialogFooter className="flex !justify-between gap-2"> |                 <DialogFooter className="flex !justify-between gap-2"> | ||||||
|                     <DialogClose asChild> |                     <DialogClose asChild> | ||||||
|   | |||||||
							
								
								
									
										128
									
								
								src/globals.css
									
									
									
									
									
								
							
							
						
						| @@ -3,69 +3,73 @@ | |||||||
| @tailwind utilities; | @tailwind utilities; | ||||||
|  |  | ||||||
| @layer base { | @layer base { | ||||||
|   :root { |     :root { | ||||||
|     --background: 0 0% 100%; |         --background: 0 0% 100%; | ||||||
|     --foreground: 222.2 84% 4.9%; |         --foreground: 222.2 84% 4.9%; | ||||||
|     --card: 0 0% 100%; |         --card: 0 0% 100%; | ||||||
|     --card-foreground: 222.2 84% 4.9%; |         --card-foreground: 222.2 84% 4.9%; | ||||||
|     --popover: 0 0% 100%; |         --popover: 0 0% 100%; | ||||||
|     --popover-foreground: 222.2 84% 4.9%; |         --popover-foreground: 222.2 84% 4.9%; | ||||||
|     --primary: 222.2 47.4% 11.2%; |         --primary: 222.2 47.4% 11.2%; | ||||||
|     --primary-foreground: 210 40% 98%; |         --primary-foreground: 210 40% 98%; | ||||||
|     --secondary: 210 40% 96.1%; |         --secondary: 210 40% 96.1%; | ||||||
|     --secondary-foreground: 222.2 47.4% 11.2%; |         --secondary-foreground: 222.2 47.4% 11.2%; | ||||||
|     --muted: 210 40% 96.1%; |         --muted: 210 40% 96.1%; | ||||||
|     --muted-foreground: 215.4 16.3% 46.9%; |         --muted-foreground: 215.4 16.3% 46.9%; | ||||||
|     --accent: 210 40% 96.1%; |         --accent: 210 40% 96.1%; | ||||||
|     --accent-foreground: 222.2 47.4% 11.2%; |         --accent-foreground: 222.2 47.4% 11.2%; | ||||||
|     --destructive: 0 84.2% 60.2%; |         --destructive: 0 84.2% 60.2%; | ||||||
|     --destructive-foreground: 210 40% 98%; |         --destructive-foreground: 210 40% 98%; | ||||||
|     --border: 214.3 31.8% 91.4%; |         --border: 214.3 31.8% 91.4%; | ||||||
|     --input: 214.3 31.8% 91.4%; |         --input: 214.3 31.8% 91.4%; | ||||||
|     --ring: 222.2 84% 4.9%; |         --ring: 222.2 84% 4.9%; | ||||||
|     --radius: 0.5rem; |         --radius: 0.5rem; | ||||||
|     --chart-1: 12 76% 61%; |         --chart-1: 12 76% 61%; | ||||||
|     --chart-2: 173 58% 39%; |         --chart-2: 173 58% 39%; | ||||||
|     --chart-3: 197 37% 24%; |         --chart-3: 197 37% 24%; | ||||||
|     --chart-4: 43 74% 66%; |         --chart-4: 43 74% 66%; | ||||||
|     --chart-5: 27 87% 67%; |         --chart-5: 27 87% 67%; | ||||||
|     --subtitle: 215.3 19.3% 34.5%; |         --subtitle: 215.3 19.3% 34.5%; | ||||||
|   } |     } | ||||||
|  |  | ||||||
|   .dark { |     .dark { | ||||||
|     --background: 222.2 84% 4.9%; |         --background: 222.2 84% 4.9%; | ||||||
|     --foreground: 210 40% 98%; |         --foreground: 210 40% 98%; | ||||||
|     --card: 222.2 84% 4.9%; |         --card: 222.2 84% 4.9%; | ||||||
|     --card-foreground: 210 40% 98%; |         --card-foreground: 210 40% 98%; | ||||||
|     --popover: 222.2 84% 4.9%; |         --popover: 222.2 84% 4.9%; | ||||||
|     --popover-foreground: 210 40% 98%; |         --popover-foreground: 210 40% 98%; | ||||||
|     --primary: 210 40% 98%; |         --primary: 210 40% 98%; | ||||||
|     --primary-foreground: 222.2 47.4% 11.2%; |         --primary-foreground: 222.2 47.4% 11.2%; | ||||||
|     --secondary: 217.2 32.6% 17.5%; |         --secondary: 217.2 32.6% 17.5%; | ||||||
|     --secondary-foreground: 210 40% 98%; |         --secondary-foreground: 210 40% 98%; | ||||||
|     --muted: 217.2 32.6% 17.5%; |         --muted: 217.2 32.6% 17.5%; | ||||||
|     --muted-foreground: 215 20.2% 65.1%; |         --muted-foreground: 215 20.2% 65.1%; | ||||||
|     --accent: 217.2 32.6% 17.5%; |         --accent: 217.2 32.6% 17.5%; | ||||||
|     --accent-foreground: 210 40% 98%; |         --accent-foreground: 210 40% 98%; | ||||||
|     --destructive: 0 62.8% 30.6%; |         --destructive: 0 62.8% 30.6%; | ||||||
|     --destructive-foreground: 210 40% 98%; |         --destructive-foreground: 210 40% 98%; | ||||||
|     --border: 217.2 32.6% 17.5%; |         --border: 217.2 32.6% 17.5%; | ||||||
|     --input: 217.2 32.6% 17.5%; |         --input: 217.2 32.6% 17.5%; | ||||||
|     --ring: 212.7 26.8% 83.9%; |         --ring: 212.7 26.8% 83.9%; | ||||||
|     --chart-1: 220 70% 50%; |         --chart-1: 220 70% 50%; | ||||||
|     --chart-2: 160 60% 45%; |         --chart-2: 160 60% 45%; | ||||||
|     --chart-3: 30 80% 55%; |         --chart-3: 30 80% 55%; | ||||||
|     --chart-4: 280 65% 60%; |         --chart-4: 280 65% 60%; | ||||||
|     --chart-5: 340 75% 55%; |         --chart-5: 340 75% 55%; | ||||||
|     --subtitle: 212.7 26.8% 83.9%; |         --subtitle: 212.7 26.8% 83.9%; | ||||||
|   } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @layer base { | @layer base { | ||||||
|   * { |     * { | ||||||
|     @apply border-border; |         @apply border-border; | ||||||
|   } |     } | ||||||
|   body { |     body { | ||||||
|     @apply bg-background text-foreground; |         @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 |         <meta | ||||||
|             property="og:image" |             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 property="og:url" content="https://app.chartdb.io" /> | ||||||
|         <meta name="twitter:card" content="summary_large_image" /> |         <meta name="twitter:card" content="summary_large_image" /> | ||||||
| @@ -32,7 +32,7 @@ export const HelmetData: React.FC = () => ( | |||||||
|         /> |         /> | ||||||
|         <meta |         <meta | ||||||
|             name="twitter:image" |             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> |         <title>ChartDB - Database schema diagrams visualizer</title> | ||||||
|     </Helmet> |     </Helmet> | ||||||
|   | |||||||
| @@ -1,12 +1,48 @@ | |||||||
| import i18n from 'i18next'; | import i18n from 'i18next'; | ||||||
| import { initReactI18next } from 'react-i18next'; | import { initReactI18next } from 'react-i18next'; | ||||||
|  | import LanguageDetector from 'i18next-browser-languagedetector'; | ||||||
|  | import type { LanguageMetadata } from './types'; | ||||||
| import { en, enMetadata } from './locales/en'; | import { en, enMetadata } from './locales/en'; | ||||||
| import { es } from './locales/es'; | import { es, esMetadata } from './locales/es'; | ||||||
| import { fr } from './locales/fr'; | import { fr, frMetadata } from './locales/fr'; | ||||||
| import { de } from './locales/de'; | import { de, deMetadata } from './locales/de'; | ||||||
| import { hi } from './locales/hi'; | import { hi, hiMetadata } from './locales/hi'; | ||||||
| import { ja } from './locales/ja'; | import { ja, jaMetadata } from './locales/ja'; | ||||||
| import { pt_BR } from './locales/pt_BR'; | 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 = { | const resources = { | ||||||
|     en, |     en, | ||||||
| @@ -15,17 +51,30 @@ const resources = { | |||||||
|     de, |     de, | ||||||
|     hi, |     hi, | ||||||
|     ja, |     ja, | ||||||
|  |     ko_KR, | ||||||
|     pt_BR, |     pt_BR, | ||||||
|  |     uk, | ||||||
|  |     ru, | ||||||
|  |     zh_CN, | ||||||
|  |     zh_TW, | ||||||
|  |     ne, | ||||||
|  |     mr, | ||||||
|  |     tr, | ||||||
|  |     id_ID, | ||||||
|  |     te, | ||||||
|  |     gu, | ||||||
|  |     vi, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| i18n.use(initReactI18next).init({ | i18n.use(LanguageDetector) | ||||||
|     resources, |     .use(initReactI18next) | ||||||
|     lng: enMetadata.code, |     .init({ | ||||||
|     interpolation: { |         resources, | ||||||
|         escapeValue: false, |         interpolation: { | ||||||
|     }, |             escapeValue: false, | ||||||
|     fallbackLng: enMetadata.code, |         }, | ||||||
|     debug: false, |         fallbackLng: enMetadata.code, | ||||||
| }); |         debug: false, | ||||||
|  |     }); | ||||||
|  |  | ||||||
| export { i18n }; | export { i18n }; | ||||||
|   | |||||||
| @@ -28,10 +28,15 @@ export const de: LanguageTranslation = { | |||||||
|                 show_cardinality: 'Kardinalität anzeigen', |                 show_cardinality: 'Kardinalität anzeigen', | ||||||
|                 zoom_on_scroll: 'Zoom beim Scrollen', |                 zoom_on_scroll: 'Zoom beim Scrollen', | ||||||
|                 theme: 'Stil', |                 theme: 'Stil', | ||||||
|                 change_language: 'Sprache', |  | ||||||
|                 show_dependencies: 'Abhängigkeiten anzeigen', |                 show_dependencies: 'Abhängigkeiten anzeigen', | ||||||
|                 hide_dependencies: 'Abhängigkeiten ausblenden', |                 hide_dependencies: 'Abhängigkeiten ausblenden', | ||||||
|             }, |             }, | ||||||
|  |             // TODO: Translate | ||||||
|  |             share: { | ||||||
|  |                 share: 'Share', | ||||||
|  |                 export_diagram: 'Export Diagram', | ||||||
|  |                 import_diagram: 'Import Diagram', | ||||||
|  |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'Hilfe', |                 help: 'Hilfe', | ||||||
|                 visit_website: 'ChartDB Webseite', |                 visit_website: 'ChartDB Webseite', | ||||||
| @@ -139,6 +144,7 @@ export const de: LanguageTranslation = { | |||||||
|                         change_schema: 'Schema ändern', |                         change_schema: 'Schema ändern', | ||||||
|                         add_field: 'Feld hinzufügen', |                         add_field: 'Feld hinzufügen', | ||||||
|                         add_index: 'Index hinzufügen', |                         add_index: 'Index hinzufügen', | ||||||
|  |                         duplicate_table: 'Duplicate Table', // TODO: Translate | ||||||
|                         delete_table: 'Tabelle löschen', |                         delete_table: 'Tabelle löschen', | ||||||
|                     }, |                     }, | ||||||
|                 }, |                 }, | ||||||
| @@ -226,6 +232,8 @@ export const de: LanguageTranslation = { | |||||||
|  |  | ||||||
|             cancel: 'Abbrechen', |             cancel: 'Abbrechen', | ||||||
|             back: 'Zurück', |             back: 'Zurück', | ||||||
|  |             // TODO: Translate | ||||||
|  |             import_from_file: 'Import from File', | ||||||
|             empty_diagram: 'Leeres Diagramm', |             empty_diagram: 'Leeres Diagramm', | ||||||
|             continue: 'Weiter', |             continue: 'Weiter', | ||||||
|             import: 'Importieren', |             import: 'Importieren', | ||||||
| @@ -329,7 +337,31 @@ export const de: LanguageTranslation = { | |||||||
|             close: 'Nicht jetzt', |             close: 'Nicht jetzt', | ||||||
|             confirm: 'Natürlich!', |             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: { |         relationship_type: { | ||||||
|             one_to_one: 'Ein zu Eins (1:1)', |             one_to_one: 'Ein zu Eins (1:1)', | ||||||
|             one_to_many: 'Ein zu Viele (1:n)', |             one_to_many: 'Ein zu Viele (1:n)', | ||||||
| @@ -344,12 +376,25 @@ export const de: LanguageTranslation = { | |||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'Tabelle bearbeiten', |             edit_table: 'Tabelle bearbeiten', | ||||||
|  |             duplicate_table: 'Duplicate Table', // TODO: Translate | ||||||
|             delete_table: 'Tabelle löschen', |             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 = { | export const deMetadata: LanguageMetadata = { | ||||||
|     name: 'Deutsch', |     name: 'German', | ||||||
|  |     nativeName: 'Deutsch', | ||||||
|     code: 'de', |     code: 'de', | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -28,10 +28,14 @@ export const en = { | |||||||
|                 show_cardinality: 'Show Cardinality', |                 show_cardinality: 'Show Cardinality', | ||||||
|                 zoom_on_scroll: 'Zoom on Scroll', |                 zoom_on_scroll: 'Zoom on Scroll', | ||||||
|                 theme: 'Theme', |                 theme: 'Theme', | ||||||
|                 change_language: 'Language', |  | ||||||
|                 show_dependencies: 'Show Dependencies', |                 show_dependencies: 'Show Dependencies', | ||||||
|                 hide_dependencies: 'Hide Dependencies', |                 hide_dependencies: 'Hide Dependencies', | ||||||
|             }, |             }, | ||||||
|  |             share: { | ||||||
|  |                 share: 'Share', | ||||||
|  |                 export_diagram: 'Export Diagram', | ||||||
|  |                 import_diagram: 'Import Diagram', | ||||||
|  |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'Help', |                 help: 'Help', | ||||||
|                 visit_website: 'Visit ChartDB', |                 visit_website: 'Visit ChartDB', | ||||||
| @@ -139,6 +143,7 @@ export const en = { | |||||||
|                         change_schema: 'Change Schema', |                         change_schema: 'Change Schema', | ||||||
|                         add_field: 'Add Field', |                         add_field: 'Add Field', | ||||||
|                         add_index: 'Add Index', |                         add_index: 'Add Index', | ||||||
|  |                         duplicate_table: 'Duplicate Table', | ||||||
|                         delete_table: 'Delete Table', |                         delete_table: 'Delete Table', | ||||||
|                     }, |                     }, | ||||||
|                 }, |                 }, | ||||||
| @@ -224,6 +229,7 @@ export const en = { | |||||||
|             }, |             }, | ||||||
|  |  | ||||||
|             cancel: 'Cancel', |             cancel: 'Cancel', | ||||||
|  |             import_from_file: 'Import from File', | ||||||
|             back: 'Back', |             back: 'Back', | ||||||
|             empty_diagram: 'Empty diagram', |             empty_diagram: 'Empty diagram', | ||||||
|             continue: 'Continue', |             continue: 'Continue', | ||||||
| @@ -328,7 +334,30 @@ export const en = { | |||||||
|             close: 'Not now', |             close: 'Not now', | ||||||
|             confirm: 'Of course!', |             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: { |         relationship_type: { | ||||||
|             one_to_one: 'One to One', |             one_to_one: 'One to One', | ||||||
|             one_to_many: 'One to Many', |             one_to_many: 'One to Many', | ||||||
| @@ -343,12 +372,24 @@ export const en = { | |||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'Edit Table', |             edit_table: 'Edit Table', | ||||||
|  |             duplicate_table: 'Duplicate Table', | ||||||
|             delete_table: 'Delete 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 = { | export const enMetadata: LanguageMetadata = { | ||||||
|     name: 'English', |     name: 'English', | ||||||
|  |     nativeName: 'English', | ||||||
|     code: 'en', |     code: 'en', | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -28,10 +28,14 @@ export const es: LanguageTranslation = { | |||||||
|                 hide_sidebar: 'Ocultar Barra Lateral', |                 hide_sidebar: 'Ocultar Barra Lateral', | ||||||
|                 zoom_on_scroll: 'Zoom al Desplazarse', |                 zoom_on_scroll: 'Zoom al Desplazarse', | ||||||
|                 theme: 'Tema', |                 theme: 'Tema', | ||||||
|                 change_language: 'Idioma', |                 show_dependencies: 'Mostrar dependencias', | ||||||
|                 // TODO: Translate |                 hide_dependencies: 'Ocultar dependencias', | ||||||
|                 show_dependencies: 'Show Dependencies', |             }, | ||||||
|                 hide_dependencies: 'Hide Dependencies', |             // TODO: Translate | ||||||
|  |             share: { | ||||||
|  |                 share: 'Share', | ||||||
|  |                 export_diagram: 'Export Diagram', | ||||||
|  |                 import_diagram: 'Import Diagram', | ||||||
|             }, |             }, | ||||||
|             help: { |             help: { | ||||||
|                 help: 'Ayuda', |                 help: 'Ayuda', | ||||||
| @@ -80,20 +84,19 @@ export const es: LanguageTranslation = { | |||||||
|         saved: 'Guardado', |         saved: 'Guardado', | ||||||
|         diagrams: 'Diagramas', |         diagrams: 'Diagramas', | ||||||
|         loading_diagram: 'Cargando diagrama...', |         loading_diagram: 'Cargando diagrama...', | ||||||
|         deselect_all: 'Deselect All', // TODO: Translate |         deselect_all: 'Deseleccionar todo', | ||||||
|         select_all: 'Select All', // TODO: Translate |         select_all: 'Seleccionar todo', | ||||||
|         clear: 'Clear', // TODO: Translate |         clear: 'Limpiar', | ||||||
|         show_more: 'Show More', // TODO: Translate |         show_more: 'Mostrar más', | ||||||
|         show_less: 'Show Less', // TODO: Translate |         show_less: 'Mostrar menos', | ||||||
|         // TODO: Translate |  | ||||||
|         copy_to_clipboard: 'Copy to Clipboard', |         copy_to_clipboard: 'Copy to Clipboard', | ||||||
|         copied: 'Copied!', |         copied: 'Copied!', | ||||||
|  |  | ||||||
|         side_panel: { |         side_panel: { | ||||||
|             schema: 'Schema:', // TODO: Translate |             schema: 'Esquema:', | ||||||
|             filter_by_schema: 'Filter by schema', // TODO: Translate |             filter_by_schema: 'Filtrar por esquema', | ||||||
|             search_schema: 'Search schema...', // TODO: Translate |             search_schema: 'Buscar esquema...', | ||||||
|             no_schemas_found: 'No schemas found.', // TODO: Translate |             no_schemas_found: 'No se encontraron esquemas.', | ||||||
|             view_all_options: 'Ver todas las opciones...', |             view_all_options: 'Ver todas las opciones...', | ||||||
|             tables_section: { |             tables_section: { | ||||||
|                 tables: 'Tablas', |                 tables: 'Tablas', | ||||||
| @@ -113,7 +116,7 @@ export const es: LanguageTranslation = { | |||||||
|                     index_select_fields: 'Seleccionar campos', |                     index_select_fields: 'Seleccionar campos', | ||||||
|                     field_name: 'Nombre', |                     field_name: 'Nombre', | ||||||
|                     field_type: 'Tipo', |                     field_type: 'Tipo', | ||||||
|                     no_types_found: 'No types found', // TODO: Translate |                     no_types_found: 'No se encontraron tipos', | ||||||
|                     field_actions: { |                     field_actions: { | ||||||
|                         title: 'Atributos del Campo', |                         title: 'Atributos del Campo', | ||||||
|                         unique: 'Único', |                         unique: 'Único', | ||||||
| @@ -132,6 +135,7 @@ export const es: LanguageTranslation = { | |||||||
|                         change_schema: 'Cambiar Esquema', |                         change_schema: 'Cambiar Esquema', | ||||||
|                         add_field: 'Agregar Campo', |                         add_field: 'Agregar Campo', | ||||||
|                         add_index: 'Agregar Índice', |                         add_index: 'Agregar Índice', | ||||||
|  |                         duplicate_table: 'Duplicate Table', // TODO: Translate | ||||||
|                         delete_table: 'Eliminar Tabla', |                         delete_table: 'Eliminar Tabla', | ||||||
|                     }, |                     }, | ||||||
|                 }, |                 }, | ||||||
| @@ -160,23 +164,22 @@ export const es: LanguageTranslation = { | |||||||
|                     description: 'Crea una relación para conectar tablas', |                     description: 'Crea una relación para conectar tablas', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|             // TODO: Translate |  | ||||||
|             dependencies_section: { |             dependencies_section: { | ||||||
|                 dependencies: 'Dependencies', |                 dependencies: 'Dependencias', | ||||||
|                 filter: 'Filter', |                 filter: 'Filtro', | ||||||
|                 collapse: 'Collapse All', |                 collapse: 'Colapsar todo', | ||||||
|                 dependency: { |                 dependency: { | ||||||
|                     table: 'Table', |                     table: 'Tabla', | ||||||
|                     dependent_table: 'Dependent View', |                     dependent_table: 'Vista dependiente', | ||||||
|                     delete_dependency: 'Delete', |                     delete_dependency: 'Eliminar', | ||||||
|                     dependency_actions: { |                     dependency_actions: { | ||||||
|                         title: 'Actions', |                         title: 'Acciones', | ||||||
|                         delete_dependency: 'Delete', |                         delete_dependency: 'Eliminar', | ||||||
|                     }, |                     }, | ||||||
|                 }, |                 }, | ||||||
|                 empty_state: { |                 empty_state: { | ||||||
|                     title: 'No dependencies', |                     title: 'Sin dependencias', | ||||||
|                     description: 'Create a view to get started', |                     description: 'Crea una vista para comenzar', | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
| @@ -189,8 +192,7 @@ export const es: LanguageTranslation = { | |||||||
|             undo: 'Deshacer', |             undo: 'Deshacer', | ||||||
|             redo: 'Rehacer', |             redo: 'Rehacer', | ||||||
|             reorder_diagram: 'Reordenar Diagrama', |             reorder_diagram: 'Reordenar Diagrama', | ||||||
|             // TODO: Translate |             highlight_overlapping_tables: 'Resaltar tablas superpuestas', | ||||||
|             highlight_overlapping_tables: 'Highlight Overlapping Tables', |  | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         new_diagram_dialog: { |         new_diagram_dialog: { | ||||||
| @@ -214,20 +216,20 @@ export const es: LanguageTranslation = { | |||||||
|                     step_1: 'Ve a Herramientas > Opciones > Resultados de Consulta > SQL Server.', |                     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).', |                     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: '¿Necesitas ayuda? mira cómo', | ||||||
|                 instructions_link: 'Need help? Watch how', |                 check_script_result: 'Revisa el resultado del script', | ||||||
|                 check_script_result: 'Check Script Result', |  | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|             cancel: 'Cancelar', |             cancel: 'Cancelar', | ||||||
|             back: 'Atrás', |             back: 'Atrás', | ||||||
|  |             // TODO: Translate | ||||||
|  |             import_from_file: 'Import from File', | ||||||
|             empty_diagram: 'Diagrama vacío', |             empty_diagram: 'Diagrama vacío', | ||||||
|             continue: 'Continuar', |             continue: 'Continuar', | ||||||
|             import: 'Importar', |             import: 'Importar', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         open_diagram_dialog: { |         open_diagram_dialog: { | ||||||
|             // TODO: Translate |  | ||||||
|             title: 'Abrir Diagrama', |             title: 'Abrir Diagrama', | ||||||
|             description: |             description: | ||||||
|                 'Selecciona un diagrama para abrir de la lista a continuación.', |                 'Selecciona un diagrama para abrir de la lista a continuación.', | ||||||
| @@ -293,16 +295,15 @@ export const es: LanguageTranslation = { | |||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         // TODO: Translate |  | ||||||
|         export_image_dialog: { |         export_image_dialog: { | ||||||
|             title: 'Export Image', |             title: 'Exportar imagen', | ||||||
|             description: 'Choose the scale factor for export:', |             description: 'Escoge el factor de escalamiento para exportar:', | ||||||
|             scale_1x: '1x Regular', |             scale_1x: '1x regular', | ||||||
|             scale_2x: '2x (Recommended)', |             scale_2x: '2x (recomendado)', | ||||||
|             scale_3x: '3x', |             scale_3x: '3x', | ||||||
|             scale_4x: '4x', |             scale_4x: '4x', | ||||||
|             cancel: 'Cancel', |             cancel: 'Cancelar', | ||||||
|             export: 'Export', |             export: 'Exportar', | ||||||
|         }, |         }, | ||||||
|  |  | ||||||
|         new_table_schema_dialog: { |         new_table_schema_dialog: { | ||||||
| @@ -336,7 +337,31 @@ export const es: LanguageTranslation = { | |||||||
|             change_schema: 'Cambiar', |             change_schema: 'Cambiar', | ||||||
|             none: 'nada', |             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: { |         relationship_type: { | ||||||
|             one_to_one: 'Uno a Uno', |             one_to_one: 'Uno a Uno', | ||||||
|             one_to_many: 'Uno a Muchos', |             one_to_many: 'Uno a Muchos', | ||||||
| @@ -351,12 +376,25 @@ export const es: LanguageTranslation = { | |||||||
|  |  | ||||||
|         table_node_context_menu: { |         table_node_context_menu: { | ||||||
|             edit_table: 'Editar Tabla', |             edit_table: 'Editar Tabla', | ||||||
|  |             duplicate_table: 'Duplicate Table', // TODO: Translate | ||||||
|             delete_table: 'Eliminar Tabla', |             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 = { | export const esMetadata: LanguageMetadata = { | ||||||
|     name: 'Español', |     name: 'Spanish', | ||||||
|  |     nativeName: 'Español', | ||||||
|     code: 'es', |     code: 'es', | ||||||
| }; | }; | ||||||
|   | |||||||