Compare commits
	
		
			77 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 47bb87a88f | ||
|  | a96c2e1078 | ||
|  | 26d95eed25 | ||
|  | be65328f24 | ||
|  | 85fd14fa02 | ||
|  | 9c485b3b01 | ||
|  | e993f1549c | ||
|  | 0db67ea42a | ||
|  | b9e621bd68 | ||
|  | 93d59f8887 | ||
|  | 190e4f4ffa | ||
|  | dc404c9d7e | ||
|  | dd4324d64f | ||
|  | 1878083056 | ||
|  | 7b6271962a | ||
|  | 2edc8dfde8 | ||
|  | 004d530880 | ||
|  | fd2cc9fcfc | ||
|  | 4c93326bb6 | ||
|  | ef3d7a8b67 | ||
|  | 3b3be086b1 | ||
|  | b424518212 | ||
|  | 99a8201398 | ||
|  | eb9b41e4f6 | ||
|  | fef6d3f499 | ||
|  | 14f11c27a7 | ||
|  | 2118bce0f0 | ||
|  | 88be6c1fd4 | ||
|  | 0dcc9b9568 | ||
|  | ff3269ec05 | ||
|  | 659dc2e3e7 | ||
|  | c36cd33180 | ||
|  | 58231c9139 | ||
|  | 1643e7bdeb | ||
|  | 42d4cbac8c | ||
|  | 7452ca6965 | ||
|  | 27aede7794 | ||
|  | e9e2736cb2 | ||
|  | 74c1730425 | ||
|  | 94bed7fcce | ||
|  | 8abf2a7bfc | ||
|  | ee659eaa03 | ||
|  | 7c5db0848e | ||
|  | 4b43f720e9 | ||
|  | 766b5164b8 | ||
|  | 7868ca9f42 | ||
|  | 0411742864 | ||
|  | 9831ac5a10 | ||
|  | 91c6fb9249 | ||
|  | c155013668 | ||
|  | 1b0f293c87 | ||
|  | df2dc03aa0 | ||
|  | 205d431c89 | ||
|  | 0abe18cdf9 | ||
|  | a151f56b5d | ||
|  | 2b6b733261 | ||
|  | b56b04925c | ||
|  | 635fb53c9f | ||
|  | d6659795bc | ||
|  | 348f80568e | ||
|  | 5f9c74a9ad | ||
|  | 5409288388 | ||
|  | 2309306ef5 | ||
|  | 3574cecc7c | ||
|  | 29b8edc051 | ||
|  | 5fc10a7e64 | ||
|  | 807cd22e0c | ||
|  | 03772f6b4f | ||
|  | 885eb719de | ||
|  | 94656ec7a5 | ||
|  | a0e966b64f | ||
|  | a8fe491c1b | ||
|  | ddeef3b134 | ||
|  | d45677e92d | ||
|  | 9c7d03c285 | ||
|  | be1b109f23 | ||
|  | 05eaf85a3d | 
| @@ -1,29 +0,0 @@ | ||||
| module.exports = { | ||||
|     root: true, | ||||
|     env: { browser: true, es2020: true }, | ||||
|     extends: [ | ||||
|         'eslint:recommended', | ||||
|         'plugin:react/recommended', | ||||
|         'plugin:@typescript-eslint/recommended', | ||||
|         'plugin:react-hooks/recommended', | ||||
|         'plugin:css-modules/recommended', | ||||
|         'plugin:tailwindcss/recommended', | ||||
|         'plugin:prettier/recommended', | ||||
|         // 'plugin:jsx-a11y/recommended', | ||||
|     ], | ||||
|     ignorePatterns: ['dist', '.eslintrc.cjs'], | ||||
|     parser: '@typescript-eslint/parser', | ||||
|     plugins: ['react-refresh', 'css-modules', 'tailwindcss', 'jsx-a11y'], | ||||
|     rules: { | ||||
|         '@typescript-eslint/consistent-type-imports': 'error', | ||||
|         'react-refresh/only-export-components': [ | ||||
|             'warn', | ||||
|             { allowConstantExport: true }, | ||||
|         ], | ||||
|         'react/no-unescaped-entities': 'off', | ||||
|         'react/prop-types': 'off', | ||||
|     }, | ||||
|     settings: { | ||||
|         react: { version: 'detect' }, | ||||
|     }, | ||||
| }; | ||||
							
								
								
									
										118
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						| @@ -1,5 +1,123 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## [1.8.0](https://github.com/chartdb/chartdb/compare/v1.7.0...v1.8.0) (2025-02-13) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * **dbml-import:** add error highlighting for dbml imports ([#556](https://github.com/chartdb/chartdb/issues/556)) ([190e4f4](https://github.com/chartdb/chartdb/commit/190e4f4ffa834fa621f264dc608ca3f3b393a331)) | ||||
| * **docker image:** add support for custom inference servers ([#543](https://github.com/chartdb/chartdb/issues/543)) ([1878083](https://github.com/chartdb/chartdb/commit/1878083056ea4db7a05cdeeb38a4f7b9f5f95bd1)) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * **canvas:** add right-click option to create relationships ([#568](https://github.com/chartdb/chartdb/issues/568)) ([e993f15](https://github.com/chartdb/chartdb/commit/e993f1549c4c86bb9e7e36062db803ba6613b3b3)) | ||||
| * **canvas:** locate table from canvas ([#560](https://github.com/chartdb/chartdb/issues/560)) ([dc404c9](https://github.com/chartdb/chartdb/commit/dc404c9d7ee272c93aac69646bac859829a5234e)) | ||||
| * **docker:** add option to hide popups ([#580](https://github.com/chartdb/chartdb/issues/580)) ([a96c2e1](https://github.com/chartdb/chartdb/commit/a96c2e107838d2dc13b586923fd9dbe06598cdd8)) | ||||
| * **export-sql:** show create script for only filtered schemas ([#570](https://github.com/chartdb/chartdb/issues/570)) ([85fd14f](https://github.com/chartdb/chartdb/commit/85fd14fa02bb2879c36bba53369dbf2e7fa578d4)) | ||||
| * **i18n:** fix Ukrainian ([#554](https://github.com/chartdb/chartdb/issues/554)) ([7b62719](https://github.com/chartdb/chartdb/commit/7b6271962a99bfe5ffbd0176e714c76368ef5c41)) | ||||
| * **import dbml:** add import for indexes ([#566](https://github.com/chartdb/chartdb/issues/566)) ([0db67ea](https://github.com/chartdb/chartdb/commit/0db67ea42a5f9585ca1d246db7a7ff0239bec0ba)) | ||||
| * **import-query:** improve the cleanup for messy json input ([#562](https://github.com/chartdb/chartdb/issues/562)) ([93d59f8](https://github.com/chartdb/chartdb/commit/93d59f8887765098d040a3184aaee32112f67267)) | ||||
| * **index unique:** extract unique toggle for faster editing ([#559](https://github.com/chartdb/chartdb/issues/559)) ([dd4324d](https://github.com/chartdb/chartdb/commit/dd4324d64f7638ada5c022a2ab38bd8e6986af25)) | ||||
| * **mssql-import:** improve script readability by adding edition comment ([#572](https://github.com/chartdb/chartdb/issues/572)) ([be65328](https://github.com/chartdb/chartdb/commit/be65328f24b0361638b9e2edb39eaa9906e77f67)) | ||||
| * **realtionships section:** add the schema to source/target tables ([#561](https://github.com/chartdb/chartdb/issues/561)) ([b9e621b](https://github.com/chartdb/chartdb/commit/b9e621bd680730a0ffbf1054d735bfa418711cae)) | ||||
| * **sqlserver-import:** open ssms guide when max chars ([#565](https://github.com/chartdb/chartdb/issues/565)) ([9c485b3](https://github.com/chartdb/chartdb/commit/9c485b3b01a131bf551c7e95916b0c416f6aa0b5)) | ||||
| * **table actions:** fix size of table actions ([#578](https://github.com/chartdb/chartdb/issues/578)) ([26d95ee](https://github.com/chartdb/chartdb/commit/26d95eed25d86452d9168a9d93a301ba50d934e3)) | ||||
|  | ||||
| ## [1.7.0](https://github.com/chartdb/chartdb/compare/v1.6.1...v1.7.0) (2025-02-03) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * **dbml-editor:** add dbml editor in side pannel ([#534](https://github.com/chartdb/chartdb/issues/534)) ([88be6c1](https://github.com/chartdb/chartdb/commit/88be6c1fd4a7e1f20937e8204c14d8fc1c2665b4)) | ||||
| * **import-dbml:** add import dbml functionality ([#549](https://github.com/chartdb/chartdb/issues/549)) ([b424518](https://github.com/chartdb/chartdb/commit/b424518212290a870fdb7c420a303f65f5901429)) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * **canvas edit:** add option to edit names in canvas ([#536](https://github.com/chartdb/chartdb/issues/536)) ([0dcc9b9](https://github.com/chartdb/chartdb/commit/0dcc9b9568cfe749d44d2e93cb365ba3d3a1e71c)) | ||||
| * **dbml-editor:** add shortcuts to dbml and filter: [#534](https://github.com/chartdb/chartdb/issues/534) ([#535](https://github.com/chartdb/chartdb/issues/535)) ([3b3be08](https://github.com/chartdb/chartdb/commit/3b3be086b1e8d5acf999f8504580d9e2f956f7da)) | ||||
| * **dbml:** add error handling ([#545](https://github.com/chartdb/chartdb/issues/545)) ([fef6d3f](https://github.com/chartdb/chartdb/commit/fef6d3f4996130a3769d1f25b4b1f2090293a1bf)) | ||||
| * **empty-state:** fix dark-mode for empty-state ([#547](https://github.com/chartdb/chartdb/issues/547)) ([99a8201](https://github.com/chartdb/chartdb/commit/99a820139861546a012d7b562ddbb9b77698151a)) | ||||
| * **examples:** fix employee example dbml ([#544](https://github.com/chartdb/chartdb/issues/544)) ([2118bce](https://github.com/chartdb/chartdb/commit/2118bce0f00d55eb19d22b9fa2d4964ba2533a09)) | ||||
| * **i18n:** translation/Ukrainian ([#529](https://github.com/chartdb/chartdb/issues/529)) ([ff3269e](https://github.com/chartdb/chartdb/commit/ff3269ec0510bbae4bc114e65a1ea86a656e8785)) | ||||
| * **open-diagram:** add arrow keys navigation in open diagram dialog ([#537](https://github.com/chartdb/chartdb/issues/537)) ([14f11c2](https://github.com/chartdb/chartdb/commit/14f11c27a7ad5b990131c8495148cabf12835082)) | ||||
| * **performance:** fix bundle size ([#551](https://github.com/chartdb/chartdb/issues/551)) ([4c93326](https://github.com/chartdb/chartdb/commit/4c93326bb6e3eaa143373c500a0c641e95a53fb9)) | ||||
| * **performance:** reduce bundle size ([#553](https://github.com/chartdb/chartdb/issues/553)) ([004d530](https://github.com/chartdb/chartdb/commit/004d530880a50dea6e9786eb9ae63cf592a4d852)) | ||||
| * **performance:** resolve error on startup ([#552](https://github.com/chartdb/chartdb/issues/552)) ([fd2cc9f](https://github.com/chartdb/chartdb/commit/fd2cc9fcfc8f4a9f0bc79def47d89114159392fb)) | ||||
| * **psql-import:** remove typo for import command (psql) ([#546](https://github.com/chartdb/chartdb/issues/546)) ([eb9b41e](https://github.com/chartdb/chartdb/commit/eb9b41e4f656bec1451c45763f4ea5b547aeec5c)) | ||||
| * **scroll:** fix scroll area ([#550](https://github.com/chartdb/chartdb/issues/550)) ([ef3d7a8](https://github.com/chartdb/chartdb/commit/ef3d7a8b67431e923b75bf8287b86bbc8abe723b)) | ||||
|  | ||||
| ## [1.6.1](https://github.com/chartdb/chartdb/compare/v1.6.0...v1.6.1) (2025-01-26) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * change empty state image ([#531](https://github.com/chartdb/chartdb/issues/531)) ([42d4cba](https://github.com/chartdb/chartdb/commit/42d4cbac8ce352e0e4e155d7003bfb85296b897f)) | ||||
| * **chat-type:** remove typo of char datatype in examples ([#530](https://github.com/chartdb/chartdb/issues/530)) ([58231c9](https://github.com/chartdb/chartdb/commit/58231c91393de30ebff817f0ebc57a5c5579f106)) | ||||
| * **empty_state:** customize empty state ([#533](https://github.com/chartdb/chartdb/issues/533)) ([1643e7b](https://github.com/chartdb/chartdb/commit/1643e7bdeb1bbaf081ab064e871d102c87243c0a)) | ||||
| * **Image Export:** importing css rules error while download image ([#524](https://github.com/chartdb/chartdb/issues/524)) ([e9e2736](https://github.com/chartdb/chartdb/commit/e9e2736cb2203702d53df9afc30b8e989a8c9953)) | ||||
| * **shortcuts:** add zoom all shortcut ([#528](https://github.com/chartdb/chartdb/issues/528)) ([7452ca6](https://github.com/chartdb/chartdb/commit/7452ca6965b0332a93b686c397ddf51013e42506)) | ||||
| * **filter-tables:** show clean filter if no-results ([#532](https://github.com/chartdb/chartdb/issues/532)) ([c36cd33](https://github.com/chartdb/chartdb/commit/c36cd33180badaa9b7f9e27c765f19cb03a50ccd)) | ||||
|  | ||||
| ## [1.6.0](https://github.com/chartdb/chartdb/compare/v1.5.1...v1.6.0) (2025-01-02) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * **view-menu:** add toggle for mini map visibility ([#496](https://github.com/chartdb/chartdb/issues/496)) ([#505](https://github.com/chartdb/chartdb/issues/505)) ([8abf2a7](https://github.com/chartdb/chartdb/commit/8abf2a7bfcc36d39e60ac133b0e5e569de1bbc72)) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * add loadDiagramFromData logic to chartdb provider ([#513](https://github.com/chartdb/chartdb/issues/513)) ([ee659ea](https://github.com/chartdb/chartdb/commit/ee659eaa038a94ee13801801e84152df4d79683d)) | ||||
| * **dependency:** upgrade react query to v7 - clean console warnings ([#504](https://github.com/chartdb/chartdb/issues/504)) ([7c5db08](https://github.com/chartdb/chartdb/commit/7c5db0848e49dfdb7e7120f77003d1e37f8d71b0)) | ||||
| * **i18n:** translation/Arabic ([#509](https://github.com/chartdb/chartdb/issues/509)) ([4b43f72](https://github.com/chartdb/chartdb/commit/4b43f720e90e49d5461e68d188e3865000f52497)) | ||||
|  | ||||
| ## [1.5.1](https://github.com/chartdb/chartdb/compare/v1.5.0...v1.5.1) (2024-12-15) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * **export:** fix SQL server field.nullable type to boolean ([#486](https://github.com/chartdb/chartdb/issues/486)) ([a151f56](https://github.com/chartdb/chartdb/commit/a151f56b5d950e0b5cc54363684ada95889024b3)) | ||||
| * **readme:** Update README.md - add CockroachDB ([#482](https://github.com/chartdb/chartdb/issues/482)) ([2b6b733](https://github.com/chartdb/chartdb/commit/2b6b73326155f18d6d56779c0657a3506e2d2cde)) | ||||
|  | ||||
| ## [1.5.0](https://github.com/chartdb/chartdb/compare/v1.4.0...v1.5.0) (2024-12-11) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * **CockroachDB:** Add CockroachDB support ([#472](https://github.com/chartdb/chartdb/issues/472)) ([5409288](https://github.com/chartdb/chartdb/commit/54092883883b135f6ace51d86754b1df76603d30)) | ||||
| * **i18n:** translate share and dialog sections in Indonesian locale files ([#468](https://github.com/chartdb/chartdb/issues/468)) ([3574cec](https://github.com/chartdb/chartdb/commit/3574cecc7c73dcab404b82115d20e1ad0cd26b37)) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * **core:** fix update diagram id ([#477](https://github.com/chartdb/chartdb/issues/477)) ([348f805](https://github.com/chartdb/chartdb/commit/348f80568e0f686ee478147fdc43a5d43b5c1ebb)) | ||||
| * **dialogs:** fix footer position on dialogs ([#470](https://github.com/chartdb/chartdb/issues/470)) ([2309306](https://github.com/chartdb/chartdb/commit/2309306ef590783b00a2489209092107dd9a3788)) | ||||
| * **sql-server import:** nullable should be boolean instead of string ([#480](https://github.com/chartdb/chartdb/issues/480)) ([635fb53](https://github.com/chartdb/chartdb/commit/635fb53c9f7ebd1e5ef4d9274af041edc08f04c3)) | ||||
|  | ||||
| ## [1.4.0](https://github.com/chartdb/chartdb/compare/v1.3.1...v1.4.0) (2024-12-02) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * **add templates:** add six more templates  ([#452](https://github.com/chartdb/chartdb/issues/452)) ([be1b109](https://github.com/chartdb/chartdb/commit/be1b109f23e62df4cc63fa8914c2754f7809cc08)) | ||||
| * **add templates:** add six more templates (django-axes, laravel-activitylog, octobox, pay-rails, pixelfed, polr) ([#460](https://github.com/chartdb/chartdb/issues/460)) ([03772f6](https://github.com/chartdb/chartdb/commit/03772f6b4f99f9c4350356aa0f2a4666f4f1794d)) | ||||
| * **add templates:** add six more templates (reversion, screeenly, staytus, deployer, devise, talk) ([#457](https://github.com/chartdb/chartdb/issues/457)) ([ddeef3b](https://github.com/chartdb/chartdb/commit/ddeef3b134efa893e1c1e15e2f87c27157200e2d)) | ||||
| * **clickhouse:** add ClickHouse support ([#463](https://github.com/chartdb/chartdb/issues/463)) ([807cd22](https://github.com/chartdb/chartdb/commit/807cd22e0c739f339fa07fe1d2f043c5411ae41f)) | ||||
| * **i18n:** Added bangla translations ([#432](https://github.com/chartdb/chartdb/issues/432)) ([885eb71](https://github.com/chartdb/chartdb/commit/885eb719de577c2652fbed1ed287f38fcc98c148)) | ||||
| * **side-panel:** Add functionality of order tables by drag & drop ([#425](https://github.com/chartdb/chartdb/issues/425)) ([a0e966b](https://github.com/chartdb/chartdb/commit/a0e966b64f8070d4595d47b2fb39e8bbf427b794)) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * **clipboard:** defensive for navigator clipboard ([#462](https://github.com/chartdb/chartdb/issues/462)) ([5fc10a7](https://github.com/chartdb/chartdb/commit/5fc10a7e649fc5877bb297b519b1b6a8b81f1323)) | ||||
| * **import-database:** update database type after importing into an existing generic diagra ([#456](https://github.com/chartdb/chartdb/issues/456)) ([a8fe491](https://github.com/chartdb/chartdb/commit/a8fe491c1b5a30d9f4144cefa9111dd3dfd5df1a)) | ||||
| * **Last Saved:** Translate the "last saved" relative date message ([#400](https://github.com/chartdb/chartdb/issues/400)) ([d45677e](https://github.com/chartdb/chartdb/commit/d45677e92d72efc6cea8f865ce46f0be6ec9961f)) | ||||
| * **mariadb-types:** Add uuid data type ([#459](https://github.com/chartdb/chartdb/issues/459)) ([94656ec](https://github.com/chartdb/chartdb/commit/94656ec7a5435c2da262fb3bc6a6d381d554b0c1)) | ||||
| * window type ([#454](https://github.com/chartdb/chartdb/issues/454)) ([9c7d03c](https://github.com/chartdb/chartdb/commit/9c7d03c285ff6f818eef3199c9b7a530d03a1fec)) | ||||
|  | ||||
| ## [1.3.1](https://github.com/chartdb/chartdb/compare/v1.3.0...v1.3.1) (2024-11-26) | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										12
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						| @@ -1,6 +1,9 @@ | ||||
| FROM node:22-alpine AS builder | ||||
|  | ||||
| ARG VITE_OPENAI_API_KEY | ||||
| ARG VITE_OPENAI_API_ENDPOINT | ||||
| ARG VITE_LLM_MODEL_NAME | ||||
| ARG VITE_HIDE_BUCKLE_DOT_DEV | ||||
|  | ||||
| WORKDIR /usr/src/app | ||||
|  | ||||
| @@ -10,9 +13,13 @@ RUN npm ci | ||||
|  | ||||
| COPY . . | ||||
|  | ||||
| RUN echo "VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}" > .env && \ | ||||
|     echo "VITE_OPENAI_API_ENDPOINT=${VITE_OPENAI_API_ENDPOINT}" >> .env && \ | ||||
|     echo "VITE_LLM_MODEL_NAME=${VITE_LLM_MODEL_NAME}" >> .env && \ | ||||
|     echo "VITE_HIDE_BUCKLE_DOT_DEV=${VITE_HIDE_BUCKLE_DOT_DEV}" >> .env  | ||||
|  | ||||
| RUN npm run build | ||||
|  | ||||
| # Use a lightweight web server to serve the production build | ||||
| FROM nginx:stable-alpine AS production | ||||
|  | ||||
| COPY --from=builder /usr/src/app/dist /usr/share/nginx/html | ||||
| @@ -20,7 +27,6 @@ COPY ./default.conf.template /etc/nginx/conf.d/default.conf.template | ||||
| COPY entrypoint.sh /entrypoint.sh | ||||
| RUN chmod +x /entrypoint.sh | ||||
|  | ||||
| # Expose the default port for the Nginx web server | ||||
| EXPOSE 80 | ||||
|  | ||||
| ENTRYPOINT ["/entrypoint.sh"] | ||||
| ENTRYPOINT ["/entrypoint.sh"] | ||||
							
								
								
									
										34
									
								
								README.md
									
									
									
									
									
								
							
							
						
						| @@ -68,6 +68,7 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif | ||||
| -   ✅ SQL Server | ||||
| -   ✅ MariaDB | ||||
| -   ✅ SQLite | ||||
| -   ✅ CockroachDB | ||||
| -   ✅ ClickHouse | ||||
|  | ||||
| ## Getting Started | ||||
| @@ -95,19 +96,44 @@ npm install | ||||
| VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> npm run build | ||||
| ``` | ||||
|  | ||||
| ### Running the Docker Container | ||||
| ### Run the Docker Container | ||||
| ```bash | ||||
| docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 ghcr.io/chartdb/chartdb:latest | ||||
| ``` | ||||
|  | ||||
| #### Build & run Docker image locally | ||||
| #### Build and Run 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 build -t chartdb . | ||||
| docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 chartdb | ||||
| ``` | ||||
|  | ||||
| #### Using Custom Inference Server | ||||
|  | ||||
| ```bash | ||||
| # Build | ||||
| docker build \ | ||||
|   --build-arg VITE_OPENAI_API_ENDPOINT=<YOUR_ENDPOINT> \ | ||||
|   --build-arg VITE_LLM_MODEL_NAME=<YOUR_MODEL_NAME> \ | ||||
|   -t chartdb . | ||||
|  | ||||
| # Run | ||||
| docker run \ | ||||
|   -e OPENAI_API_ENDPOINT=<YOUR_ENDPOINT> \ | ||||
|   -e LLM_MODEL_NAME=<YOUR_MODEL_NAME> \ | ||||
|   -p 8080:80 chartdb | ||||
| ``` | ||||
|  | ||||
| > **Note:** You must configure either Option 1 (OpenAI API key) OR Option 2 (Custom endpoint and model name) for AI capabilities to work. Do not mix the two options. | ||||
|  | ||||
| Open your browser and navigate to `http://localhost:8080`. | ||||
|  | ||||
| Example configuration for a local vLLM server: | ||||
|  | ||||
| ```bash | ||||
| VITE_OPENAI_API_ENDPOINT=http://localhost:8000/v1 | ||||
| VITE_LLM_MODEL_NAME=Qwen/Qwen2.5-32B-Instruct-AWQ | ||||
| ``` | ||||
|  | ||||
| ## Try it on our website | ||||
|  | ||||
| 1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2) | ||||
|   | ||||
| @@ -10,7 +10,12 @@ server { | ||||
|  | ||||
|     location /config.js { | ||||
|         default_type application/javascript; | ||||
|         return 200 "window.env = { OPENAI_API_KEY: \"$OPENAI_API_KEY\" };"; | ||||
|         return 200 "window.env = {  | ||||
|             OPENAI_API_KEY: \"$OPENAI_API_KEY\", | ||||
|             OPENAI_API_ENDPOINT: \"$OPENAI_API_ENDPOINT\", | ||||
|             LLM_MODEL_NAME: \"$LLM_MODEL_NAME\", | ||||
|             HIDE_BUCKLE_DOT_DEV: \"$HIDE_BUCKLE_DOT_DEV\" | ||||
|         };"; | ||||
|     } | ||||
|  | ||||
|     error_page   500 502 503 504  /50x.html; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| #!/bin/sh | ||||
|  | ||||
| # Replace placeholders in nginx.conf | ||||
| envsubst '${OPENAI_API_KEY}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf | ||||
| envsubst '${OPENAI_API_KEY} ${OPENAI_API_ENDPOINT} ${LLM_MODEL_NAME} ${HIDE_BUCKLE_DOT_DEV}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf | ||||
|  | ||||
| # Start Nginx | ||||
| nginx -g "daemon off;" | ||||
|   | ||||
							
								
								
									
										77
									
								
								eslint.config.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,77 @@ | ||||
| import { fixupConfigRules, fixupPluginRules } from '@eslint/compat'; | ||||
| import reactRefresh from 'eslint-plugin-react-refresh'; | ||||
| import cssModules from 'eslint-plugin-css-modules'; | ||||
| import tailwindcss from 'eslint-plugin-tailwindcss'; | ||||
| import jsxA11Y from 'eslint-plugin-jsx-a11y'; | ||||
| import globals from 'globals'; | ||||
| import tsParser from '@typescript-eslint/parser'; | ||||
| import path from 'node:path'; | ||||
| import { fileURLToPath } from 'node:url'; | ||||
| import js from '@eslint/js'; | ||||
| import { FlatCompat } from '@eslint/eslintrc'; | ||||
|  | ||||
| const __filename = fileURLToPath(import.meta.url); | ||||
| const __dirname = path.dirname(__filename); | ||||
| const compat = new FlatCompat({ | ||||
|     baseDirectory: __dirname, | ||||
|     recommendedConfig: js.configs.recommended, | ||||
|     allConfig: js.configs.all, | ||||
| }); | ||||
|  | ||||
| export default [ | ||||
|     { | ||||
|         ignores: ['**/dist', '**/.eslintrc.cjs', '**/tailwind.config.js'], | ||||
|         // files: ['**/*.ts', '**/*.tsx'], | ||||
|     }, | ||||
|     ...fixupConfigRules( | ||||
|         compat.extends( | ||||
|             'eslint:recommended', | ||||
|             'plugin:react/recommended', | ||||
|             'plugin:@typescript-eslint/recommended', | ||||
|             'plugin:react-hooks/recommended', | ||||
|             'plugin:css-modules/recommended', | ||||
|             'plugin:tailwindcss/recommended', | ||||
|             'plugin:prettier/recommended' | ||||
|         ) | ||||
|     ), | ||||
|     { | ||||
|         plugins: { | ||||
|             'react-refresh': reactRefresh, | ||||
|             'css-modules': fixupPluginRules(cssModules), | ||||
|             tailwindcss: fixupPluginRules(tailwindcss), | ||||
|             'jsx-a11y': jsxA11Y, | ||||
|         }, | ||||
|  | ||||
|         languageOptions: { | ||||
|             globals: { | ||||
|                 ...globals.browser, | ||||
|             }, | ||||
|  | ||||
|             parser: tsParser, | ||||
|             // parserOptions: { | ||||
|             //     project: './tsconfig.json', | ||||
|             // }, | ||||
|         }, | ||||
|  | ||||
|         settings: { | ||||
|             react: { | ||||
|                 version: 'detect', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         rules: { | ||||
|             '@typescript-eslint/consistent-type-imports': 'error', | ||||
|  | ||||
|             'react-refresh/only-export-components': [ | ||||
|                 'warn', | ||||
|                 { | ||||
|                     allowConstantExport: true, | ||||
|                 }, | ||||
|             ], | ||||
|  | ||||
|             'react/no-unescaped-entities': 'off', | ||||
|             'react/prop-types': 'off', | ||||
|             '@typescript-eslint/no-empty-object-type': 'off', | ||||
|         }, | ||||
|     }, | ||||
| ]; | ||||
							
								
								
									
										20759
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										23
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,18 +1,19 @@ | ||||
| { | ||||
|     "name": "chartdb", | ||||
|     "private": true, | ||||
|     "version": "1.3.1", | ||||
|     "version": "1.8.0", | ||||
|     "type": "module", | ||||
|     "scripts": { | ||||
|         "dev": "vite", | ||||
|         "build": "npm run lint && tsc -b && vite build", | ||||
|         "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", | ||||
|         "lint": "eslint . --report-unused-disable-directives --max-warnings 0", | ||||
|         "lint:fix": "npm run lint -- --fix", | ||||
|         "preview": "vite preview", | ||||
|         "prepare": "husky" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@ai-sdk/openai": "^0.0.51", | ||||
|         "@dbml/core": "^3.9.5", | ||||
|         "@dnd-kit/sortable": "^8.0.0", | ||||
|         "@monaco-editor/react": "^4.6.0", | ||||
|         "@radix-ui/react-accordion": "^1.2.0", | ||||
| @@ -28,10 +29,10 @@ | ||||
|         "@radix-ui/react-label": "^2.1.0", | ||||
|         "@radix-ui/react-menubar": "^1.1.1", | ||||
|         "@radix-ui/react-popover": "^1.1.1", | ||||
|         "@radix-ui/react-scroll-area": "^1.1.0", | ||||
|         "@radix-ui/react-scroll-area": "1.2.0", | ||||
|         "@radix-ui/react-select": "^2.1.1", | ||||
|         "@radix-ui/react-separator": "^1.1.0", | ||||
|         "@radix-ui/react-slot": "^1.1.0", | ||||
|         "@radix-ui/react-slot": "^1.1.1", | ||||
|         "@radix-ui/react-tabs": "^1.1.0", | ||||
|         "@radix-ui/react-toast": "^1.2.1", | ||||
|         "@radix-ui/react-toggle": "^1.1.0", | ||||
| @@ -60,7 +61,7 @@ | ||||
|         "react-i18next": "^15.0.1", | ||||
|         "react-resizable-panels": "^2.0.22", | ||||
|         "react-responsive": "^10.0.0", | ||||
|         "react-router-dom": "^6.26.0", | ||||
|         "react-router-dom": "^7.1.1", | ||||
|         "react-use": "^17.5.1", | ||||
|         "tailwind-merge": "^2.4.0", | ||||
|         "tailwindcss-animate": "^1.0.7", | ||||
| @@ -69,22 +70,26 @@ | ||||
|         "zod": "^3.23.8" | ||||
|     }, | ||||
|     "devDependencies": { | ||||
|         "@eslint/compat": "^1.2.4", | ||||
|         "@eslint/eslintrc": "^3.2.0", | ||||
|         "@eslint/js": "^9.16.0", | ||||
|         "@types/node": "^22.1.0", | ||||
|         "@types/react": "^18.3.3", | ||||
|         "@types/react-dom": "^18.3.0", | ||||
|         "@typescript-eslint/eslint-plugin": "^7.15.0", | ||||
|         "@typescript-eslint/parser": "^7.15.0", | ||||
|         "@typescript-eslint/eslint-plugin": "^8.18.0", | ||||
|         "@typescript-eslint/parser": "^8.18.0", | ||||
|         "@vitejs/plugin-react": "^4.3.1", | ||||
|         "autoprefixer": "^10.4.20", | ||||
|         "eslint": "^8.57.0", | ||||
|         "eslint": "^9.16.0", | ||||
|         "eslint-config-prettier": "^9.1.0", | ||||
|         "eslint-plugin-css-modules": "^2.12.0", | ||||
|         "eslint-plugin-jsx-a11y": "^6.9.0", | ||||
|         "eslint-plugin-prettier": "^5.2.1", | ||||
|         "eslint-plugin-react": "^7.35.0", | ||||
|         "eslint-plugin-react-hooks": "^4.6.2", | ||||
|         "eslint-plugin-react-hooks": "^5.1.0", | ||||
|         "eslint-plugin-react-refresh": "^0.4.7", | ||||
|         "eslint-plugin-tailwindcss": "^3.17.4", | ||||
|         "globals": "^15.13.0", | ||||
|         "husky": "^9.1.5", | ||||
|         "postcss": "^8.4.40", | ||||
|         "prettier": "^3.3.3", | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								public/buckle-animated.gif
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 404 KiB | 
							
								
								
									
										
											BIN
										
									
								
								public/buckle.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 28 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/clickhouse_logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 12 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/clickhouse_logo_2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 1.3 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/clickhouse_logo_dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 7.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/cockroachdb_logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 8.0 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/cockroachdb_logo_2.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 270 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/cockroachdb_logo_dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 20 KiB | 
| Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 6.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/empty_state_dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 6.1 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/cachet-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 447 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/cachet-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 486 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/canvas-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 346 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/canvas-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 379 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/deployer-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 424 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/deployer-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 497 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/devise-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 207 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/devise-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 231 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/django-axes-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 250 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/django-axes-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 264 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/doorkeeper-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 288 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/doorkeeper-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 319 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/flipper-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 189 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/flipper-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 207 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/laravel-activitylog-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 198 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/laravel-activitylog-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 217 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/octobox-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 352 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/octobox-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 382 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/orchid-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 303 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/orchid-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 340 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/pay-rails-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 352 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/pay-rails-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 371 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/pixelfed-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 593 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/pixelfed-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 687 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/polr-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 246 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/polr-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 278 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/reversion-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 229 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/reversion-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 266 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/screeenly-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 251 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/screeenly-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 266 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/staytus-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 424 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/staytus-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 471 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/taggit-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 169 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/taggit-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 184 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/talk-db-dark.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 229 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/templates/talk-db.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 253 KiB | 
| @@ -3,6 +3,7 @@ import React, { lazy, Suspense, useCallback, useEffect } from 'react'; | ||||
| import { Spinner } from '../spinner/spinner'; | ||||
| import { useTheme } from '@/hooks/use-theme'; | ||||
| import { useMonaco } from '@monaco-editor/react'; | ||||
| import { useToast } from '@/components/toast/use-toast'; | ||||
| import { Button } from '../button/button'; | ||||
| import { Copy, CopyCheck } from 'lucide-react'; | ||||
| import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip'; | ||||
| @@ -11,6 +12,14 @@ import { DarkTheme } from './themes/dark'; | ||||
| import { LightTheme } from './themes/light'; | ||||
| import './config.ts'; | ||||
|  | ||||
| export const Editor = lazy(() => | ||||
|     import('./code-editor').then((module) => ({ | ||||
|         default: module.Editor, | ||||
|     })) | ||||
| ); | ||||
|  | ||||
| type EditorType = typeof Editor; | ||||
|  | ||||
| export interface CodeSnippetProps { | ||||
|     className?: string; | ||||
|     code: string; | ||||
| @@ -18,14 +27,9 @@ export interface CodeSnippetProps { | ||||
|     loading?: boolean; | ||||
|     autoScroll?: boolean; | ||||
|     isComplete?: boolean; | ||||
|     editorProps?: React.ComponentProps<EditorType>; | ||||
| } | ||||
|  | ||||
| export const Editor = lazy(() => | ||||
|     import('./code-editor').then((module) => ({ | ||||
|         default: module.Editor, | ||||
|     })) | ||||
| ); | ||||
|  | ||||
| export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | ||||
|     ({ | ||||
|         className, | ||||
| @@ -34,10 +38,12 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | ||||
|         language = 'sql', | ||||
|         autoScroll = false, | ||||
|         isComplete = true, | ||||
|         editorProps, | ||||
|     }) => { | ||||
|         const { t } = useTranslation(); | ||||
|         const monaco = useMonaco(); | ||||
|         const { effectiveTheme } = useTheme(); | ||||
|         const { toast } = useToast(); | ||||
|         const [isCopied, setIsCopied] = React.useState(false); | ||||
|         const [tooltipOpen, setTooltipOpen] = React.useState(false); | ||||
|  | ||||
| @@ -66,10 +72,32 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | ||||
|             } | ||||
|         }, [code, monaco, autoScroll]); | ||||
|  | ||||
|         const copyToClipboard = useCallback(() => { | ||||
|             navigator.clipboard.writeText(code); | ||||
|             setIsCopied(true); | ||||
|         }, [code]); | ||||
|         const copyToClipboard = useCallback(async () => { | ||||
|             if (!navigator?.clipboard) { | ||||
|                 toast({ | ||||
|                     title: t('copy_to_clipboard_toast.unsupported.title'), | ||||
|                     variant: 'destructive', | ||||
|                     description: t( | ||||
|                         'copy_to_clipboard_toast.unsupported.description' | ||||
|                     ), | ||||
|                 }); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             try { | ||||
|                 await navigator.clipboard.writeText(code); | ||||
|                 setIsCopied(true); | ||||
|             } catch { | ||||
|                 setIsCopied(false); | ||||
|                 toast({ | ||||
|                     title: t('copy_to_clipboard_toast.failed.title'), | ||||
|                     variant: 'destructive', | ||||
|                     description: t( | ||||
|                         'copy_to_clipboard_toast.failed.description' | ||||
|                     ), | ||||
|                 }); | ||||
|             } | ||||
|         }, [code, t, toast]); | ||||
|  | ||||
|         return ( | ||||
|             <div | ||||
| @@ -120,27 +148,32 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | ||||
|                             language={language} | ||||
|                             loading={<Spinner />} | ||||
|                             theme={effectiveTheme} | ||||
|                             {...editorProps} | ||||
|                             options={{ | ||||
|                                 minimap: { | ||||
|                                     enabled: false, | ||||
|                                 }, | ||||
|                                 readOnly: true, | ||||
|                                 automaticLayout: true, | ||||
|                                 scrollbar: { | ||||
|                                     vertical: 'hidden', | ||||
|                                     horizontal: 'hidden', | ||||
|                                     alwaysConsumeMouseWheel: false, | ||||
|                                 }, | ||||
|                                 scrollBeyondLastLine: false, | ||||
|                                 renderValidationDecorations: 'off', | ||||
|                                 lineDecorationsWidth: 0, | ||||
|                                 overviewRulerBorder: false, | ||||
|                                 overviewRulerLanes: 0, | ||||
|                                 hideCursorInOverviewRuler: true, | ||||
|                                 contextmenu: false, | ||||
|                                 ...editorProps?.options, | ||||
|                                 guides: { | ||||
|                                     indentation: false, | ||||
|                                     ...editorProps?.options?.guides, | ||||
|                                 }, | ||||
|                                 scrollbar: { | ||||
|                                     vertical: 'hidden', | ||||
|                                     horizontal: 'hidden', | ||||
|                                     alwaysConsumeMouseWheel: false, | ||||
|                                     ...editorProps?.options?.scrollbar, | ||||
|                                 }, | ||||
|                                 minimap: { | ||||
|                                     enabled: false, | ||||
|                                     ...editorProps?.options?.minimap, | ||||
|                                 }, | ||||
|                                 contextmenu: false, | ||||
|                             }} | ||||
|                         /> | ||||
|                         {!isComplete ? ( | ||||
|   | ||||
							
								
								
									
										54
									
								
								src/components/code-snippet/languages/dbml-language.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,54 @@ | ||||
| import type { Monaco } from '@monaco-editor/react'; | ||||
| import { dataTypes } from '@/lib/data/data-types/data-types'; | ||||
|  | ||||
| export const setupDBMLLanguage = (monaco: Monaco) => { | ||||
|     monaco.languages.register({ id: 'dbml' }); | ||||
|  | ||||
|     // Define themes for DBML | ||||
|     monaco.editor.defineTheme('dbml-dark', { | ||||
|         base: 'vs-dark', | ||||
|         inherit: true, | ||||
|         rules: [ | ||||
|             { token: 'keyword', foreground: '569CD6' }, // Table, Ref keywords | ||||
|             { token: 'string', foreground: 'CE9178' }, // Strings | ||||
|             { token: 'annotation', foreground: '9CDCFE' }, // [annotations] | ||||
|             { token: 'delimiter', foreground: 'D4D4D4' }, // Braces {} | ||||
|             { token: 'operator', foreground: 'D4D4D4' }, // Operators | ||||
|             { token: 'datatype', foreground: '4EC9B0' }, // Data types | ||||
|         ], | ||||
|         colors: {}, | ||||
|     }); | ||||
|  | ||||
|     monaco.editor.defineTheme('dbml-light', { | ||||
|         base: 'vs', | ||||
|         inherit: true, | ||||
|         rules: [ | ||||
|             { token: 'keyword', foreground: '0000FF' }, // Table, Ref keywords | ||||
|             { token: 'string', foreground: 'A31515' }, // Strings | ||||
|             { token: 'annotation', foreground: '001080' }, // [annotations] | ||||
|             { token: 'delimiter', foreground: '000000' }, // Braces {} | ||||
|             { token: 'operator', foreground: '000000' }, // Operators | ||||
|             { token: 'type', foreground: '267F99' }, // Data types | ||||
|         ], | ||||
|         colors: {}, | ||||
|     }); | ||||
|  | ||||
|     const dataTypesNames = dataTypes.map((dt) => dt.name); | ||||
|     const datatypePattern = dataTypesNames.join('|'); | ||||
|  | ||||
|     monaco.languages.setMonarchTokensProvider('dbml', { | ||||
|         keywords: ['Table', 'Ref', 'Indexes'], | ||||
|         datatypes: dataTypesNames, | ||||
|         tokenizer: { | ||||
|             root: [ | ||||
|                 [/\b(Table|Ref|Indexes)\b/, 'keyword'], | ||||
|                 [/\[.*?\]/, 'annotation'], | ||||
|                 [/".*?"/, 'string'], | ||||
|                 [/'.*?'/, 'string'], | ||||
|                 [/[{}]/, 'delimiter'], | ||||
|                 [/[<>]/, 'operator'], | ||||
|                 [new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching | ||||
|             ], | ||||
|         }, | ||||
|     }); | ||||
| }; | ||||
| @@ -1,6 +1,6 @@ | ||||
| import React from 'react'; | ||||
| import type { Diagram } from '@/lib/domain/diagram'; | ||||
| import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip'; | ||||
| import type { DatabaseEdition } from '@/lib/domain/database-edition'; | ||||
| import { | ||||
|     databaseEditionToImageMap, | ||||
|     databaseEditionToLabelMap, | ||||
| @@ -9,39 +9,44 @@ import { | ||||
|     databaseSecondaryLogoMap, | ||||
|     databaseTypeToLabelMap, | ||||
| } from '@/lib/databases'; | ||||
| import type { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import { cn } from '@/lib/utils'; | ||||
|  | ||||
| export interface DiagramIconProps { | ||||
|     diagram: Diagram; | ||||
| export interface DiagramIconProps | ||||
|     extends React.ComponentPropsWithoutRef<'div'> { | ||||
|     databaseType: DatabaseType; | ||||
|     databaseEdition?: DatabaseEdition; | ||||
|     imgClassName?: string; | ||||
| } | ||||
|  | ||||
| export const DiagramIcon = React.forwardRef< | ||||
|     React.ElementRef<typeof TooltipTrigger>, | ||||
|     DiagramIconProps | ||||
| >(({ diagram }, ref) => | ||||
|     diagram.databaseEdition ? ( | ||||
| >(({ databaseType, databaseEdition, className, imgClassName }, ref) => | ||||
|     databaseEdition ? ( | ||||
|         <Tooltip> | ||||
|             <TooltipTrigger className="mr-1" ref={ref}> | ||||
|             <TooltipTrigger className={cn('mr-1', className)} ref={ref} asChild> | ||||
|                 <img | ||||
|                     src={databaseEditionToImageMap[diagram.databaseEdition]} | ||||
|                     className="h-5 max-w-fit rounded-full" | ||||
|                     src={databaseEditionToImageMap[databaseEdition]} | ||||
|                     className={cn('h-5 max-w-fit rounded-full', imgClassName)} | ||||
|                     alt="database" | ||||
|                 /> | ||||
|             </TooltipTrigger> | ||||
|             <TooltipContent> | ||||
|                 {databaseEditionToLabelMap[diagram.databaseEdition]} | ||||
|                 {databaseEditionToLabelMap[databaseEdition]} | ||||
|             </TooltipContent> | ||||
|         </Tooltip> | ||||
|     ) : ( | ||||
|         <Tooltip> | ||||
|             <TooltipTrigger className="mr-2" ref={ref}> | ||||
|             <TooltipTrigger className={cn('mr-2', className)} ref={ref} asChild> | ||||
|                 <img | ||||
|                     src={databaseSecondaryLogoMap[diagram.databaseType]} | ||||
|                     className="h-5 max-w-fit" | ||||
|                     src={databaseSecondaryLogoMap[databaseType]} | ||||
|                     className={cn('h-5 max-w-fit', imgClassName)} | ||||
|                     alt="database" | ||||
|                 /> | ||||
|             </TooltipTrigger> | ||||
|             <TooltipContent> | ||||
|                 {databaseTypeToLabelMap[diagram.databaseType]} | ||||
|                 {databaseTypeToLabelMap[databaseType]} | ||||
|             </TooltipContent> | ||||
|         </Tooltip> | ||||
|     ) | ||||
|   | ||||
| @@ -117,7 +117,10 @@ const DialogInternalContent = React.forwardRef< | ||||
| >(({ className, ...props }, ref) => ( | ||||
|     <ScrollArea | ||||
|         ref={ref} | ||||
|         className={cn('flex max-h-screen flex-col overflow-y-auto', className)} | ||||
|         className={cn( | ||||
|             'flex flex-1 max-h-screen flex-col overflow-y-auto', | ||||
|             className | ||||
|         )} | ||||
|         {...props} | ||||
|     /> | ||||
| )); | ||||
|   | ||||
| @@ -1,30 +1,66 @@ | ||||
| import React, { forwardRef } from 'react'; | ||||
| import EmptyStateImage from '@/assets/empty_state.png'; | ||||
| import EmptyStateImageDark from '@/assets/empty_state_dark.png'; | ||||
| import { Label } from '@/components/label/label'; | ||||
| import { cn } from '@/lib/utils'; | ||||
| import { useTheme } from '@/hooks/use-theme'; | ||||
|  | ||||
| export interface EmptyStateProps { | ||||
|     title: string; | ||||
|     description: string; | ||||
|     imageClassName?: string; | ||||
|     titleClassName?: string; | ||||
|     descriptionClassName?: string; | ||||
| } | ||||
|  | ||||
| export const EmptyState = forwardRef< | ||||
|     HTMLDivElement, | ||||
|     React.HTMLAttributes<HTMLDivElement> & EmptyStateProps | ||||
| >(({ title, description, className }, ref) => ( | ||||
|     <div | ||||
|         ref={ref} | ||||
|         className={cn( | ||||
|             'flex flex-1 flex-col items-center justify-center space-y-1', | ||||
|             className | ||||
|         )} | ||||
|     > | ||||
|         <img src={EmptyStateImage} alt="Empty state" className="w-32" /> | ||||
|         <Label className="text-base">{title}</Label> | ||||
|         <Label className="text-sm font-normal text-muted-foreground"> | ||||
|             {description} | ||||
|         </Label> | ||||
|     </div> | ||||
| )); | ||||
| >( | ||||
|     ( | ||||
|         { | ||||
|             title, | ||||
|             description, | ||||
|             className, | ||||
|             titleClassName, | ||||
|             descriptionClassName, | ||||
|             imageClassName, | ||||
|         }, | ||||
|         ref | ||||
|     ) => { | ||||
|         const { effectiveTheme } = useTheme(); | ||||
|  | ||||
|         return ( | ||||
|             <div | ||||
|                 ref={ref} | ||||
|                 className={cn( | ||||
|                     'flex flex-1 flex-col items-center justify-center space-y-1', | ||||
|                     className | ||||
|                 )} | ||||
|             > | ||||
|                 <img | ||||
|                     src={ | ||||
|                         effectiveTheme === 'dark' | ||||
|                             ? EmptyStateImageDark | ||||
|                             : EmptyStateImage | ||||
|                     } | ||||
|                     alt="Empty state" | ||||
|                     className={cn('mb-2 w-20', imageClassName)} | ||||
|                 /> | ||||
|                 <Label className={cn('text-base', titleClassName)}> | ||||
|                     {title} | ||||
|                 </Label> | ||||
|                 <Label | ||||
|                     className={cn( | ||||
|                         'text-sm font-normal text-muted-foreground', | ||||
|                         descriptionClassName | ||||
|                     )} | ||||
|                 > | ||||
|                     {description} | ||||
|                 </Label> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
| ); | ||||
|  | ||||
| EmptyState.displayName = 'EmptyState'; | ||||
|   | ||||
| @@ -14,6 +14,7 @@ type ToasterToast = ToastProps & { | ||||
|     layout?: 'row' | 'column'; | ||||
| }; | ||||
|  | ||||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
| const actionTypes = { | ||||
|     ADD_TOAST: 'ADD_TOAST', | ||||
|     UPDATE_TOAST: 'UPDATE_TOAST', | ||||
|   | ||||
							
								
								
									
										15
									
								
								src/context/alert-context/alert-context.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | ||||
| import { createContext, useContext } from 'react'; | ||||
| import { emptyFn } from '@/lib/utils'; | ||||
| import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog'; | ||||
|  | ||||
| export interface AlertContext { | ||||
|     showAlert: (params: BaseAlertDialogProps) => void; | ||||
|     closeAlert: () => void; | ||||
| } | ||||
|  | ||||
| export const alertContext = createContext<AlertContext>({ | ||||
|     closeAlert: emptyFn, | ||||
|     showAlert: emptyFn, | ||||
| }); | ||||
|  | ||||
| export const useAlert = () => useContext(alertContext); | ||||
							
								
								
									
										36
									
								
								src/context/alert-context/alert-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,36 @@ | ||||
| import React, { useCallback, useState } from 'react'; | ||||
| import type { AlertContext } from './alert-context'; | ||||
| import { alertContext } from './alert-context'; | ||||
| import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog'; | ||||
| import { BaseAlertDialog } from '@/dialogs/base-alert-dialog/base-alert-dialog'; | ||||
|  | ||||
| export const AlertProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     children, | ||||
| }) => { | ||||
|     const [showAlert, setShowAlert] = useState(false); | ||||
|     const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({ | ||||
|         title: '', | ||||
|     }); | ||||
|     const showAlertHandler: AlertContext['showAlert'] = useCallback( | ||||
|         (params) => { | ||||
|             setAlertParams(params); | ||||
|             setShowAlert(true); | ||||
|         }, | ||||
|         [setShowAlert, setAlertParams] | ||||
|     ); | ||||
|     const closeAlertHandler = useCallback(() => { | ||||
|         setShowAlert(false); | ||||
|     }, [setShowAlert]); | ||||
|  | ||||
|     return ( | ||||
|         <alertContext.Provider | ||||
|             value={{ | ||||
|                 showAlert: showAlertHandler, | ||||
|                 closeAlert: closeAlertHandler, | ||||
|             }} | ||||
|         > | ||||
|             {children} | ||||
|             <BaseAlertDialog dialog={{ open: showAlert }} {...alertParams} /> | ||||
|         </alertContext.Provider> | ||||
|     ); | ||||
| }; | ||||
							
								
								
									
										22
									
								
								src/context/canvas-context/canvas-context.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,22 @@ | ||||
| import { createContext } from 'react'; | ||||
| import { emptyFn } from '@/lib/utils'; | ||||
| import type { Graph } from '@/lib/graph'; | ||||
| import { createGraph } from '@/lib/graph'; | ||||
|  | ||||
| export interface CanvasContext { | ||||
|     reorderTables: (options?: { updateHistory?: boolean }) => void; | ||||
|     fitView: (options?: { | ||||
|         duration?: number; | ||||
|         padding?: number; | ||||
|         maxZoom?: number; | ||||
|     }) => void; | ||||
|     setOverlapGraph: (graph: Graph<string>) => void; | ||||
|     overlapGraph: Graph<string>; | ||||
| } | ||||
|  | ||||
| export const canvasContext = createContext<CanvasContext>({ | ||||
|     reorderTables: emptyFn, | ||||
|     fitView: emptyFn, | ||||
|     setOverlapGraph: emptyFn, | ||||
|     overlapGraph: createGraph(), | ||||
| }); | ||||
							
								
								
									
										85
									
								
								src/context/canvas-context/canvas-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,85 @@ | ||||
| import React, { type ReactNode, useCallback, useState } from 'react'; | ||||
| import { canvasContext } from './canvas-context'; | ||||
| import { useChartDB } from '@/hooks/use-chartdb'; | ||||
| import { | ||||
|     adjustTablePositions, | ||||
|     shouldShowTablesBySchemaFilter, | ||||
| } from '@/lib/domain/db-table'; | ||||
| import { useReactFlow } from '@xyflow/react'; | ||||
| import { findOverlappingTables } from '@/pages/editor-page/canvas/canvas-utils'; | ||||
| import type { Graph } from '@/lib/graph'; | ||||
| import { createGraph } from '@/lib/graph'; | ||||
|  | ||||
| interface CanvasProviderProps { | ||||
|     children: ReactNode; | ||||
| } | ||||
|  | ||||
| export const CanvasProvider = ({ children }: CanvasProviderProps) => { | ||||
|     const { tables, relationships, updateTablesState, filteredSchemas } = | ||||
|         useChartDB(); | ||||
|     const { fitView } = useReactFlow(); | ||||
|     const [overlapGraph, setOverlapGraph] = | ||||
|         useState<Graph<string>>(createGraph()); | ||||
|  | ||||
|     const reorderTables = useCallback( | ||||
|         ( | ||||
|             options: { updateHistory?: boolean } = { | ||||
|                 updateHistory: true, | ||||
|             } | ||||
|         ) => { | ||||
|             const newTables = adjustTablePositions({ | ||||
|                 relationships, | ||||
|                 tables: tables.filter((table) => | ||||
|                     shouldShowTablesBySchemaFilter(table, filteredSchemas) | ||||
|                 ), | ||||
|                 mode: 'all', // Use 'all' mode for manual reordering | ||||
|             }); | ||||
|  | ||||
|             const updatedOverlapGraph = findOverlappingTables({ | ||||
|                 tables: newTables, | ||||
|             }); | ||||
|  | ||||
|             updateTablesState( | ||||
|                 (currentTables) => | ||||
|                     currentTables.map((table) => { | ||||
|                         const newTable = newTables.find( | ||||
|                             (t) => t.id === table.id | ||||
|                         ); | ||||
|                         return { | ||||
|                             id: table.id, | ||||
|                             x: newTable?.x ?? table.x, | ||||
|                             y: newTable?.y ?? table.y, | ||||
|                         }; | ||||
|                     }), | ||||
|                 { | ||||
|                     updateHistory: options.updateHistory ?? true, | ||||
|                     forceOverride: false, | ||||
|                 } | ||||
|             ); | ||||
|  | ||||
|             setOverlapGraph(updatedOverlapGraph); | ||||
|  | ||||
|             setTimeout(() => { | ||||
|                 fitView({ | ||||
|                     duration: 500, | ||||
|                     padding: 0.2, | ||||
|                     maxZoom: 0.8, | ||||
|                 }); | ||||
|             }, 500); | ||||
|         }, | ||||
|         [filteredSchemas, relationships, tables, updateTablesState, fitView] | ||||
|     ); | ||||
|  | ||||
|     return ( | ||||
|         <canvasContext.Provider | ||||
|             value={{ | ||||
|                 reorderTables, | ||||
|                 fitView, | ||||
|                 setOverlapGraph, | ||||
|                 overlapGraph, | ||||
|             }} | ||||
|         > | ||||
|             {children} | ||||
|         </canvasContext.Provider> | ||||
|     ); | ||||
| }; | ||||
| @@ -84,6 +84,7 @@ export interface ChartDBContext { | ||||
|         options?: { updateHistory: boolean } | ||||
|     ) => Promise<void>; | ||||
|     loadDiagram: (diagramId: string) => Promise<Diagram | undefined>; | ||||
|     loadDiagramFromData: (diagram: Diagram) => void; | ||||
|     updateDiagramUpdatedAt: () => Promise<void>; | ||||
|     clearDiagramData: () => Promise<void>; | ||||
|     deleteDiagram: () => Promise<void>; | ||||
| @@ -246,6 +247,7 @@ export const chartDBContext = createContext<ChartDBContext>({ | ||||
|     updateDiagramName: emptyFn, | ||||
|     updateDiagramUpdatedAt: emptyFn, | ||||
|     loadDiagram: emptyFn, | ||||
|     loadDiagramFromData: emptyFn, | ||||
|     clearDiagramData: emptyFn, | ||||
|     deleteDiagram: emptyFn, | ||||
|  | ||||
|   | ||||
| @@ -27,6 +27,7 @@ export interface ChartDBProviderProps { | ||||
|     diagram?: Diagram; | ||||
|     readonly?: boolean; | ||||
| } | ||||
|  | ||||
| export const ChartDBProvider: React.FC< | ||||
|     React.PropsWithChildren<ChartDBProviderProps> | ||||
| > = ({ children, diagram, readonly }) => { | ||||
| @@ -310,6 +311,7 @@ export const ChartDBProvider: React.FC< | ||||
|                 color: randomColor(), | ||||
|                 createdAt: Date.now(), | ||||
|                 isView: false, | ||||
|                 order: tables.length, | ||||
|                 ...attributes, | ||||
|             }; | ||||
|             await addTable(table); | ||||
| @@ -1334,15 +1336,9 @@ export const ChartDBProvider: React.FC< | ||||
|         ] | ||||
|     ); | ||||
|  | ||||
|     const loadDiagram: ChartDBContext['loadDiagram'] = useCallback( | ||||
|         async (diagramId: string) => { | ||||
|             const diagram = await db.getDiagram(diagramId, { | ||||
|                 includeRelationships: true, | ||||
|                 includeTables: true, | ||||
|                 includeDependencies: true, | ||||
|             }); | ||||
|  | ||||
|             if (diagram) { | ||||
|     const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] = | ||||
|         useCallback( | ||||
|             async (diagram) => { | ||||
|                 setDiagramId(diagram.id); | ||||
|                 setDiagramName(diagram.name); | ||||
|                 setDatabaseType(diagram.databaseType); | ||||
| @@ -1354,23 +1350,36 @@ export const ChartDBProvider: React.FC< | ||||
|                 setDiagramUpdatedAt(diagram.updatedAt); | ||||
|  | ||||
|                 events.emit({ action: 'load_diagram', data: { diagram } }); | ||||
|             }, | ||||
|             [ | ||||
|                 setDiagramId, | ||||
|                 setDiagramName, | ||||
|                 setDatabaseType, | ||||
|                 setDatabaseEdition, | ||||
|                 setTables, | ||||
|                 setRelationships, | ||||
|                 setDependencies, | ||||
|                 setDiagramCreatedAt, | ||||
|                 setDiagramUpdatedAt, | ||||
|                 events, | ||||
|             ] | ||||
|         ); | ||||
|  | ||||
|     const loadDiagram: ChartDBContext['loadDiagram'] = useCallback( | ||||
|         async (diagramId: string) => { | ||||
|             const diagram = await db.getDiagram(diagramId, { | ||||
|                 includeRelationships: true, | ||||
|                 includeTables: true, | ||||
|                 includeDependencies: true, | ||||
|             }); | ||||
|  | ||||
|             if (diagram) { | ||||
|                 loadDiagramFromData(diagram); | ||||
|             } | ||||
|  | ||||
|             return diagram; | ||||
|         }, | ||||
|         [ | ||||
|             db, | ||||
|             setDiagramId, | ||||
|             setDiagramName, | ||||
|             setDatabaseType, | ||||
|             setDatabaseEdition, | ||||
|             setTables, | ||||
|             setRelationships, | ||||
|             setDependencies, | ||||
|             setDiagramCreatedAt, | ||||
|             setDiagramUpdatedAt, | ||||
|             events, | ||||
|         ] | ||||
|         [db, loadDiagramFromData] | ||||
|     ); | ||||
|  | ||||
|     return ( | ||||
| @@ -1391,6 +1400,7 @@ export const ChartDBProvider: React.FC< | ||||
|                 updateDiagramId, | ||||
|                 updateDiagramName, | ||||
|                 loadDiagram, | ||||
|                 loadDiagramFromData, | ||||
|                 updateDatabaseType, | ||||
|                 updateDatabaseEdition, | ||||
|                 clearDiagramData, | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { createContext } from 'react'; | ||||
| import { emptyFn } from '@/lib/utils'; | ||||
| import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog'; | ||||
| import type { TableSchemaDialogProps } from '@/dialogs/table-schema-dialog/table-schema-dialog'; | ||||
| import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog'; | ||||
| import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog'; | ||||
| import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog'; | ||||
| import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog'; | ||||
| import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/import-diagram-dialog'; | ||||
| import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog'; | ||||
|  | ||||
| export interface DialogContext { | ||||
|     // Create diagram dialog | ||||
| @@ -21,12 +21,10 @@ export interface DialogContext { | ||||
|     openExportSQLDialog: (params: Omit<ExportSQLDialogProps, 'dialog'>) => void; | ||||
|     closeExportSQLDialog: () => void; | ||||
|  | ||||
|     // Alert dialog | ||||
|     showAlert: (params: BaseAlertDialogProps) => void; | ||||
|     closeAlert: () => void; | ||||
|  | ||||
|     // Create relationship dialog | ||||
|     openCreateRelationshipDialog: () => void; | ||||
|     openCreateRelationshipDialog: ( | ||||
|         params?: Omit<CreateRelationshipDialogProps, 'dialog'> | ||||
|     ) => void; | ||||
|     closeCreateRelationshipDialog: () => void; | ||||
|  | ||||
|     // Import database dialog | ||||
| @@ -45,6 +43,10 @@ export interface DialogContext { | ||||
|     openStarUsDialog: () => void; | ||||
|     closeStarUsDialog: () => void; | ||||
|  | ||||
|     // Buckle dialog | ||||
|     openBuckleDialog: () => void; | ||||
|     closeBuckleDialog: () => void; | ||||
|  | ||||
|     // Export image dialog | ||||
|     openExportImageDialog: ( | ||||
|         params: Omit<ExportImageDialogProps, 'dialog'> | ||||
| @@ -62,6 +64,10 @@ export interface DialogContext { | ||||
|         params: Omit<ImportDiagramDialogProps, 'dialog'> | ||||
|     ) => void; | ||||
|     closeImportDiagramDialog: () => void; | ||||
|  | ||||
|     // Import DBML dialog | ||||
|     openImportDBMLDialog: () => void; | ||||
|     closeImportDBMLDialog: () => void; | ||||
| } | ||||
|  | ||||
| export const dialogContext = createContext<DialogContext>({ | ||||
| @@ -71,8 +77,6 @@ export const dialogContext = createContext<DialogContext>({ | ||||
|     closeOpenDiagramDialog: emptyFn, | ||||
|     openExportSQLDialog: emptyFn, | ||||
|     closeExportSQLDialog: emptyFn, | ||||
|     closeAlert: emptyFn, | ||||
|     showAlert: emptyFn, | ||||
|     closeCreateRelationshipDialog: emptyFn, | ||||
|     openCreateRelationshipDialog: emptyFn, | ||||
|     openImportDatabaseDialog: emptyFn, | ||||
| @@ -87,4 +91,8 @@ export const dialogContext = createContext<DialogContext>({ | ||||
|     closeExportDiagramDialog: emptyFn, | ||||
|     openImportDiagramDialog: emptyFn, | ||||
|     closeImportDiagramDialog: emptyFn, | ||||
|     openBuckleDialog: emptyFn, | ||||
|     closeBuckleDialog: emptyFn, | ||||
|     openImportDBMLDialog: emptyFn, | ||||
|     closeImportDBMLDialog: emptyFn, | ||||
| }); | ||||
|   | ||||
| @@ -6,8 +6,7 @@ import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-di | ||||
| import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog'; | ||||
| import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog'; | ||||
| import { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog'; | ||||
| import { BaseAlertDialog } from '@/dialogs/base-alert-dialog/base-alert-dialog'; | ||||
| import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog'; | ||||
| import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog'; | ||||
| import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog'; | ||||
| import { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog'; | ||||
| @@ -19,6 +18,8 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor | ||||
| import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog'; | ||||
| import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog'; | ||||
| import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog'; | ||||
| import { BuckleDialog } from '@/dialogs/buckle-dialog/buckle-dialog'; | ||||
| import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog'; | ||||
|  | ||||
| export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     children, | ||||
| @@ -28,7 +29,19 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|  | ||||
|     const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] = | ||||
|         useState(false); | ||||
|     const [createRelationshipDialogParams, setCreateRelationshipDialogParams] = | ||||
|         useState<Omit<CreateRelationshipDialogProps, 'dialog'>>(); | ||||
|     const openCreateRelationshipDialogHandler: DialogContext['openCreateRelationshipDialog'] = | ||||
|         useCallback( | ||||
|             (params) => { | ||||
|                 setCreateRelationshipDialogParams(params); | ||||
|                 setOpenCreateRelationshipDialog(true); | ||||
|             }, | ||||
|             [setOpenCreateRelationshipDialog] | ||||
|         ); | ||||
|  | ||||
|     const [openStarUsDialog, setOpenStarUsDialog] = useState(false); | ||||
|     const [openBuckleDialog, setOpenBuckleDialog] = useState(false); | ||||
|  | ||||
|     // Export image dialog | ||||
|     const [openExportImageDialog, setOpenExportImageDialog] = useState(false); | ||||
| @@ -88,7 +101,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|             [setOpenTableSchemaDialog] | ||||
|         ); | ||||
|  | ||||
|     // Export image dialog | ||||
|     // Export diagram dialog | ||||
|     const [openExportDiagramDialog, setOpenExportDiagramDialog] = | ||||
|         useState(false); | ||||
|  | ||||
| @@ -96,21 +109,8 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     const [openImportDiagramDialog, setOpenImportDiagramDialog] = | ||||
|         useState(false); | ||||
|  | ||||
|     // Alert dialog | ||||
|     const [showAlert, setShowAlert] = useState(false); | ||||
|     const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({ | ||||
|         title: '', | ||||
|     }); | ||||
|     const showAlertHandler: DialogContext['showAlert'] = useCallback( | ||||
|         (params) => { | ||||
|             setAlertParams(params); | ||||
|             setShowAlert(true); | ||||
|         }, | ||||
|         [setShowAlert, setAlertParams] | ||||
|     ); | ||||
|     const closeAlertHandler = useCallback(() => { | ||||
|         setShowAlert(false); | ||||
|     }, [setShowAlert]); | ||||
|     // Import DBML dialog | ||||
|     const [openImportDBMLDialog, setOpenImportDBMLDialog] = useState(false); | ||||
|  | ||||
|     return ( | ||||
|         <dialogContext.Provider | ||||
| @@ -121,10 +121,8 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false), | ||||
|                 openExportSQLDialog: openExportSQLDialogHandler, | ||||
|                 closeExportSQLDialog: () => setOpenExportSQLDialog(false), | ||||
|                 showAlert: showAlertHandler, | ||||
|                 closeAlert: closeAlertHandler, | ||||
|                 openCreateRelationshipDialog: () => | ||||
|                     setOpenCreateRelationshipDialog(true), | ||||
|                 openCreateRelationshipDialog: | ||||
|                     openCreateRelationshipDialogHandler, | ||||
|                 closeCreateRelationshipDialog: () => | ||||
|                     setOpenCreateRelationshipDialog(false), | ||||
|                 openImportDatabaseDialog: openImportDatabaseDialogHandler, | ||||
| @@ -134,6 +132,8 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 closeTableSchemaDialog: () => setOpenTableSchemaDialog(false), | ||||
|                 openStarUsDialog: () => setOpenStarUsDialog(true), | ||||
|                 closeStarUsDialog: () => setOpenStarUsDialog(false), | ||||
|                 closeBuckleDialog: () => setOpenBuckleDialog(false), | ||||
|                 openBuckleDialog: () => setOpenBuckleDialog(true), | ||||
|                 closeExportImageDialog: () => setOpenExportImageDialog(false), | ||||
|                 openExportImageDialog: openExportImageDialogHandler, | ||||
|                 openExportDiagramDialog: () => setOpenExportDiagramDialog(true), | ||||
| @@ -142,6 +142,8 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 openImportDiagramDialog: () => setOpenImportDiagramDialog(true), | ||||
|                 closeImportDiagramDialog: () => | ||||
|                     setOpenImportDiagramDialog(false), | ||||
|                 openImportDBMLDialog: () => setOpenImportDBMLDialog(true), | ||||
|                 closeImportDBMLDialog: () => setOpenImportDBMLDialog(false), | ||||
|             }} | ||||
|         > | ||||
|             {children} | ||||
| @@ -151,9 +153,9 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 dialog={{ open: openExportSQLDialog }} | ||||
|                 {...exportSQLDialogParams} | ||||
|             /> | ||||
|             <BaseAlertDialog dialog={{ open: showAlert }} {...alertParams} /> | ||||
|             <CreateRelationshipDialog | ||||
|                 dialog={{ open: openCreateRelationshipDialog }} | ||||
|                 {...createRelationshipDialogParams} | ||||
|             /> | ||||
|             <ImportDatabaseDialog | ||||
|                 dialog={{ open: openImportDatabaseDialog }} | ||||
| @@ -170,6 +172,8 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|             /> | ||||
|             <ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} /> | ||||
|             <ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} /> | ||||
|             <BuckleDialog dialog={{ open: openBuckleDialog }} /> | ||||
|             <ImportDBMLDialog dialog={{ open: openImportDBMLDialog }} /> | ||||
|         </dialogContext.Provider> | ||||
|     ); | ||||
| }; | ||||
|   | ||||
| @@ -166,6 +166,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                         }, | ||||
|                         quality: 1, | ||||
|                         pixelRatio: scale, | ||||
|                         skipFonts: true, | ||||
|                     }); | ||||
|  | ||||
|                     downloadImage(dataUrl, type); | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import { useHistory } from '@/hooks/use-history'; | ||||
| import { useDialog } from '@/hooks/use-dialog'; | ||||
| import { useChartDB } from '@/hooks/use-chartdb'; | ||||
| import { useLayout } from '@/hooks/use-layout'; | ||||
| import { useReactFlow } from '@xyflow/react'; | ||||
|  | ||||
| export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     children, | ||||
| @@ -17,6 +18,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     const { openOpenDiagramDialog } = useDialog(); | ||||
|     const { updateDiagramUpdatedAt } = useChartDB(); | ||||
|     const { toggleSidePanel } = useLayout(); | ||||
|     const { fitView } = useReactFlow(); | ||||
|  | ||||
|     useHotkeys( | ||||
|         keyboardShortcutsForOS[KeyboardShortcutAction.REDO].keyCombination, | ||||
| @@ -61,6 +63,20 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         }, | ||||
|         [toggleSidePanel] | ||||
|     ); | ||||
|     useHotkeys( | ||||
|         keyboardShortcutsForOS[KeyboardShortcutAction.SHOW_ALL].keyCombination, | ||||
|         () => { | ||||
|             fitView({ | ||||
|                 duration: 500, | ||||
|                 padding: 0.1, | ||||
|                 maxZoom: 0.8, | ||||
|             }); | ||||
|         }, | ||||
|         { | ||||
|             preventDefault: true, | ||||
|         }, | ||||
|         [fitView] | ||||
|     ); | ||||
|  | ||||
|     return ( | ||||
|         <keyboardShortcutsContext.Provider value={{}}> | ||||
|   | ||||
| @@ -6,6 +6,7 @@ export enum KeyboardShortcutAction { | ||||
|     OPEN_DIAGRAM = 'open_diagram', | ||||
|     SAVE_DIAGRAM = 'save_diagram', | ||||
|     TOGGLE_SIDE_PANEL = 'toggle_side_panel', | ||||
|     SHOW_ALL = 'show_all', | ||||
| } | ||||
|  | ||||
| export interface KeyboardShortcut { | ||||
| @@ -55,6 +56,13 @@ export const keyboardShortcuts: Record< | ||||
|         keyCombinationMac: 'meta+b', | ||||
|         keyCombinationWin: 'ctrl+b', | ||||
|     }, | ||||
|     [KeyboardShortcutAction.SHOW_ALL]: { | ||||
|         action: KeyboardShortcutAction.SHOW_ALL, | ||||
|         keyCombinationLabelMac: '⌘0', | ||||
|         keyCombinationLabelWin: 'Ctrl+0', | ||||
|         keyCombinationMac: 'meta+0', | ||||
|         keyCombinationWin: 'ctrl+0', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export interface KeyboardShortcutForOS { | ||||
|   | ||||
| @@ -30,8 +30,17 @@ export interface LocalConfigContext { | ||||
|     starUsDialogLastOpen: number; | ||||
|     setStarUsDialogLastOpen: (lastOpen: number) => void; | ||||
|  | ||||
|     buckleWaitlistOpened: boolean; | ||||
|     setBuckleWaitlistOpened: (githubRepoOpened: boolean) => void; | ||||
|  | ||||
|     buckleDialogLastOpen: number; | ||||
|     setBuckleDialogLastOpen: (lastOpen: number) => void; | ||||
|  | ||||
|     showDependenciesOnCanvas: boolean; | ||||
|     setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void; | ||||
|  | ||||
|     showMiniMapOnCanvas: boolean; | ||||
|     setShowMiniMapOnCanvas: (showMiniMapOnCanvas: boolean) => void; | ||||
| } | ||||
|  | ||||
| export const LocalConfigContext = createContext<LocalConfigContext>({ | ||||
| @@ -56,6 +65,15 @@ export const LocalConfigContext = createContext<LocalConfigContext>({ | ||||
|     starUsDialogLastOpen: 0, | ||||
|     setStarUsDialogLastOpen: emptyFn, | ||||
|  | ||||
|     buckleWaitlistOpened: false, | ||||
|     setBuckleWaitlistOpened: emptyFn, | ||||
|  | ||||
|     buckleDialogLastOpen: 0, | ||||
|     setBuckleDialogLastOpen: emptyFn, | ||||
|  | ||||
|     showDependenciesOnCanvas: false, | ||||
|     setShowDependenciesOnCanvas: emptyFn, | ||||
|  | ||||
|     showMiniMapOnCanvas: false, | ||||
|     setShowMiniMapOnCanvas: emptyFn, | ||||
| }); | ||||
|   | ||||
| @@ -10,7 +10,10 @@ const showCardinalityKey = 'show_cardinality'; | ||||
| const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification'; | ||||
| const githubRepoOpenedKey = 'github_repo_opened'; | ||||
| const starUsDialogLastOpenKey = 'star_us_dialog_last_open'; | ||||
| const buckleWaitlistOpenedKey = 'buckle_waitlist_opened'; | ||||
| const buckleDialogLastOpenKey = 'buckle_dialog_last_open'; | ||||
| const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas'; | ||||
| const showMiniMapOnCanvasKey = 'show_minimap_on_canvas'; | ||||
|  | ||||
| export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     children, | ||||
| @@ -48,12 +51,28 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|             parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0') | ||||
|         ); | ||||
|  | ||||
|     const [buckleWaitlistOpened, setBuckleWaitlistOpened] = | ||||
|         React.useState<boolean>( | ||||
|             (localStorage.getItem(buckleWaitlistOpenedKey) || 'false') === | ||||
|                 'true' | ||||
|         ); | ||||
|  | ||||
|     const [buckleDialogLastOpen, setBuckleDialogLastOpen] = | ||||
|         React.useState<number>( | ||||
|             parseInt(localStorage.getItem(buckleDialogLastOpenKey) || '0') | ||||
|         ); | ||||
|  | ||||
|     const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] = | ||||
|         React.useState<boolean>( | ||||
|             (localStorage.getItem(showDependenciesOnCanvasKey) || 'false') === | ||||
|                 'true' | ||||
|         ); | ||||
|  | ||||
|     const [showMiniMapOnCanvas, setShowMiniMapOnCanvas] = | ||||
|         React.useState<boolean>( | ||||
|             (localStorage.getItem(showMiniMapOnCanvasKey) || 'true') === 'true' | ||||
|         ); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem( | ||||
|             starUsDialogLastOpenKey, | ||||
| @@ -65,6 +84,20 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString()); | ||||
|     }, [githubRepoOpened]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem( | ||||
|             buckleDialogLastOpenKey, | ||||
|             buckleDialogLastOpen.toString() | ||||
|         ); | ||||
|     }, [buckleDialogLastOpen]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem( | ||||
|             buckleWaitlistOpenedKey, | ||||
|             buckleWaitlistOpened.toString() | ||||
|         ); | ||||
|     }, [buckleWaitlistOpened]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem( | ||||
|             hideMultiSchemaNotificationKey, | ||||
| @@ -95,6 +128,13 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         ); | ||||
|     }, [showDependenciesOnCanvas]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem( | ||||
|             showMiniMapOnCanvasKey, | ||||
|             showMiniMapOnCanvas.toString() | ||||
|         ); | ||||
|     }, [showMiniMapOnCanvas]); | ||||
|  | ||||
|     return ( | ||||
|         <LocalConfigContext.Provider | ||||
|             value={{ | ||||
| @@ -114,6 +154,12 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 setStarUsDialogLastOpen, | ||||
|                 showDependenciesOnCanvas, | ||||
|                 setShowDependenciesOnCanvas, | ||||
|                 setBuckleDialogLastOpen, | ||||
|                 buckleDialogLastOpen, | ||||
|                 buckleWaitlistOpened, | ||||
|                 setBuckleWaitlistOpened, | ||||
|                 showMiniMapOnCanvas, | ||||
|                 setShowMiniMapOnCanvas, | ||||
|             }} | ||||
|         > | ||||
|             {children} | ||||
|   | ||||
| @@ -122,6 +122,32 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         config: '++id, defaultDiagramId', | ||||
|     }); | ||||
|  | ||||
|     db.version(8).stores({ | ||||
|         diagrams: | ||||
|             '++id, name, databaseType, databaseEdition, createdAt, updatedAt', | ||||
|         db_tables: | ||||
|             '++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment, isView, isMaterializedView, order', | ||||
|         db_relationships: | ||||
|             '++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt', | ||||
|         db_dependencies: | ||||
|             '++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt', | ||||
|         config: '++id, defaultDiagramId', | ||||
|     }); | ||||
|  | ||||
|     db.version(9).upgrade((tx) => | ||||
|         tx | ||||
|             .table<DBTable & { diagramId: string }>('db_tables') | ||||
|             .toCollection() | ||||
|             .modify((table) => { | ||||
|                 for (const field of table.fields) { | ||||
|                     if (typeof field.nullable === 'string') { | ||||
|                         field.nullable = | ||||
|                             (field.nullable as string).toLowerCase() === 'true'; | ||||
|                     } | ||||
|                 } | ||||
|             }) | ||||
|     ); | ||||
|  | ||||
|     db.on('ready', async () => { | ||||
|         const config = await getConfig(); | ||||
|  | ||||
| @@ -270,6 +296,23 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         attributes: Partial<Diagram>; | ||||
|     }) => { | ||||
|         await db.diagrams.update(id, attributes); | ||||
|  | ||||
|         if (attributes.id) { | ||||
|             await Promise.all([ | ||||
|                 db.db_tables | ||||
|                     .where('diagramId') | ||||
|                     .equals(id) | ||||
|                     .modify({ diagramId: attributes.id }), | ||||
|                 db.db_relationships | ||||
|                     .where('diagramId') | ||||
|                     .equals(id) | ||||
|                     .modify({ diagramId: attributes.id }), | ||||
|                 db.db_dependencies | ||||
|                     .where('diagramId') | ||||
|                     .equals(id) | ||||
|                     .modify({ diagramId: attributes.id }), | ||||
|             ]); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const deleteDiagram: StorageContext['deleteDiagram'] = async ( | ||||
| @@ -345,15 +388,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|             .equals(diagramId) | ||||
|             .toArray(); | ||||
|  | ||||
|         // Sort tables first alphabetically, then views alphabetically | ||||
|         return tables.sort((a, b) => { | ||||
|             if (a.isView === b.isView) { | ||||
|                 // Both are either tables or views, so sort alphabetically by name | ||||
|                 return a.name.localeCompare(b.name); | ||||
|             } | ||||
|             // If one is a view and the other is not, put tables first | ||||
|             return a.isView ? 1 : -1; | ||||
|         }); | ||||
|         return tables; | ||||
|     }; | ||||
|  | ||||
|     const addRelationship: StorageContext['addRelationship'] = async ({ | ||||
|   | ||||
| @@ -10,7 +10,7 @@ import { | ||||
|     AlertDialogTitle, | ||||
| } from '@/components/alert-dialog/alert-dialog'; | ||||
| import type { AlertDialogProps } from '@radix-ui/react-alert-dialog'; | ||||
| import { useDialog } from '@/hooks/use-dialog'; | ||||
| import { useAlert } from '@/context/alert-context/alert-context'; | ||||
|  | ||||
| export interface BaseAlertDialogProps { | ||||
|     title: string; | ||||
| @@ -33,7 +33,7 @@ export const BaseAlertDialog: React.FC<BaseAlertDialogProps> = ({ | ||||
|     content, | ||||
|     onClose, | ||||
| }) => { | ||||
|     const { closeAlert } = useDialog(); | ||||
|     const { closeAlert } = useAlert(); | ||||
|  | ||||
|     const closeAlertHandler = useCallback(() => { | ||||
|         onClose?.(); | ||||
|   | ||||
							
								
								
									
										80
									
								
								src/dialogs/buckle-dialog/buckle-dialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,80 @@ | ||||
| import React, { useCallback, useEffect } from 'react'; | ||||
| import { useDialog } from '@/hooks/use-dialog'; | ||||
| import { | ||||
|     Dialog, | ||||
|     DialogClose, | ||||
|     DialogContent, | ||||
|     DialogDescription, | ||||
|     DialogFooter, | ||||
|     DialogHeader, | ||||
|     DialogTitle, | ||||
| } from '@/components/dialog/dialog'; | ||||
| import { Button } from '@/components/button/button'; | ||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||
| import { useLocalConfig } from '@/hooks/use-local-config'; | ||||
| import { useTheme } from '@/hooks/use-theme'; | ||||
|  | ||||
| export interface BuckleDialogProps extends BaseDialogProps {} | ||||
|  | ||||
| export const BuckleDialog: React.FC<BuckleDialogProps> = ({ dialog }) => { | ||||
|     const { setBuckleWaitlistOpened } = useLocalConfig(); | ||||
|     const { effectiveTheme } = useTheme(); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (!dialog.open) return; | ||||
|     }, [dialog.open]); | ||||
|     const { closeBuckleDialog } = useDialog(); | ||||
|  | ||||
|     const handleConfirm = useCallback(() => { | ||||
|         setBuckleWaitlistOpened(true); | ||||
|         window.open('https://waitlist.buckle.dev', '_blank'); | ||||
|     }, [setBuckleWaitlistOpened]); | ||||
|  | ||||
|     return ( | ||||
|         <Dialog | ||||
|             {...dialog} | ||||
|             onOpenChange={(open) => { | ||||
|                 if (!open) { | ||||
|                     closeBuckleDialog(); | ||||
|                 } | ||||
|             }} | ||||
|         > | ||||
|             <DialogContent | ||||
|                 className="flex flex-col" | ||||
|                 showClose={false} | ||||
|                 onInteractOutside={(e) => { | ||||
|                     e.preventDefault(); | ||||
|                 }} | ||||
|             > | ||||
|                 <DialogHeader> | ||||
|                     <DialogTitle className="hidden" /> | ||||
|                     <DialogDescription className="hidden" /> | ||||
|                 </DialogHeader> | ||||
|                 <div className="flex w-full flex-col items-center"> | ||||
|                     <img | ||||
|                         src={ | ||||
|                             effectiveTheme === 'light' | ||||
|                                 ? '/buckle-animated.gif' | ||||
|                                 : '/buckle.png' | ||||
|                         } | ||||
|                         className="h-16" | ||||
|                     /> | ||||
|                     <div className="mt-6 text-center text-base"> | ||||
|                         We've been working on something big -{' '} | ||||
|                         <span className="font-semibold">Ready to explore?</span> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <DialogFooter className="flex gap-1 md:justify-between"> | ||||
|                     <DialogClose asChild> | ||||
|                         <Button variant="secondary">Not now</Button> | ||||
|                     </DialogClose> | ||||
|                     <DialogClose asChild> | ||||
|                         <Button onClick={handleConfirm}> | ||||
|                             Try ChartDB v2.0! | ||||
|                         </Button> | ||||
|                     </DialogClose> | ||||
|                 </DialogFooter> | ||||
|             </DialogContent> | ||||
|         </Dialog> | ||||
|     ); | ||||
| }; | ||||
| @@ -85,6 +85,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|     const [showCheckJsonButton, setShowCheckJsonButton] = useState(false); | ||||
|     const [isCheckingJson, setIsCheckingJson] = useState(false); | ||||
|  | ||||
|     const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         const loadScripts = async () => { | ||||
|             const { importMetadataScripts } = await import( | ||||
| @@ -127,6 +129,11 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|         (e: React.ChangeEvent<HTMLTextAreaElement>) => { | ||||
|             const inputValue = e.target.value; | ||||
|             setScriptResult(inputValue); | ||||
|  | ||||
|             // Automatically open SSMS info when input length is exactly 65535 | ||||
|             if (inputValue.length === 65535) { | ||||
|                 setShowSSMSInfoDialog(true); | ||||
|             } | ||||
|         }, | ||||
|         [setScriptResult] | ||||
|     ); | ||||
| @@ -245,7 +252,10 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|                                 {t('new_diagram_dialog.import_database.step_1')} | ||||
|                             </div> | ||||
|                             {databaseType === DatabaseType.SQL_SERVER && ( | ||||
|                                 <SSMSInfo /> | ||||
|                                 <SSMSInfo | ||||
|                                     open={showSSMSInfoDialog} | ||||
|                                     setOpen={setShowSSMSInfoDialog} | ||||
|                                 /> | ||||
|                             )} | ||||
|                         </div> | ||||
|                         {databaseTypeToClientsMap[databaseType].length > 0 ? ( | ||||
| @@ -369,6 +379,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|         showCheckJsonButton, | ||||
|         isCheckingJson, | ||||
|         handleCheckJson, | ||||
|         showSSMSInfoDialog, | ||||
|         setShowSSMSInfoDialog, | ||||
|     ]); | ||||
|  | ||||
|     const renderFooter = useCallback(() => { | ||||
|   | ||||
| @@ -4,32 +4,55 @@ import { | ||||
|     HoverCardTrigger, | ||||
| } from '@/components/hover-card/hover-card'; | ||||
| import { Label } from '@/components/label/label'; | ||||
| import { Info } from 'lucide-react'; | ||||
| import React from 'react'; | ||||
| import { Info, X } from 'lucide-react'; | ||||
| import React, { useCallback, useEffect, useMemo } from 'react'; | ||||
| import SSMSInstructions from '@/assets/ssms-instructions.png'; | ||||
| import { ZoomableImage } from '@/components/zoomable-image/zoomable-image'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
|  | ||||
| export interface SSMSInfoProps {} | ||||
| export interface SSMSInfoProps { | ||||
|     open?: boolean; | ||||
|     setOpen?: (open: boolean) => void; | ||||
| } | ||||
|  | ||||
| export const SSMSInfo = React.forwardRef< | ||||
|     React.ElementRef<typeof HoverCardTrigger>, | ||||
|     SSMSInfoProps | ||||
| >((props, ref) => { | ||||
| >(({ open: controlledOpen, setOpen: setControlledOpen }, ref) => { | ||||
|     const [open, setOpen] = React.useState(false); | ||||
|     const { t } = useTranslation(); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (controlledOpen) { | ||||
|             setOpen(true); | ||||
|         } | ||||
|     }, [controlledOpen]); | ||||
|  | ||||
|     const closeHandler = useCallback(() => { | ||||
|         setOpen(false); | ||||
|         setControlledOpen?.(false); | ||||
|     }, [setControlledOpen]); | ||||
|  | ||||
|     const isOpen = useMemo( | ||||
|         () => open || controlledOpen, | ||||
|         [open, controlledOpen] | ||||
|     ); | ||||
|  | ||||
|     return ( | ||||
|         <HoverCard | ||||
|             open={open} | ||||
|             open={isOpen} | ||||
|             onOpenChange={(isOpen) => { | ||||
|                 if (controlledOpen) { | ||||
|                     return; | ||||
|                 } | ||||
|                 setOpen(isOpen); | ||||
|             }} | ||||
|         > | ||||
|             <HoverCardTrigger ref={ref} {...props} asChild> | ||||
|             <HoverCardTrigger ref={ref} asChild> | ||||
|                 <div | ||||
|                     className="flex flex-row items-center gap-1 text-pink-600" | ||||
|                     onClick={() => { | ||||
|                         setOpen(!open); | ||||
|                         setOpen?.(!open); | ||||
|                     }} | ||||
|                 > | ||||
|                     <Info size={14} /> | ||||
| @@ -41,13 +64,21 @@ export const SSMSInfo = React.forwardRef< | ||||
|                 </div> | ||||
|             </HoverCardTrigger> | ||||
|             <HoverCardContent className="w-80"> | ||||
|                 <div className="flex"> | ||||
|                     <div className="space-y-1"> | ||||
|                 <div className="flex flex-col"> | ||||
|                     <div className="flex items-start justify-between"> | ||||
|                         <h4 className="text-sm font-semibold"> | ||||
|                             {t( | ||||
|                                 'new_diagram_dialog.import_database.ssms_instructions.title' | ||||
|                             )} | ||||
|                         </h4> | ||||
|                         <button | ||||
|                             onClick={closeHandler} | ||||
|                             className="text-muted-foreground hover:text-foreground" | ||||
|                         > | ||||
|                             <X size={16} /> | ||||
|                         </button> | ||||
|                     </div> | ||||
|                     <div className="space-y-1"> | ||||
|                         <p className="text-xs text-muted-foreground"> | ||||
|                             <span className="font-semibold">1. </span> | ||||
|                             {t( | ||||
|   | ||||
| @@ -8,10 +8,13 @@ export interface ExampleOptionProps {} | ||||
| export const ExampleOption: React.FC<ExampleOptionProps> = () => { | ||||
|     const { t } = useTranslation(); | ||||
|     return ( | ||||
|         <Link href="/examples" className="text-primary hover:text-primary"> | ||||
|             <div className="flex size-20 cursor-pointer flex-col items-center rounded-md border py-3 text-center md:size-32"> | ||||
|                 <div className="flex flex-1 items-center"> | ||||
|                     <LayoutGrid size={34} /> | ||||
|         <Link | ||||
|             href="/examples" | ||||
|             className="col-span-3 text-primary hover:text-primary" | ||||
|         > | ||||
|             <div className="flex h-8 w-full cursor-pointer flex-row items-center justify-center gap-2 rounded-md border py-3 text-center"> | ||||
|                 <div className="flex items-center"> | ||||
|                     <LayoutGrid className="size-4" /> | ||||
|                 </div> | ||||
|                 <div className="flex flex-col-reverse"> | ||||
|                     <div className="hidden text-sm text-primary md:flex"> | ||||
|   | ||||
| @@ -1,22 +1,61 @@ | ||||
| import React from 'react'; | ||||
| import React, { useMemo, useState } from 'react'; | ||||
| import { ToggleGroup } from '@/components/toggle/toggle-group'; | ||||
| import { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import { DatabaseOption } from './database-option'; | ||||
| import { ExampleOption } from './example-option'; | ||||
|  | ||||
| import { Button } from '@/components/button/button'; | ||||
| import { ChevronDown, ChevronUp } from 'lucide-react'; | ||||
| export interface SelectDatabaseContentProps { | ||||
|     databaseType: DatabaseType; | ||||
|     setDatabaseType: React.Dispatch<React.SetStateAction<DatabaseType>>; | ||||
|     onContinue: () => void; | ||||
| } | ||||
|  | ||||
| const ROW_SIZE = 3; | ||||
| const ROWS = 2; | ||||
| const TOTAL_SLOTS = ROW_SIZE * ROWS; | ||||
| const SUPPORTED_DB_TYPES: DatabaseType[] = [ | ||||
|     DatabaseType.MYSQL, | ||||
|     DatabaseType.POSTGRESQL, | ||||
|     DatabaseType.MARIADB, | ||||
|     DatabaseType.SQLITE, | ||||
|     DatabaseType.SQL_SERVER, | ||||
|     DatabaseType.COCKROACHDB, | ||||
|     DatabaseType.CLICKHOUSE, | ||||
| ]; | ||||
|  | ||||
| export const SelectDatabaseContent: React.FC<SelectDatabaseContentProps> = ({ | ||||
|     databaseType, | ||||
|     setDatabaseType, | ||||
|     onContinue, | ||||
| }) => { | ||||
|     const [currentRow, setCurrentRow] = useState(0); | ||||
|     const currentDatabasesTypes = useMemo( | ||||
|         () => | ||||
|             SUPPORTED_DB_TYPES.slice( | ||||
|                 currentRow * ROW_SIZE, | ||||
|                 currentRow * ROW_SIZE + TOTAL_SLOTS | ||||
|             ), | ||||
|         [currentRow] | ||||
|     ); | ||||
|  | ||||
|     const hasNextRow = useMemo( | ||||
|         () => (currentRow + 1) * ROW_SIZE < SUPPORTED_DB_TYPES.length, | ||||
|         [currentRow] | ||||
|     ); | ||||
|  | ||||
|     const hasPreviousRow = useMemo(() => currentRow > 0, [currentRow]); | ||||
|  | ||||
|     const toggleRow = () => { | ||||
|         if (currentRow === 0 && hasNextRow) { | ||||
|             setCurrentRow(currentRow + 1); | ||||
|         } else if (currentRow > 0) { | ||||
|             setCurrentRow(currentRow - 1); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|         <div className="flex flex-1 items-center justify-center"> | ||||
|         <div className="flex flex-1 flex-col items-center justify-center gap-4"> | ||||
|             <ToggleGroup | ||||
|                 value={databaseType} | ||||
|                 onValueChange={(value: DatabaseType) => { | ||||
| @@ -30,12 +69,41 @@ export const SelectDatabaseContent: React.FC<SelectDatabaseContentProps> = ({ | ||||
|                 type="single" | ||||
|                 className="grid grid-flow-row grid-cols-3 gap-6" | ||||
|             > | ||||
|                 <DatabaseOption type={DatabaseType.MYSQL} /> | ||||
|                 <DatabaseOption type={DatabaseType.POSTGRESQL} /> | ||||
|                 <DatabaseOption type={DatabaseType.MARIADB} /> | ||||
|                 <DatabaseOption type={DatabaseType.SQLITE} /> | ||||
|                 <DatabaseOption type={DatabaseType.SQL_SERVER} /> | ||||
|                 <ExampleOption /> | ||||
|                 {Array.from({ length: TOTAL_SLOTS }).map((_, index) => | ||||
|                     currentDatabasesTypes?.[index] ? ( | ||||
|                         <DatabaseOption | ||||
|                             key={currentDatabasesTypes[index]} | ||||
|                             type={currentDatabasesTypes[index]} | ||||
|                         /> | ||||
|                     ) : null | ||||
|                 )} | ||||
|  | ||||
|                 <div className="col-span-3 flex flex-1 flex-col gap-1"> | ||||
|                     {hasNextRow || hasPreviousRow ? ( | ||||
|                         <Button | ||||
|                             variant="ghost" | ||||
|                             onClick={toggleRow} | ||||
|                             className="col-span-3 h-8" | ||||
|                         > | ||||
|                             {currentRow === 0 ? ( | ||||
|                                 <div className="flex h-8 w-full cursor-pointer flex-row items-center justify-center gap-2 py-3 text-center md:h-12"> | ||||
|                                     <ChevronDown className="mr-2 size-3.5" /> | ||||
|                                     <span className="text-xs"> | ||||
|                                         More Databases | ||||
|                                     </span> | ||||
|                                 </div> | ||||
|                             ) : ( | ||||
|                                 <div className="flex h-8 w-full cursor-pointer flex-row items-center justify-center gap-2 py-3 text-center md:h-12"> | ||||
|                                     <ChevronUp className="mr-2 size-3.5" /> | ||||
|                                     <span className="text-xs"> | ||||
|                                         Primary Databases | ||||
|                                     </span> | ||||
|                                 </div> | ||||
|                             )} | ||||
|                         </Button> | ||||
|                     ) : null} | ||||
|                     <ExampleOption /> | ||||
|                 </div> | ||||
|             </ToggleGroup> | ||||
|         </div> | ||||
|     ); | ||||
|   | ||||
| @@ -22,13 +22,17 @@ import { areFieldTypesCompatible } from '@/lib/data/data-types/data-types'; | ||||
| const ErrorMessageRelationshipFieldsNotSameType = | ||||
|     'Relationships can only be created between fields of the same type'; | ||||
|  | ||||
| export interface CreateRelationshipDialogProps extends BaseDialogProps {} | ||||
| export interface CreateRelationshipDialogProps extends BaseDialogProps { | ||||
|     sourceTableId?: string; | ||||
| } | ||||
|  | ||||
| export const CreateRelationshipDialog: React.FC< | ||||
|     CreateRelationshipDialogProps | ||||
| > = ({ dialog }) => { | ||||
| > = ({ dialog, sourceTableId: preSelectedSourceTableId }) => { | ||||
|     const { closeCreateRelationshipDialog } = useDialog(); | ||||
|     const [primaryTableId, setPrimaryTableId] = useState<string | undefined>(); | ||||
|     const [primaryTableId, setPrimaryTableId] = useState<string | undefined>( | ||||
|         preSelectedSourceTableId | ||||
|     ); | ||||
|     const [primaryFieldId, setPrimaryFieldId] = useState<string | undefined>(); | ||||
|     const [referencedTableId, setReferencedTableId] = useState< | ||||
|         string | undefined | ||||
| @@ -43,6 +47,9 @@ export const CreateRelationshipDialog: React.FC< | ||||
|     const [canCreateRelationship, setCanCreateRelationship] = useState(false); | ||||
|     const { fitView, setEdges } = useReactFlow(); | ||||
|     const { databaseType } = useChartDB(); | ||||
|     const [primaryFieldSelectOpen, setPrimaryFieldSelectOpen] = useState(false); | ||||
|     const [referencedTableSelectOpen, setReferencedTableSelectOpen] = | ||||
|         useState(false); | ||||
|  | ||||
|     const tableOptions = useMemo(() => { | ||||
|         return tables.map( | ||||
| @@ -89,8 +96,23 @@ export const CreateRelationshipDialog: React.FC< | ||||
|         setReferencedTableId(undefined); | ||||
|         setReferencedFieldId(undefined); | ||||
|         setErrorMessage(''); | ||||
|         setPrimaryFieldSelectOpen(false); | ||||
|         setReferencedTableSelectOpen(false); | ||||
|     }, [dialog.open]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (preSelectedSourceTableId) { | ||||
|             const table = getTable(preSelectedSourceTableId); | ||||
|             if (table) { | ||||
|                 setPrimaryTableId(preSelectedSourceTableId); | ||||
|             } | ||||
|  | ||||
|             setTimeout(() => { | ||||
|                 setPrimaryFieldSelectOpen(true); | ||||
|             }, 100); | ||||
|         } | ||||
|     }, [preSelectedSourceTableId, getTable]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         setCanCreateRelationship(false); | ||||
|         setErrorMessage(''); | ||||
| @@ -223,8 +245,14 @@ export const CreateRelationshipDialog: React.FC< | ||||
|                                     )} | ||||
|                                     value={primaryTableId} | ||||
|                                     onChange={(value) => { | ||||
|                                         setPrimaryTableId(value as string); | ||||
|                                         setPrimaryFieldId(undefined); | ||||
|                                         const newTableId = value as string; | ||||
|                                         setPrimaryTableId(newTableId); | ||||
|                                         if ( | ||||
|                                             newTableId !== | ||||
|                                             preSelectedSourceTableId | ||||
|                                         ) { | ||||
|                                             setPrimaryFieldId(undefined); | ||||
|                                         } | ||||
|                                     }} | ||||
|                                     emptyPlaceholder={t( | ||||
|                                         'create_relationship_dialog.no_tables_found' | ||||
| @@ -253,6 +281,8 @@ export const CreateRelationshipDialog: React.FC< | ||||
|                                             'create_relationship_dialog.primary_field_placeholder' | ||||
|                                         )} | ||||
|                                         value={primaryFieldId} | ||||
|                                         open={primaryFieldSelectOpen} | ||||
|                                         onOpenChange={setPrimaryFieldSelectOpen} | ||||
|                                         onChange={(value) => | ||||
|                                             setPrimaryFieldId(value as string) | ||||
|                                         } | ||||
| @@ -283,6 +313,8 @@ export const CreateRelationshipDialog: React.FC< | ||||
|                                         'create_relationship_dialog.referenced_table_placeholder' | ||||
|                                     )} | ||||
|                                     value={referencedTableId} | ||||
|                                     open={referencedTableSelectOpen} | ||||
|                                     onOpenChange={setReferencedTableSelectOpen} | ||||
|                                     onChange={(value) => { | ||||
|                                         setReferencedTableId(value as string); | ||||
|                                         setReferencedFieldId(undefined); | ||||
|   | ||||
| @@ -20,10 +20,12 @@ import { | ||||
| } from '@/lib/data/export-metadata/export-sql-script'; | ||||
| import { databaseTypeToLabelMap } from '@/lib/databases'; | ||||
| import { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table'; | ||||
| import { Annoyed, Sparkles } from 'lucide-react'; | ||||
| import React, { useCallback, useEffect, useRef } from 'react'; | ||||
| import { Trans, useTranslation } from 'react-i18next'; | ||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||
| import type { Diagram } from '@/lib/domain/diagram'; | ||||
|  | ||||
| export interface ExportSQLDialogProps extends BaseDialogProps { | ||||
|     targetDatabaseType: DatabaseType; | ||||
| @@ -34,7 +36,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | ||||
|     targetDatabaseType, | ||||
| }) => { | ||||
|     const { closeExportSQLDialog } = useDialog(); | ||||
|     const { currentDiagram } = useChartDB(); | ||||
|     const { currentDiagram, filteredSchemas } = useChartDB(); | ||||
|     const { t } = useTranslation(); | ||||
|     const [script, setScript] = React.useState<string>(); | ||||
|     const [error, setError] = React.useState<boolean>(false); | ||||
| @@ -43,17 +45,58 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | ||||
|     const abortControllerRef = useRef<AbortController | null>(null); | ||||
|  | ||||
|     const exportSQLScript = useCallback(async () => { | ||||
|         const filteredDiagram: Diagram = { | ||||
|             ...currentDiagram, | ||||
|             tables: currentDiagram.tables?.filter((table) => | ||||
|                 shouldShowTablesBySchemaFilter(table, filteredSchemas) | ||||
|             ), | ||||
|             relationships: currentDiagram.relationships?.filter((rel) => { | ||||
|                 const sourceTable = currentDiagram.tables?.find( | ||||
|                     (t) => t.id === rel.sourceTableId | ||||
|                 ); | ||||
|                 const targetTable = currentDiagram.tables?.find( | ||||
|                     (t) => t.id === rel.targetTableId | ||||
|                 ); | ||||
|                 return ( | ||||
|                     sourceTable && | ||||
|                     targetTable && | ||||
|                     shouldShowTablesBySchemaFilter( | ||||
|                         sourceTable, | ||||
|                         filteredSchemas | ||||
|                     ) && | ||||
|                     shouldShowTablesBySchemaFilter(targetTable, filteredSchemas) | ||||
|                 ); | ||||
|             }), | ||||
|             dependencies: currentDiagram.dependencies?.filter((dep) => { | ||||
|                 const table = currentDiagram.tables?.find( | ||||
|                     (t) => t.id === dep.tableId | ||||
|                 ); | ||||
|                 const dependentTable = currentDiagram.tables?.find( | ||||
|                     (t) => t.id === dep.dependentTableId | ||||
|                 ); | ||||
|                 return ( | ||||
|                     table && | ||||
|                     dependentTable && | ||||
|                     shouldShowTablesBySchemaFilter(table, filteredSchemas) && | ||||
|                     shouldShowTablesBySchemaFilter( | ||||
|                         dependentTable, | ||||
|                         filteredSchemas | ||||
|                     ) | ||||
|                 ); | ||||
|             }), | ||||
|         }; | ||||
|  | ||||
|         if (targetDatabaseType === DatabaseType.GENERIC) { | ||||
|             return Promise.resolve(exportBaseSQL(currentDiagram)); | ||||
|             return Promise.resolve(exportBaseSQL(filteredDiagram)); | ||||
|         } else { | ||||
|             return exportSQL(currentDiagram, targetDatabaseType, { | ||||
|             return exportSQL(filteredDiagram, targetDatabaseType, { | ||||
|                 stream: true, | ||||
|                 onResultStream: (text) => | ||||
|                     setScript((prev) => (prev ? prev + text : text)), | ||||
|                 signal: abortControllerRef.current?.signal, | ||||
|             }); | ||||
|         } | ||||
|     }, [targetDatabaseType, currentDiagram]); | ||||
|     }, [targetDatabaseType, currentDiagram, filteredSchemas]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (!dialog.open) { | ||||
| @@ -70,7 +113,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | ||||
|                 const script = await exportSQLScript(); | ||||
|                 setScript(script); | ||||
|                 setIsScriptLoading(false); | ||||
|             } catch (e) { | ||||
|             } catch { | ||||
|                 setError(true); | ||||
|             } | ||||
|         }; | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { Dialog, DialogContent } from '@/components/dialog/dialog'; | ||||
| import { useDialog } from '@/hooks/use-dialog'; | ||||
| import type { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import React, { useCallback, useEffect, useState } from 'react'; | ||||
| import { ImportDatabase } from '../common/import-database/import-database'; | ||||
| import type { DatabaseEdition } from '@/lib/domain/database-edition'; | ||||
| @@ -12,6 +12,7 @@ import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack'; | ||||
| import { Trans, useTranslation } from 'react-i18next'; | ||||
| import { useReactFlow } from '@xyflow/react'; | ||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||
| import { useAlert } from '@/context/alert-context/alert-context'; | ||||
|  | ||||
| export interface ImportDatabaseDialogProps extends BaseDialogProps { | ||||
|     databaseType: DatabaseType; | ||||
| @@ -21,7 +22,8 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({ | ||||
|     dialog, | ||||
|     databaseType, | ||||
| }) => { | ||||
|     const { closeImportDatabaseDialog, showAlert } = useDialog(); | ||||
|     const { closeImportDatabaseDialog } = useDialog(); | ||||
|     const { showAlert } = useAlert(); | ||||
|     const { | ||||
|         tables, | ||||
|         relationships, | ||||
| @@ -30,6 +32,8 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({ | ||||
|         addTables, | ||||
|         addRelationships, | ||||
|         diagramName, | ||||
|         databaseType: currentDatabaseType, | ||||
|         updateDatabaseType, | ||||
|     } = useChartDB(); | ||||
|     const [scriptResult, setScriptResult] = useState(''); | ||||
|     const { resetRedoStack, resetUndoStack } = useRedoUndoStack(); | ||||
| @@ -282,6 +286,10 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({ | ||||
|             }), | ||||
|         ]); | ||||
|  | ||||
|         if (currentDatabaseType === DatabaseType.GENERIC) { | ||||
|             await updateDatabaseType(databaseType); | ||||
|         } | ||||
|  | ||||
|         setNodes((nodes) => | ||||
|             nodes.map((node) => ({ | ||||
|                 ...node, | ||||
| @@ -297,6 +305,8 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({ | ||||
|         closeImportDatabaseDialog(); | ||||
|     }, [ | ||||
|         databaseEdition, | ||||
|         currentDatabaseType, | ||||
|         updateDatabaseType, | ||||
|         databaseType, | ||||
|         scriptResult, | ||||
|         tables, | ||||
|   | ||||
							
								
								
									
										399
									
								
								src/dialogs/import-dbml-dialog/import-dbml-dialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,399 @@ | ||||
| import React, { | ||||
|     useCallback, | ||||
|     useEffect, | ||||
|     useState, | ||||
|     Suspense, | ||||
|     useRef, | ||||
| } from 'react'; | ||||
| import * as monaco from 'monaco-editor'; | ||||
| import { useDialog } from '@/hooks/use-dialog'; | ||||
| import { | ||||
|     Dialog, | ||||
|     DialogClose, | ||||
|     DialogContent, | ||||
|     DialogDescription, | ||||
|     DialogFooter, | ||||
|     DialogHeader, | ||||
|     DialogInternalContent, | ||||
|     DialogTitle, | ||||
| } from '@/components/dialog/dialog'; | ||||
| import { Button } from '@/components/button/button'; | ||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { Editor } from '@/components/code-snippet/code-snippet'; | ||||
| import { useTheme } from '@/hooks/use-theme'; | ||||
| import { AlertCircle } from 'lucide-react'; | ||||
| import { importDBMLToDiagram } from '@/lib/dbml-import'; | ||||
| import { useChartDB } from '@/hooks/use-chartdb'; | ||||
| import { Parser } from '@dbml/core'; | ||||
| import { useCanvas } from '@/hooks/use-canvas'; | ||||
| import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language'; | ||||
| import { useToast } from '@/components/toast/use-toast'; | ||||
| import { Spinner } from '@/components/spinner/spinner'; | ||||
| import { debounce } from '@/lib/utils'; | ||||
|  | ||||
| interface DBMLError { | ||||
|     message: string; | ||||
|     line: number; | ||||
|     column: number; | ||||
| } | ||||
|  | ||||
| function parseDBMLError(error: unknown): DBMLError | null { | ||||
|     try { | ||||
|         if (typeof error === 'string') { | ||||
|             const parsed = JSON.parse(error); | ||||
|             if (parsed.diags?.[0]) { | ||||
|                 const diag = parsed.diags[0]; | ||||
|                 return { | ||||
|                     message: diag.message, | ||||
|                     line: diag.location.start.line, | ||||
|                     column: diag.location.start.column, | ||||
|                 }; | ||||
|             } | ||||
|         } else if (error && typeof error === 'object' && 'diags' in error) { | ||||
|             const parsed = error as { | ||||
|                 diags: Array<{ | ||||
|                     message: string; | ||||
|                     location: { start: { line: number; column: number } }; | ||||
|                 }>; | ||||
|             }; | ||||
|             if (parsed.diags?.[0]) { | ||||
|                 return { | ||||
|                     message: parsed.diags[0].message, | ||||
|                     line: parsed.diags[0].location.start.line, | ||||
|                     column: parsed.diags[0].location.start.column, | ||||
|                 }; | ||||
|             } | ||||
|         } | ||||
|     } catch (e) { | ||||
|         console.error('Error parsing DBML error:', e); | ||||
|     } | ||||
|     return null; | ||||
| } | ||||
|  | ||||
| export interface ImportDBMLDialogProps extends BaseDialogProps {} | ||||
|  | ||||
| export const ImportDBMLDialog: React.FC<ImportDBMLDialogProps> = ({ | ||||
|     dialog, | ||||
| }) => { | ||||
|     const { t } = useTranslation(); | ||||
|     const initialDBML = `// Use DBML to define your database structure | ||||
| // Simple Blog System with Comments Example | ||||
|  | ||||
| Table users { | ||||
|   id integer [primary key] | ||||
|   name varchar | ||||
|   email varchar | ||||
| } | ||||
|  | ||||
| Table posts { | ||||
|   id integer [primary key] | ||||
|   title varchar | ||||
|   content text | ||||
|   user_id integer | ||||
|   created_at timestamp | ||||
| } | ||||
|  | ||||
| Table comments { | ||||
|   id integer [primary key] | ||||
|   content text | ||||
|   post_id integer | ||||
|   user_id integer | ||||
|   created_at timestamp | ||||
| } | ||||
|  | ||||
| // Relationships | ||||
| Ref: posts.user_id > users.id // Each post belongs to one user | ||||
| Ref: comments.post_id > posts.id // Each comment belongs to one post | ||||
| Ref: comments.user_id > users.id // Each comment is written by one user`; | ||||
|  | ||||
|     const [dbmlContent, setDBMLContent] = useState<string>(initialDBML); | ||||
|     const { closeImportDBMLDialog } = useDialog(); | ||||
|     const [errorMessage, setErrorMessage] = useState<string | undefined>(); | ||||
|     const { effectiveTheme } = useTheme(); | ||||
|     const { toast } = useToast(); | ||||
|     const { | ||||
|         addTables, | ||||
|         addRelationships, | ||||
|         tables, | ||||
|         relationships, | ||||
|         removeTables, | ||||
|         removeRelationships, | ||||
|     } = useChartDB(); | ||||
|     const { reorderTables } = useCanvas(); | ||||
|     const [reorder, setReorder] = useState(false); | ||||
|     const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>(); | ||||
|     const decorationsCollection = | ||||
|         useRef<monaco.editor.IEditorDecorationsCollection>(); | ||||
|  | ||||
|     const handleEditorDidMount = ( | ||||
|         editor: monaco.editor.IStandaloneCodeEditor | ||||
|     ) => { | ||||
|         editorRef.current = editor; | ||||
|         decorationsCollection.current = editor.createDecorationsCollection(); | ||||
|     }; | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (reorder) { | ||||
|             reorderTables({ | ||||
|                 updateHistory: false, | ||||
|             }); | ||||
|             setReorder(false); | ||||
|         } | ||||
|     }, [reorder, reorderTables]); | ||||
|  | ||||
|     const highlightErrorLine = useCallback((error: DBMLError) => { | ||||
|         if (!editorRef.current) return; | ||||
|  | ||||
|         const model = editorRef.current.getModel(); | ||||
|         if (!model) return; | ||||
|  | ||||
|         const decorations = [ | ||||
|             { | ||||
|                 range: new monaco.Range( | ||||
|                     error.line, | ||||
|                     1, | ||||
|                     error.line, | ||||
|                     model.getLineMaxColumn(error.line) | ||||
|                 ), | ||||
|                 options: { | ||||
|                     isWholeLine: true, | ||||
|                     className: 'dbml-error-line', | ||||
|                     glyphMarginClassName: 'dbml-error-glyph', | ||||
|                     hoverMessage: { value: error.message }, | ||||
|                     overviewRuler: { | ||||
|                         color: '#ff0000', | ||||
|                         position: monaco.editor.OverviewRulerLane.Right, | ||||
|                         darkColor: '#ff0000', | ||||
|                     }, | ||||
|                 }, | ||||
|             }, | ||||
|         ]; | ||||
|  | ||||
|         decorationsCollection.current?.set(decorations); | ||||
|     }, []); | ||||
|  | ||||
|     const clearDecorations = useCallback(() => { | ||||
|         decorationsCollection.current?.clear(); | ||||
|     }, []); | ||||
|  | ||||
|     const validateDBML = useCallback( | ||||
|         async (content: string) => { | ||||
|             // Clear previous errors | ||||
|             setErrorMessage(undefined); | ||||
|             clearDecorations(); | ||||
|  | ||||
|             if (!content.trim()) return; | ||||
|  | ||||
|             try { | ||||
|                 const parser = new Parser(); | ||||
|                 parser.parse(content, 'dbml'); | ||||
|             } catch (e) { | ||||
|                 const parsedError = parseDBMLError(e); | ||||
|                 if (parsedError) { | ||||
|                     setErrorMessage( | ||||
|                         t('import_dbml_dialog.error.description') + | ||||
|                             ` (1 error found - in line ${parsedError.line})` | ||||
|                     ); | ||||
|                     highlightErrorLine(parsedError); | ||||
|                 } else { | ||||
|                     setErrorMessage( | ||||
|                         e instanceof Error ? e.message : JSON.stringify(e) | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         [clearDecorations, highlightErrorLine, t] | ||||
|     ); | ||||
|  | ||||
|     const debouncedValidateRef = useRef<((value: string) => void) | null>(null); | ||||
|  | ||||
|     // Set up debounced validation | ||||
|     useEffect(() => { | ||||
|         debouncedValidateRef.current = debounce((value: string) => { | ||||
|             validateDBML(value); | ||||
|         }, 500); | ||||
|  | ||||
|         return () => { | ||||
|             debouncedValidateRef.current = null; | ||||
|         }; | ||||
|     }, [validateDBML]); | ||||
|  | ||||
|     // Trigger validation when content changes | ||||
|     useEffect(() => { | ||||
|         if (debouncedValidateRef.current) { | ||||
|             debouncedValidateRef.current(dbmlContent); | ||||
|         } | ||||
|     }, [dbmlContent]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (!dialog.open) { | ||||
|             setErrorMessage(undefined); | ||||
|             clearDecorations(); | ||||
|             setDBMLContent(initialDBML); | ||||
|         } | ||||
|     }, [dialog.open, initialDBML, clearDecorations]); | ||||
|  | ||||
|     const handleImport = useCallback(async () => { | ||||
|         if (!dbmlContent.trim() || errorMessage) return; | ||||
|  | ||||
|         try { | ||||
|             const importedDiagram = await importDBMLToDiagram(dbmlContent); | ||||
|             const tableIdsToRemove = tables | ||||
|                 .filter((table) => | ||||
|                     importedDiagram.tables?.some( | ||||
|                         (t) => | ||||
|                             t.name === table.name && t.schema === table.schema | ||||
|                     ) | ||||
|                 ) | ||||
|                 .map((table) => table.id); | ||||
|             // Find relationships that need to be removed | ||||
|             const relationshipIdsToRemove = relationships | ||||
|                 .filter((relationship) => { | ||||
|                     const sourceTable = tables.find( | ||||
|                         (table) => table.id === relationship.sourceTableId | ||||
|                     ); | ||||
|                     const targetTable = tables.find( | ||||
|                         (table) => table.id === relationship.targetTableId | ||||
|                     ); | ||||
|                     if (!sourceTable || !targetTable) return true; | ||||
|                     const replacementSourceTable = importedDiagram.tables?.find( | ||||
|                         (table) => | ||||
|                             table.name === sourceTable.name && | ||||
|                             table.schema === sourceTable.schema | ||||
|                     ); | ||||
|                     const replacementTargetTable = importedDiagram.tables?.find( | ||||
|                         (table) => | ||||
|                             table.name === targetTable.name && | ||||
|                             table.schema === targetTable.schema | ||||
|                     ); | ||||
|                     return replacementSourceTable || replacementTargetTable; | ||||
|                 }) | ||||
|                 .map((relationship) => relationship.id); | ||||
|  | ||||
|             // Remove existing items | ||||
|             await Promise.all([ | ||||
|                 removeTables(tableIdsToRemove, { updateHistory: false }), | ||||
|                 removeRelationships(relationshipIdsToRemove, { | ||||
|                     updateHistory: false, | ||||
|                 }), | ||||
|             ]); | ||||
|  | ||||
|             // Add new items | ||||
|             await Promise.all([ | ||||
|                 addTables(importedDiagram.tables ?? [], { | ||||
|                     updateHistory: false, | ||||
|                 }), | ||||
|                 addRelationships(importedDiagram.relationships ?? [], { | ||||
|                     updateHistory: false, | ||||
|                 }), | ||||
|             ]); | ||||
|             setReorder(true); | ||||
|             closeImportDBMLDialog(); | ||||
|         } catch (e) { | ||||
|             toast({ | ||||
|                 title: t('import_dbml_dialog.error.title'), | ||||
|                 variant: 'destructive', | ||||
|                 description: ( | ||||
|                     <> | ||||
|                         <div>{t('import_dbml_dialog.error.description')}</div> | ||||
|                         {e instanceof Error ? e.message : JSON.stringify(e)} | ||||
|                     </> | ||||
|                 ), | ||||
|             }); | ||||
|         } | ||||
|     }, [ | ||||
|         dbmlContent, | ||||
|         closeImportDBMLDialog, | ||||
|         tables, | ||||
|         relationships, | ||||
|         removeTables, | ||||
|         removeRelationships, | ||||
|         addTables, | ||||
|         addRelationships, | ||||
|         errorMessage, | ||||
|         toast, | ||||
|         setReorder, | ||||
|         t, | ||||
|     ]); | ||||
|  | ||||
|     return ( | ||||
|         <Dialog | ||||
|             {...dialog} | ||||
|             onOpenChange={(open) => { | ||||
|                 if (!open) { | ||||
|                     closeImportDBMLDialog(); | ||||
|                 } | ||||
|             }} | ||||
|         > | ||||
|             <DialogContent | ||||
|                 className="flex h-[80vh] max-h-screen flex-col" | ||||
|                 showClose | ||||
|             > | ||||
|                 <DialogHeader> | ||||
|                     <DialogTitle>{t('import_dbml_dialog.title')}</DialogTitle> | ||||
|                     <DialogDescription> | ||||
|                         {t('import_dbml_dialog.description')} | ||||
|                     </DialogDescription> | ||||
|                 </DialogHeader> | ||||
|                 <DialogInternalContent> | ||||
|                     <Suspense fallback={<Spinner />}> | ||||
|                         <Editor | ||||
|                             value={dbmlContent} | ||||
|                             onChange={(value) => setDBMLContent(value || '')} | ||||
|                             language="dbml" | ||||
|                             onMount={handleEditorDidMount} | ||||
|                             theme={ | ||||
|                                 effectiveTheme === 'dark' | ||||
|                                     ? 'dbml-dark' | ||||
|                                     : 'dbml-light' | ||||
|                             } | ||||
|                             beforeMount={setupDBMLLanguage} | ||||
|                             options={{ | ||||
|                                 minimap: { enabled: false }, | ||||
|                                 scrollBeyondLastLine: false, | ||||
|                                 automaticLayout: true, | ||||
|                                 glyphMargin: true, | ||||
|                                 lineNumbers: 'on', | ||||
|                                 scrollbar: { | ||||
|                                     vertical: 'visible', | ||||
|                                     horizontal: 'visible', | ||||
|                                 }, | ||||
|                             }} | ||||
|                             className="size-full" | ||||
|                         /> | ||||
|                     </Suspense> | ||||
|                 </DialogInternalContent> | ||||
|                 <DialogFooter> | ||||
|                     <div className="flex w-full items-center justify-between"> | ||||
|                         <div className="flex items-center gap-4"> | ||||
|                             <DialogClose asChild> | ||||
|                                 <Button variant="secondary"> | ||||
|                                     {t('import_dbml_dialog.cancel')} | ||||
|                                 </Button> | ||||
|                             </DialogClose> | ||||
|                             {errorMessage ? ( | ||||
|                                 <div className="flex items-center gap-1"> | ||||
|                                     <AlertCircle className="size-4 text-destructive" /> | ||||
|  | ||||
|                                     <span className="text-xs text-destructive"> | ||||
|                                         {errorMessage || | ||||
|                                             t( | ||||
|                                                 'import_dbml_dialog.error.description' | ||||
|                                             )} | ||||
|                                     </span> | ||||
|                                 </div> | ||||
|                             ) : null} | ||||
|                         </div> | ||||
|                         <Button | ||||
|                             onClick={handleImport} | ||||
|                             disabled={!dbmlContent.trim() || !!errorMessage} | ||||
|                         > | ||||
|                             {t('import_dbml_dialog.import')} | ||||
|                         </Button> | ||||
|                     </div> | ||||
|                 </DialogFooter> | ||||
|             </DialogContent> | ||||
|         </Dialog> | ||||
|     ); | ||||
| }; | ||||
| @@ -22,10 +22,11 @@ import { useConfig } from '@/hooks/use-config'; | ||||
| import { useDialog } from '@/hooks/use-dialog'; | ||||
| import { useStorage } from '@/hooks/use-storage'; | ||||
| import type { Diagram } from '@/lib/domain/diagram'; | ||||
| import React, { useEffect, useState } from 'react'; | ||||
| import React, { useCallback, useEffect, useState } from 'react'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||
| import { useDebounce } from '@/hooks/use-debounce'; | ||||
|  | ||||
| export interface OpenDiagramDialogProps extends BaseDialogProps {} | ||||
|  | ||||
| @@ -58,12 +59,65 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | ||||
|         fetchDiagrams(); | ||||
|     }, [listDiagrams, setDiagrams, dialog.open]); | ||||
|  | ||||
|     const openDiagram = (diagramId: string) => { | ||||
|         if (diagramId) { | ||||
|             updateConfig({ defaultDiagramId: diagramId }); | ||||
|             navigate(`/diagrams/${diagramId}`); | ||||
|         } | ||||
|     }; | ||||
|     const openDiagram = useCallback( | ||||
|         (diagramId: string) => { | ||||
|             if (diagramId) { | ||||
|                 updateConfig({ defaultDiagramId: diagramId }); | ||||
|                 navigate(`/diagrams/${diagramId}`); | ||||
|             } | ||||
|         }, | ||||
|         [updateConfig, navigate] | ||||
|     ); | ||||
|  | ||||
|     const handleRowKeyDown = useCallback( | ||||
|         (e: React.KeyboardEvent<HTMLTableRowElement>) => { | ||||
|             const element = e.target as HTMLElement; | ||||
|             const diagramId = element.getAttribute('data-diagram-id'); | ||||
|             const selectionIndexAttr = element.getAttribute( | ||||
|                 'data-selection-index' | ||||
|             ); | ||||
|  | ||||
|             if (!diagramId || !selectionIndexAttr) return; | ||||
|  | ||||
|             const selectionIndex = parseInt(selectionIndexAttr, 10); | ||||
|  | ||||
|             switch (e.key) { | ||||
|                 case 'Enter': | ||||
|                 case ' ': | ||||
|                     e.preventDefault(); | ||||
|                     openDiagram(diagramId); | ||||
|                     closeOpenDiagramDialog(); | ||||
|                     break; | ||||
|                 case 'ArrowDown': { | ||||
|                     e.preventDefault(); | ||||
|  | ||||
|                     ( | ||||
|                         document.querySelector( | ||||
|                             `[data-selection-index="${selectionIndex + 1}"]` | ||||
|                         ) as HTMLElement | ||||
|                     )?.focus(); | ||||
|                     break; | ||||
|                 } | ||||
|                 case 'ArrowUp': { | ||||
|                     e.preventDefault(); | ||||
|  | ||||
|                     ( | ||||
|                         document.querySelector( | ||||
|                             `[data-selection-index="${selectionIndex - 1}"]` | ||||
|                         ) as HTMLElement | ||||
|                     )?.focus(); | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         [openDiagram, closeOpenDiagramDialog] | ||||
|     ); | ||||
|  | ||||
|     const onFocusHandler = useDebounce( | ||||
|         (diagramId: string) => setSelectedDiagramId(diagramId), | ||||
|         50 | ||||
|     ); | ||||
|  | ||||
|     return ( | ||||
|         <Dialog | ||||
|             {...dialog} | ||||
| @@ -74,7 +128,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | ||||
|             }} | ||||
|         > | ||||
|             <DialogContent | ||||
|                 className="flex h-[30rem] max-h-screen w-[90vw] flex-col overflow-y-auto md:w-screen xl:min-w-[55vw]" | ||||
|                 className="flex h-[30rem] max-h-screen flex-col overflow-y-auto md:min-w-[80vw] xl:min-w-[55vw]" | ||||
|                 showClose | ||||
|             > | ||||
|                 <DialogHeader> | ||||
| @@ -112,10 +166,17 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | ||||
|                                 </TableRow> | ||||
|                             </TableHeader> | ||||
|                             <TableBody> | ||||
|                                 {diagrams.map((diagram) => ( | ||||
|                                 {diagrams.map((diagram, index) => ( | ||||
|                                     <TableRow | ||||
|                                         key={diagram.id} | ||||
|                                         data-state={`${selectedDiagramId === diagram.id ? 'selected' : ''}`} | ||||
|                                         data-diagram-id={diagram.id} | ||||
|                                         data-selection-index={index} | ||||
|                                         tabIndex={0} | ||||
|                                         onFocus={() => | ||||
|                                             onFocusHandler(diagram.id) | ||||
|                                         } | ||||
|                                         className="focus:bg-accent focus:outline-none" | ||||
|                                         onClick={(e) => { | ||||
|                                             switch (e.detail) { | ||||
|                                                 case 1: | ||||
| @@ -133,11 +194,17 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | ||||
|                                                     ); | ||||
|                                             } | ||||
|                                         }} | ||||
|                                         onKeyDown={handleRowKeyDown} | ||||
|                                     > | ||||
|                                         <TableCell className="table-cell"> | ||||
|                                             <div className="flex justify-center"> | ||||
|                                                 <DiagramIcon | ||||
|                                                     diagram={diagram} | ||||
|                                                     databaseType={ | ||||
|                                                         diagram.databaseType | ||||
|                                                     } | ||||
|                                                     databaseEdition={ | ||||
|                                                         diagram.databaseEdition | ||||
|                                                     } | ||||
|                                                 /> | ||||
|                                             </div> | ||||
|                                         </TableCell> | ||||
|   | ||||
| @@ -73,3 +73,68 @@ | ||||
|         @apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] rounded-md cursor-pointer; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .gradient-background { | ||||
|     /* Fallback: Set a background color. */ | ||||
|     background-color: #f46b24; | ||||
|  | ||||
|     /* Create the gradient. */ | ||||
|     background-image: linear-gradient( | ||||
|         45deg, | ||||
|         #2e6579 20%, | ||||
|         #4fafca 20%, | ||||
|         #4fafca 40%, | ||||
|         #6dc630 40%, | ||||
|         #6dc630 60%, | ||||
|         #f9dc3a 60%, | ||||
|         #f9dc3a 80%, | ||||
|         #f46b24 80% | ||||
|     ); | ||||
|  | ||||
|     /* Set the background size and repeat properties. */ | ||||
|     background-size: 100%; | ||||
|     background-repeat: repeat; | ||||
|  | ||||
|     /* Use the text as a mask for the background. */ | ||||
|     /* This will show the gradient as a text color rather than element bg. */ | ||||
|     /* -webkit-background-clip: text; | ||||
|     -webkit-text-fill-color: transparent; */ | ||||
|  | ||||
|     /* Animate the text when loading the element. */ | ||||
|     /* This animates it on page load and when hovering out. */ | ||||
|     animation: rainbow-text-simple-animation-rev 0.75s ease forwards; | ||||
| } | ||||
|  | ||||
| .gradient-background:hover { | ||||
|     animation: rainbow-text-simple-animation 0.5s ease-in forwards; | ||||
| } | ||||
|  | ||||
| .dbml-error-line { | ||||
|     background-color: rgba(255, 0, 0, 0.2) !important; | ||||
| } | ||||
|  | ||||
| @keyframes rainbow-text-simple-animation-rev { | ||||
|     0% { | ||||
|         background-size: 650%; | ||||
|     } | ||||
|     40% { | ||||
|         background-size: 650%; | ||||
|     } | ||||
|     100% { | ||||
|         background-size: 100%; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* Move the background and make it larger. */ | ||||
| /* Animation shown when hovering over the text. */ | ||||
| @keyframes rainbow-text-simple-animation { | ||||
|     0% { | ||||
|         background-size: 100%; | ||||
|     } | ||||
|     80% { | ||||
|         background-size: 650%; | ||||
|     } | ||||
|     100% { | ||||
|         background-size: 650%; | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										4
									
								
								src/hooks/use-canvas.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,4 @@ | ||||
| import { useContext } from 'react'; | ||||
| import { canvasContext } from '@/context/canvas-context/canvas-context'; | ||||
|  | ||||
| export const useCanvas = () => useContext(canvasContext); | ||||
							
								
								
									
										21
									
								
								src/hooks/use-debounce.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,21 @@ | ||||
| import { useCallback, useRef } from 'react'; | ||||
|  | ||||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||
| type AnyFunction = (...args: any[]) => any; | ||||
|  | ||||
| export const useDebounce = <T extends AnyFunction>( | ||||
|     func: T, | ||||
|     delay: number | ||||
| ): ((...args: Parameters<T>) => void) => { | ||||
|     const inDebounce = useRef<NodeJS.Timeout>(); | ||||
|  | ||||
|     const debounce = useCallback( | ||||
|         (...args: Parameters<T>) => { | ||||
|             clearTimeout(inDebounce.current); | ||||
|             inDebounce.current = setTimeout(() => func(...args), delay); | ||||
|         }, | ||||
|         [func, delay] | ||||
|     ); | ||||
|  | ||||
|     return debounce; | ||||
| }; | ||||
| @@ -19,8 +19,10 @@ 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 { bn, bnMetadata } from './locales/bn'; | ||||
| import { gu, guMetadata } from './locales/gu'; | ||||
| import { vi, viMetadata } from './locales/vi'; | ||||
| import { ar, arMetadata } from './locales/ar'; | ||||
|  | ||||
| export const languages: LanguageMetadata[] = [ | ||||
|     enMetadata, | ||||
| @@ -40,8 +42,10 @@ export const languages: LanguageMetadata[] = [ | ||||
|     trMetadata, | ||||
|     id_IDMetadata, | ||||
|     teMetadata, | ||||
|     bnMetadata, | ||||
|     guMetadata, | ||||
|     viMetadata, | ||||
|     arMetadata, | ||||
| ]; | ||||
|  | ||||
| const resources = { | ||||
| @@ -62,8 +66,10 @@ const resources = { | ||||
|     tr, | ||||
|     id_ID, | ||||
|     te, | ||||
|     bn, | ||||
|     gu, | ||||
|     vi, | ||||
|     ar, | ||||
| }; | ||||
|  | ||||
| i18n.use(LanguageDetector) | ||||
|   | ||||
							
								
								
									
										424
									
								
								src/i18n/locales/ar.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,424 @@ | ||||
| import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const ar: LanguageTranslation = { | ||||
|     translation: { | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'ملف', | ||||
|                 new: 'جديد', | ||||
|                 open: 'فتح', | ||||
|                 save: 'حفظ', | ||||
|                 import: 'استيراد قاعدة بيانات', | ||||
|                 export_sql: 'SQL تصدير', | ||||
|                 export_as: 'تصدير كـ', | ||||
|                 delete_diagram: 'حذف الرسم البياني', | ||||
|                 exit: 'خروج', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'تحرير', | ||||
|                 undo: 'تراجع', | ||||
|                 redo: 'إعادة', | ||||
|                 clear: 'مسح', | ||||
|             }, | ||||
|             view: { | ||||
|                 view: 'عرض', | ||||
|                 show_sidebar: 'إظهار الشريط الجانبي', | ||||
|                 hide_sidebar: 'إخفاء الشريط الجانبي', | ||||
|                 hide_cardinality: 'إخفاء الكاردينالية', | ||||
|                 show_cardinality: 'إظهار الكاردينالية', | ||||
|                 zoom_on_scroll: 'تكبير/تصغير عند التمرير', | ||||
|                 theme: 'المظهر', | ||||
|                 show_dependencies: 'إظهار الاعتمادات', | ||||
|                 hide_dependencies: 'إخفاء الاعتمادات', | ||||
|                 // TODO: Translate | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             share: { | ||||
|                 share: 'مشاركة', | ||||
|                 export_diagram: 'تصدير المخطط', | ||||
|                 import_diagram: 'استيراد المخطط', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'مساعدة', | ||||
|                 visit_website: 'ChartDB قم بزيارة', | ||||
|                 join_discord: 'Discord انضم إلينا على', | ||||
|                 schedule_a_call: '!تحدث معنا', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         delete_diagram_alert: { | ||||
|             title: 'حذف المخطط', | ||||
|             description: | ||||
|                 '.لا يمكن التراجع عن هذا الإجراء. سيتم حذف الرسم البياني بشكل دائم', | ||||
|             cancel: 'إلغاء', | ||||
|             delete: 'حذف', | ||||
|         }, | ||||
|  | ||||
|         clear_diagram_alert: { | ||||
|             title: 'مسح الرسم البياني', | ||||
|             description: | ||||
|                 '.لا يمكن التراجع عن هذا الاجراء. سيتم حذف جميع البيانات في الرسم البياني بشكل دائم', | ||||
|             cancel: 'إلغاء', | ||||
|             clear: 'مسح', | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'إعادة ترتيب الرسم البياني', | ||||
|             description: | ||||
|                 'هذا الإجراء سيقوم بإعادة ترتيب الجداول في المخطط بشكل تلقائي. هل تريد المتابعة؟', | ||||
|             reorder: 'إعادة ترتيب', | ||||
|             cancel: 'إلغاء', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'مخططات متعددة', | ||||
|             description: | ||||
|                 '{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك', | ||||
|             dont_show_again: 'لا تظهره مجدداً', | ||||
|             change_schema: 'تغيير', | ||||
|             none: 'لا شيء', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'فشل النسخ', | ||||
|                 description: '.الحافظة غير مدعومة', | ||||
|             }, | ||||
|             failed: { | ||||
|                 title: 'فشل النسخ', | ||||
|                 description: 'حدث خطأ أثناء النسخ. حاول مجدداً', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         theme: { | ||||
|             system: 'النظام', | ||||
|             light: 'فاتح', | ||||
|             dark: 'داكن', | ||||
|         }, | ||||
|  | ||||
|         zoom: { | ||||
|             on: 'تشغيل', | ||||
|             off: 'إيقاف', | ||||
|         }, | ||||
|  | ||||
|         last_saved: 'آخر حفظ', | ||||
|         saved: 'تم الحفظ', | ||||
|         loading_diagram: '...جارِ تحميل الرسم البياني', | ||||
|         deselect_all: 'إلغاء تحديد الكل', | ||||
|         select_all: 'تحديد الكل', | ||||
|         clear: 'مسح', | ||||
|         show_more: 'عرض المزيد', | ||||
|         show_less: 'عرض أقل', | ||||
|         copy_to_clipboard: 'نسخ إلى الحافظة', | ||||
|         copied: '!تم النسخ', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: ':المخطط', | ||||
|             filter_by_schema: 'تصفية حسب المخطط', | ||||
|             search_schema: '...بحث في المخطط', | ||||
|             no_schemas_found: '.لم يتم العثور على مخططات', | ||||
|             view_all_options: '...عرض جميع الخيارات', | ||||
|             tables_section: { | ||||
|                 tables: 'الجداول', | ||||
|                 add_table: 'إضافة جدول', | ||||
|                 filter: 'تصفية', | ||||
|                 collapse: 'طي الكل', | ||||
|                 // TODO: Translate | ||||
|                 clear: 'Clear Filter', | ||||
|                 no_results: 'No tables found matching your filter.', | ||||
|                 // TODO: Translate | ||||
|                 show_list: 'Show Table List', | ||||
|                 show_dbml: 'Show DBML Editor', | ||||
|  | ||||
|                 table: { | ||||
|                     fields: 'الحقول', | ||||
|                     nullable: 'يمكن ان يكون فارغاً؟', | ||||
|                     primary_key: 'المفتاح الأساسي', | ||||
|                     indexes: 'الفهارس', | ||||
|                     comments: 'تعليقات', | ||||
|                     no_comments: 'لا توجد تعليقات', | ||||
|                     add_field: 'إضافة حقل', | ||||
|                     add_index: 'إضافة فهرس', | ||||
|                     index_select_fields: 'حدد الحقول', | ||||
|                     no_types_found: 'لا يوجد أنواع', | ||||
|                     field_name: 'الإسم', | ||||
|                     field_type: 'النوع', | ||||
|                     field_actions: { | ||||
|                         title: 'خصائص الحقل', | ||||
|                         unique: 'فريد', | ||||
|                         comments: 'تعليقات', | ||||
|                         no_comments: 'لا يوجد تعليقات', | ||||
|                         delete_field: 'حذف الحقل', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'خصائص الفهرس', | ||||
|                         name: 'الإسم', | ||||
|                         unique: 'فريد', | ||||
|                         delete_index: 'حذف الفهرس', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
|                         title: 'إجراءات الجدول', | ||||
|                         change_schema: 'تغيير المخطط', | ||||
|                         add_field: 'إضافة حقل', | ||||
|                         add_index: 'إضافة فهرس', | ||||
|                         duplicate_table: 'نسخ الجدول', | ||||
|                         delete_table: 'حذف الجدول', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'لا توجد جداول', | ||||
|                     description: 'أنشئ جدولاً للبدء', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'العلاقات', | ||||
|                 filter: 'تصفية', | ||||
|                 add_relationship: 'إضافة علاقة', | ||||
|                 collapse: 'طي الكل', | ||||
|                 relationship: { | ||||
|                     primary: 'الجدول الأساسي', | ||||
|                     foreign: 'الجدول المرتبط', | ||||
|                     cardinality: 'الكاردينالية', | ||||
|                     delete_relationship: 'حذف', | ||||
|                     relationship_actions: { | ||||
|                         title: 'إجراءات', | ||||
|                         delete_relationship: 'حذف', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'لا توجد علاقات', | ||||
|                     description: 'إنشئ علاقة لربط الجداول', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'الاعتمادات', | ||||
|                 filter: 'تصفية', | ||||
|                 collapse: 'طي الكل', | ||||
|                 dependency: { | ||||
|                     table: 'الجدول', | ||||
|                     dependent_table: 'عرض الاعتمادات', | ||||
|                     delete_dependency: 'حذف', | ||||
|                     dependency_actions: { | ||||
|                         title: 'إجراءات', | ||||
|                         delete_dependency: 'حذف', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'لا توجد اعتمادات', | ||||
|                     description: 'إنشاء اعتماد للبدء', | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         toolbar: { | ||||
|             zoom_in: 'تكبير', | ||||
|             zoom_out: 'تصغير', | ||||
|             save: 'حفظ', | ||||
|             show_all: 'عرض الكل', | ||||
|             undo: 'تراجع', | ||||
|             redo: 'إعادة', | ||||
|             reorder_diagram: 'إعادة ترتيب الرسم البياني', | ||||
|             highlight_overlapping_tables: 'تمييز الجداول المتداخلة', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
|             database_selection: { | ||||
|                 title: 'ما هو نوع قاعدة البيانات الخاصة بك؟', | ||||
|                 description: | ||||
|                     'تتمتع كل قاعدة بيانات بمميزاتها وقدراتها الفريدة.', | ||||
|                 check_examples_long: 'ألقي نظرة على الأمثلة', | ||||
|                 check_examples_short: 'أمثلة', | ||||
|             }, | ||||
|  | ||||
|             import_database: { | ||||
|                 title: 'إسترد قاعدة بياناتك', | ||||
|                 database_edition: ':إصدار قاعدة البيانات', | ||||
|                 step_1: ':قم بتشغيل هذا البرنامج النصي في قاعدة بياناتك', | ||||
|                 step_2: ':إلصق نتيجة البرنامج النصي هنا', | ||||
|                 script_results_placeholder: '...نتيجة البرنامج النصي هنا', | ||||
|                 ssms_instructions: { | ||||
|                     button_text: 'SSMS تعليمات', | ||||
|                     title: 'تعليمات', | ||||
|                     step_1: 'SQL SERVER < انتقل إلى الأدوات > الخيارات > نتائح الاستعلام', | ||||
|                     step_2: '(اضبطها على 9999999) XML اذا كنت تستخدم "نتائج إلى الشبكة"، قم بتغيير الحد الاقصى للاحرف المستردة للبيانات غير', | ||||
|                 }, | ||||
|                 instructions_link: 'تحتاج مساعدة؟ شاهد الفيديو', | ||||
|                 check_script_result: 'تحقق من نتيجة البرنامج النصي', | ||||
|             }, | ||||
|  | ||||
|             cancel: 'إلغاء', | ||||
|             import_from_file: 'استيراد من ملف', | ||||
|             back: 'رجوع', | ||||
|             empty_diagram: 'مخطط فارغ', | ||||
|             continue: 'متابعة', | ||||
|             import: 'استيراد', | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'فتح مخطط', | ||||
|             description: 'اختر مخططًا لفتحه من القائمة ادناه', | ||||
|             table_columns: { | ||||
|                 name: 'الإسم', | ||||
|                 created_at: 'تاريخ الإنشاء', | ||||
|                 last_modified: 'آخر تعديل', | ||||
|                 tables_count: 'الجداول', | ||||
|             }, | ||||
|             cancel: 'إلغاء', | ||||
|             open: 'فتح', | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
|             title: 'SQL تصدير', | ||||
|             description: | ||||
|                 '{{databaseType}} صدّر مخطط الرسم البياني إلى برنامج نصي لـ', | ||||
|             close: 'إغلاق', | ||||
|             loading: { | ||||
|                 text: '...{{databaseType}} ل SQL يقوم الذكاء الاصطناعي بإنشاء', | ||||
|                 description: 'هذا قد يستغرق 30 ثانية', | ||||
|             }, | ||||
|             error: { | ||||
|                 message: | ||||
|                     'النصي. يرجى المحاولة مرة اخرى لاحقاً او <0>اتصل بنا</0> SQL خطأ في إنشاء برنامج', | ||||
|                 description: | ||||
|                     ' الخاصة بك. راجع الدليل <0>هنا</0> OPENAI_TOKEN لا تتردد في استخدام', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         create_relationship_dialog: { | ||||
|             title: 'إنشاء علاقة', | ||||
|             primary_table: 'الجدول الأساسي', | ||||
|             primary_field: 'الحقل الأساسي', | ||||
|             referenced_table: 'الجدول المرتبط', | ||||
|             referenced_field: 'الحقل المرتبط', | ||||
|             primary_table_placeholder: 'حدد الجدول', | ||||
|             primary_field_placeholder: 'حدد الحقل', | ||||
|             referenced_table_placeholder: 'حدد الجدول', | ||||
|             referenced_field_placeholder: 'حدد الحقل', | ||||
|             no_tables_found: 'لم يتم العثور على جداول', | ||||
|             no_fields_found: 'لم يتم العثور على حقول', | ||||
|             create: 'إنشاء', | ||||
|             cancel: 'إلغاء', | ||||
|         }, | ||||
|  | ||||
|         import_database_dialog: { | ||||
|             title: 'استيراد إلى المخطط الحالي', | ||||
|             override_alert: { | ||||
|                 title: 'استيراد قاعدة بيانات', | ||||
|                 content: { | ||||
|                     alert: 'سيؤدي استيراد هذا المخطط إلى التأثير على الجداول والعلاقات الحالية.', | ||||
|                     new_tables: | ||||
|                         'جداول جديدة <bold>{{newTablesNumber}}</bold> سيتم إضافة', | ||||
|                     new_relationships: | ||||
|                         'علاقات جديدة <bold>{{newRelationshipsNumber}}</bold> سيتم إنشاء', | ||||
|                     tables_override: | ||||
|                         'جداول <bold>{{tablesOverrideNumber}}</bold> سيتم تعديل', | ||||
|                     proceed: 'هل تريد المتابعة؟', | ||||
|                 }, | ||||
|                 import: 'استيراد', | ||||
|                 cancel: 'إلغاء', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_image_dialog: { | ||||
|             title: 'تصدير الصورة', | ||||
|             description: ':اختر عامل المقياس للتصدير', | ||||
|             scale_1x: '1x عادي', | ||||
|             scale_2x: '2x (موصى به)', | ||||
|             scale_3x: '3x', | ||||
|             scale_4x: '4x', | ||||
|             cancel: 'إلغاء', | ||||
|             export: 'تصدير', | ||||
|         }, | ||||
|  | ||||
|         new_table_schema_dialog: { | ||||
|             title: 'اختر مخططاً', | ||||
|             description: | ||||
|                 '.يتم حالياً عرض مخططات متعددة. اختر واحداً للجدول الجديد', | ||||
|             cancel: 'إلغاء', | ||||
|             confirm: 'تأكيد', | ||||
|         }, | ||||
|  | ||||
|         update_table_schema_dialog: { | ||||
|             title: 'تغيير المخطط', | ||||
|             description: '"{{tableName}}" تحديث مخطط الجدول', | ||||
|             cancel: 'إلغاء', | ||||
|             confirm: 'تغيير', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: '!ساعدنا على التحسن', | ||||
|             description: '؟! إنها مجرد نقرة واحدةGITHUB هل ترغب في تقييمنا على', | ||||
|             close: 'ليس الآن', | ||||
|             confirm: '!بالتأكيد', | ||||
|         }, | ||||
|         export_diagram_dialog: { | ||||
|             title: 'تصدير المخطط', | ||||
|             description: ':اختر التنسيق للتصدير', | ||||
|             format_json: 'JSON', | ||||
|             cancel: 'إلغاء', | ||||
|             export: 'تصدير', | ||||
|             error: { | ||||
|                 title: 'حدث خطأ أثناء التصدير', | ||||
|                 description: | ||||
|                     'chartdb.io@gmail.com حدث خطأ ما. هل تحتاج إلى مساعدة؟', | ||||
|             }, | ||||
|         }, | ||||
|         import_diagram_dialog: { | ||||
|             title: 'استيراد الرسم البياني', | ||||
|             description: ':للرسم البياني ادناه JSON قم بلصق', | ||||
|             cancel: 'إلغاء', | ||||
|             import: 'استيراد', | ||||
|             error: { | ||||
|                 title: 'حدث خطأ أثناء الاستيراد', | ||||
|                 description: | ||||
|                     'chartdb.io@gmail.com و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني', | ||||
|             }, | ||||
|         }, | ||||
|         import_dbml_dialog: { | ||||
|             // TODO: Translate | ||||
|             title: 'Import DBML', | ||||
|             description: 'Import a database schema from DBML format.', | ||||
|             import: 'Import', | ||||
|             cancel: 'Cancel', | ||||
|             error: { | ||||
|                 title: 'Error', | ||||
|                 description: 'Failed to parse DBML. Please check the syntax.', | ||||
|             }, | ||||
|         }, | ||||
|         relationship_type: { | ||||
|             one_to_one: 'واحد إلى واحد', | ||||
|             one_to_many: 'واحد إلى متعدد', | ||||
|             many_to_one: 'متعدد إلى واحد', | ||||
|             many_to_many: 'متعدد إلى متعدد', | ||||
|         }, | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'جدول جديد', | ||||
|             new_relationship: 'علاقة جديدة', | ||||
|         }, | ||||
|  | ||||
|         table_node_context_menu: { | ||||
|             edit_table: 'تعديل الجدول', | ||||
|             duplicate_table: 'نسخ الجدول', | ||||
|             delete_table: 'حذف الجدول', | ||||
|             add_relationship: 'Add Relationship', // TODO: Translate | ||||
|         }, | ||||
|  | ||||
|         snap_to_grid_tooltip: '({{key}} مغنظة الشبكة (اضغط مع الاستمرار على', | ||||
|  | ||||
|         tool_tips: { | ||||
|             double_click_to_edit: 'انقر مرتين للتعديل', | ||||
|         }, | ||||
|  | ||||
|         language_select: { | ||||
|             change_language: 'اللغة', | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const arMetadata: LanguageMetadata = { | ||||
|     name: 'Arabic', | ||||
|     nativeName: 'العربية', | ||||
|     code: 'ar', | ||||
| }; | ||||
							
								
								
									
										428
									
								
								src/i18n/locales/bn.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,428 @@ | ||||
| import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const bn: LanguageTranslation = { | ||||
|     translation: { | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'ফাইল', | ||||
|                 new: 'নতুন', | ||||
|                 open: 'খুলুন', | ||||
|                 save: 'সংরক্ষণ করুন', | ||||
|                 import: 'ডাটাবেস আমদানি করুন', | ||||
|                 export_sql: 'SQL রপ্তানি করুন', | ||||
|                 export_as: 'রূপে রপ্তানি করুন', | ||||
|                 delete_diagram: 'ডায়াগ্রাম মুছুন', | ||||
|                 exit: 'প্রস্থান করুন', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'সম্পাদনা', | ||||
|                 undo: 'পূর্বাবস্থায় ফিরুন', | ||||
|                 redo: 'পুনরায় করুন', | ||||
|                 clear: 'পরিষ্কার করুন', | ||||
|             }, | ||||
|             view: { | ||||
|                 view: 'দেখুন', | ||||
|                 show_sidebar: 'সাইডবার দেখান', | ||||
|                 hide_sidebar: 'সাইডবার লুকান', | ||||
|                 hide_cardinality: 'কার্ডিনালিটি লুকান', | ||||
|                 show_cardinality: 'কার্ডিনালিটি দেখান', | ||||
|                 zoom_on_scroll: 'স্ক্রলে জুম করুন', | ||||
|                 theme: 'থিম', | ||||
|                 show_dependencies: 'নির্ভরতাগুলি দেখান', | ||||
|                 hide_dependencies: 'নির্ভরতাগুলি লুকান', | ||||
|                 // TODO: Translate | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|  | ||||
|             share: { | ||||
|                 share: 'শেয়ার করুন', | ||||
|                 export_diagram: 'ডায়াগ্রাম রপ্তানি করুন', | ||||
|                 import_diagram: 'ডায়াগ্রাম আমদানি করুন', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'সাহায্য', | ||||
|                 visit_website: 'ChartDB ওয়েবসাইটে যান', | ||||
|                 join_discord: 'আমাদের Discord-এ যোগ দিন', | ||||
|                 schedule_a_call: 'আমাদের সাথে কথা বলুন!', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         delete_diagram_alert: { | ||||
|             title: 'ডায়াগ্রাম মুছুন', | ||||
|             description: | ||||
|                 'এই কাজটি পূর্বাবস্থায় ফিরিয়ে আনা যাবে না। এই ডায়াগ্রাম স্থায়ীভাবে মুছে ফেলা হবে।', | ||||
|             cancel: 'বাতিল করুন', | ||||
|             delete: 'মুছুন', | ||||
|         }, | ||||
|  | ||||
|         clear_diagram_alert: { | ||||
|             title: 'ডায়াগ্রাম পরিষ্কার করুন', | ||||
|             description: | ||||
|                 'এই কাজটি পূর্বাবস্থায় ফিরিয়ে আনা যাবে না। এই ডায়াগ্রামের সমস্ত তথ্য স্থায়ীভাবে মুছে যাবে।', | ||||
|             cancel: 'বাতিল করুন', | ||||
|             clear: 'পরিষ্কার করুন', | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'ডায়াগ্রাম পুনর্বিন্যাস করুন', | ||||
|             description: | ||||
|                 'এই কাজটি ডায়াগ্রামের সমস্ত টেবিল পুনর্বিন্যাস করবে। আপনি কি চালিয়ে যেতে চান?', | ||||
|             reorder: 'পুনর্বিন্যাস করুন', | ||||
|             cancel: 'বাতিল করুন', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'বহু স্কিমা', | ||||
|             description: | ||||
|                 '{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।', | ||||
|             dont_show_again: 'পুনরায় দেখাবেন না', | ||||
|             change_schema: 'পরিবর্তন করুন', | ||||
|             none: 'কিছুই না', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'কপি ব্যর্থ হয়েছে', | ||||
|                 description: 'ক্লিপবোর্ড সমর্থিত নয়', | ||||
|             }, | ||||
|             failed: { | ||||
|                 title: 'কপি ব্যর্থ হয়েছে', | ||||
|                 description: 'কিছু ভুল হয়েছে। অনুগ্রহ করে আবার চেষ্টা করুন।', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         theme: { | ||||
|             system: 'সিস্টেম', | ||||
|             light: 'হালকা', | ||||
|             dark: 'অন্ধকার', | ||||
|         }, | ||||
|  | ||||
|         zoom: { | ||||
|             on: 'চালু', | ||||
|             off: 'বন্ধ', | ||||
|         }, | ||||
|  | ||||
|         last_saved: 'সর্বশেষ সংরক্ষণ', | ||||
|         saved: 'সংরক্ষিত', | ||||
|         loading_diagram: 'ডায়াগ্রাম লোড হচ্ছে...', | ||||
|         deselect_all: 'সব নির্বাচন সরান', | ||||
|         select_all: 'সব নির্বাচন করুন', | ||||
|         clear: 'পরিষ্কার করুন', | ||||
|         show_more: 'আরও দেখুন', | ||||
|         show_less: 'কম দেখুন', | ||||
|         copy_to_clipboard: 'ক্লিপবোর্ডে অনুলিপি করুন', | ||||
|         copied: 'অনুলিপি সম্পন্ন!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'স্কিমা:', | ||||
|             filter_by_schema: 'স্কিমা দ্বারা ফিল্টার করুন', | ||||
|             search_schema: 'স্কিমা খুঁজুন...', | ||||
|             no_schemas_found: 'কোনো স্কিমা পাওয়া যায়নি।', | ||||
|             view_all_options: 'সমস্ত বিকল্প দেখুন...', | ||||
|             tables_section: { | ||||
|                 tables: 'টেবিল', | ||||
|                 add_table: 'টেবিল যোগ করুন', | ||||
|                 filter: 'ফিল্টার', | ||||
|                 collapse: 'সব ভাঁজ করুন', | ||||
|                 // TODO: Translate | ||||
|                 clear: 'Clear Filter', | ||||
|                 no_results: 'No tables found matching your filter.', | ||||
|                 // TODO: Translate | ||||
|                 show_list: 'Show Table List', | ||||
|                 show_dbml: 'Show DBML Editor', | ||||
|  | ||||
|                 table: { | ||||
|                     fields: 'ফিল্ড', | ||||
|                     nullable: 'নালযোগ্য?', | ||||
|                     primary_key: 'প্রাথমিক কী', | ||||
|                     indexes: 'ইনডেক্স', | ||||
|                     comments: 'মন্তব্য', | ||||
|                     no_comments: 'কোনো মন্তব্য নেই', | ||||
|                     add_field: 'ফিল্ড যোগ করুন', | ||||
|                     add_index: 'ইনডেক্স যোগ করুন', | ||||
|                     index_select_fields: 'ফিল্ড নির্বাচন করুন', | ||||
|                     no_types_found: 'কোনো ধরন পাওয়া যায়নি', | ||||
|                     field_name: 'নাম', | ||||
|                     field_type: 'ধরন', | ||||
|                     field_actions: { | ||||
|                         title: 'ফিল্ড কর্ম', | ||||
|                         unique: 'অদ্বিতীয়', | ||||
|                         comments: 'মন্তব্য', | ||||
|                         no_comments: 'কোনো মন্তব্য নেই', | ||||
|                         delete_field: 'ফিল্ড মুছুন', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'ইনডেক্স কর্ম', | ||||
|                         name: 'নাম', | ||||
|                         unique: 'অদ্বিতীয়', | ||||
|                         delete_index: 'ইনডেক্স মুছুন', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
|                         title: 'টেবিল কর্ম', | ||||
|                         change_schema: 'স্কিমা পরিবর্তন করুন', | ||||
|                         add_field: 'ফিল্ড যোগ করুন', | ||||
|                         add_index: 'ইনডেক্স যোগ করুন', | ||||
|                         duplicate_table: 'টেবিল নকল করুন', | ||||
|                         delete_table: 'টেবিল মুছুন', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'কোনো টেবিল নেই', | ||||
|                     description: 'শুরু করতে একটি টেবিল তৈরি করুন', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'সম্পর্ক', | ||||
|                 filter: 'ফিল্টার', | ||||
|                 add_relationship: 'সম্পর্ক যোগ করুন', | ||||
|                 collapse: 'সব ভাঁজ করুন', | ||||
|                 relationship: { | ||||
|                     primary: 'প্রাথমিক টেবিল', | ||||
|                     foreign: 'বিদেশি টেবিল', | ||||
|                     cardinality: 'কার্ডিনালিটি', | ||||
|                     delete_relationship: 'মুছুন', | ||||
|                     relationship_actions: { | ||||
|                         title: 'কর্ম', | ||||
|                         delete_relationship: 'মুছুন', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'কোনো সম্পর্ক নেই', | ||||
|                     description: 'টেবিল সংযোগ করতে একটি সম্পর্ক তৈরি করুন', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'নির্ভরতাগুলি', | ||||
|                 filter: 'ফিল্টার', | ||||
|                 collapse: 'ভাঁজ করুন', | ||||
|                 dependency: { | ||||
|                     table: 'টেবিল', | ||||
|                     dependent_table: 'নির্ভরশীল টেবিল', | ||||
|                     delete_dependency: 'নির্ভরতা মুছুন', | ||||
|                     dependency_actions: { | ||||
|                         title: 'কর্ম', | ||||
|                         delete_dependency: 'নির্ভরতা মুছুন', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'কোনো নির্ভরতাগুলি নেই', | ||||
|                     description: 'এই অংশে কোনো নির্ভরতা উপলব্ধ নেই।', | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         toolbar: { | ||||
|             zoom_in: 'জুম ইন', | ||||
|             zoom_out: 'জুম আউট', | ||||
|             save: 'সংরক্ষণ করুন', | ||||
|             show_all: 'সব দেখান', | ||||
|             undo: 'পূর্বাবস্থায় ফিরুন', | ||||
|             redo: 'পুনরায় করুন', | ||||
|             reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন', | ||||
|             highlight_overlapping_tables: 'ওভারল্যাপিং টেবিল হাইলাইট করুন', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
|             database_selection: { | ||||
|                 title: 'আপনার ডাটাবেস কী?', | ||||
|                 description: | ||||
|                     'প্রত্যেক ডাটাবেসের নিজস্ব বৈশিষ্ট্য এবং ক্ষমতা রয়েছে।', | ||||
|                 check_examples_long: 'উদাহরণ দেখুন', | ||||
|                 check_examples_short: 'উদাহরণ', | ||||
|             }, | ||||
|  | ||||
|             import_database: { | ||||
|                 title: 'আপনার ডাটাবেস আমদানি করুন', | ||||
|                 database_edition: 'ডাটাবেস সংস্করণ:', | ||||
|                 step_1: 'আপনার ডাটাবেসে এই স্ক্রিপ্ট চালান:', | ||||
|                 step_2: 'স্ক্রিপ্টের ফলাফল এখানে পেস্ট করুন:', | ||||
|                 script_results_placeholder: 'স্ক্রিপ্টের ফলাফল এখানে...', | ||||
|                 ssms_instructions: { | ||||
|                     button_text: 'SSMS নির্দেশনা', | ||||
|                     title: 'নির্দেশনা', | ||||
|                     step_1: 'টুলস > অপশন > কোয়েরি ফলাফল > SQL সার্ভারে যান।', | ||||
|                     step_2: 'যদি আপনি "গ্রিডে ফলাফল" ব্যবহার করেন, তাহলে নন-XML ডেটার জন্য সর্বাধিক চরিত্রগুলি 9999999-এ সেট করুন।', | ||||
|                 }, | ||||
|                 instructions_link: 'সাহায্যের প্রয়োজন? এখানে দেখুন', | ||||
|                 check_script_result: 'স্ক্রিপ্ট ফলাফল যাচাই করুন', | ||||
|             }, | ||||
|  | ||||
|             cancel: 'বাতিল করুন', | ||||
|             back: 'ফিরে যান', | ||||
|             import_from_file: 'ফাইল থেকে আমদানি করুন', | ||||
|             empty_diagram: 'ফাঁকা চিত্র', | ||||
|             continue: 'চালিয়ে যান', | ||||
|             import: 'আমদানি করুন', | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'চিত্র খুলুন', | ||||
|             description: 'নিচের তালিকা থেকে একটি চিত্র নির্বাচন করুন।', | ||||
|             table_columns: { | ||||
|                 name: 'নাম', | ||||
|                 created_at: 'তৈরির তারিখ', | ||||
|                 last_modified: 'সর্বশেষ পরিবর্তিত', | ||||
|                 tables_count: 'টেবিল', | ||||
|             }, | ||||
|             cancel: 'বাতিল করুন', | ||||
|             open: 'খুলুন', | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
|             title: 'SQL রপ্তানি করুন', | ||||
|             description: | ||||
|                 '{{databaseType}} স্ক্রিপ্টের জন্য আপনার ডায়াগ্রাম স্কিমা রপ্তানি করুন', | ||||
|             close: 'বন্ধ করুন', | ||||
|             loading: { | ||||
|                 text: '{{databaseType}} এর জন্য AI SQL তৈরি হচ্ছে...', | ||||
|                 description: 'এতে ৩০ সেকেন্ড পর্যন্ত সময় লাগতে পারে।', | ||||
|             }, | ||||
|             error: { | ||||
|                 message: | ||||
|                     'SQL স্ক্রিপ্ট তৈরি করার সময় একটি ত্রুটি ঘটেছে। অনুগ্রহ করে পরে আবার চেষ্টা করুন বা <0>আমাদের সাথে যোগাযোগ করুন</0>।', | ||||
|                 description: | ||||
|                     'আপনার OPENAI_TOKEN ব্যবহার করার জন্য বিনামূল্যে অভিজ্ঞতা নিন, ম্যানুয়াল <0>এখানে দেখুন</0>।', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         create_relationship_dialog: { | ||||
|             title: 'সম্পর্ক তৈরি করুন', | ||||
|             primary_table: 'প্রাথমিক টেবিল', | ||||
|             primary_field: 'প্রাথমিক ক্ষেত্র', | ||||
|             referenced_table: 'রেফারেন্স করা টেবিল', | ||||
|             referenced_field: 'রেফারেন্স করা ক্ষেত্র', | ||||
|             primary_table_placeholder: 'টেবিল নির্বাচন করুন', | ||||
|             primary_field_placeholder: 'ক্ষেত্র নির্বাচন করুন', | ||||
|             referenced_table_placeholder: 'টেবিল নির্বাচন করুন', | ||||
|             referenced_field_placeholder: 'ক্ষেত্র নির্বাচন করুন', | ||||
|             no_tables_found: 'কোন টেবিল পাওয়া যায়নি', | ||||
|             no_fields_found: 'কোন ক্ষেত্র পাওয়া যায়নি', | ||||
|             create: 'তৈরি করুন', | ||||
|             cancel: 'বাতিল করুন', | ||||
|         }, | ||||
|  | ||||
|         import_database_dialog: { | ||||
|             title: 'বর্তমান চিত্রে আমদানি করুন', | ||||
|             override_alert: { | ||||
|                 title: 'ডাটাবেস আমদানি করুন', | ||||
|                 content: { | ||||
|                     alert: 'এই চিত্র আমদানির ফলে বিদ্যমান টেবিল ও সম্পর্ক প্রভাবিত হবে।', | ||||
|                     new_tables: | ||||
|                         '<bold>{{newTablesNumber}}</bold> নতুন টেবিল যোগ করা হবে।', | ||||
|                     new_relationships: | ||||
|                         '<bold>{{newRelationshipsNumber}}</bold> নতুন সম্পর্ক তৈরি করা হবে।', | ||||
|                     tables_override: | ||||
|                         '<bold>{{tablesOverrideNumber}}</bold> টেবিল ওভাররাইট করা হবে।', | ||||
|                     proceed: 'আপনি কি এগিয়ে যেতে চান?', | ||||
|                 }, | ||||
|                 import: 'আমদানি করুন', | ||||
|                 cancel: 'বাতিল করুন', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_image_dialog: { | ||||
|             title: 'চিত্র রপ্তানি করুন', | ||||
|             description: 'রপ্তানির জন্য স্কেল ফ্যাক্টর নির্বাচন করুন:', | ||||
|             scale_1x: '1x স্বাভাবিক', | ||||
|             scale_2x: '2x (প্রস্তাবিত)', | ||||
|             scale_3x: '3x', | ||||
|             scale_4x: '4x', | ||||
|             cancel: 'বাতিল করুন', | ||||
|             export: 'রপ্তানি করুন', | ||||
|         }, | ||||
|  | ||||
|         new_table_schema_dialog: { | ||||
|             title: 'স্কিমা নির্বাচন করুন', | ||||
|             description: | ||||
|                 'বর্তমানে অনেক স্কিমা প্রদর্শিত হচ্ছে। নতুন টেবিলের জন্য একটি নির্বাচন করুন।', | ||||
|             cancel: 'বাতিল করুন', | ||||
|             confirm: 'নিশ্চিত করুন', | ||||
|         }, | ||||
|  | ||||
|         update_table_schema_dialog: { | ||||
|             title: 'স্কিমা পরিবর্তন করুন', | ||||
|             description: 'টেবিল "{{tableName}}" এর জন্য স্কিমা আপডেট করুন', | ||||
|             cancel: 'বাতিল করুন', | ||||
|             confirm: 'পরিবর্তন করুন', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: 'আমাদের উন্নত করতে সাহায্য করুন!', | ||||
|             description: | ||||
|                 'আপনি কি GitHub-এ আমাদের একটি স্টার দিতে পারবেন? এটি মাত্র এক ক্লিক দূরে!', | ||||
|             close: 'এখন নয়', | ||||
|             confirm: 'অবশ্যই!', | ||||
|         }, | ||||
|  | ||||
|         export_diagram_dialog: { | ||||
|             title: 'চিত্র রপ্তানি করুন', | ||||
|             description: 'রপ্তানির জন্য ফরম্যাট নির্বাচন করুন:', | ||||
|             format_json: 'JSON', | ||||
|             cancel: 'বাতিল করুন', | ||||
|             export: 'রপ্তানি করুন', | ||||
|             error: { | ||||
|                 title: 'চিত্র রপ্তানিতে ত্রুটি', | ||||
|                 description: | ||||
|                     'কিছু ভুল হয়েছে। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         import_diagram_dialog: { | ||||
|             title: 'চিত্র আমদানি করুন', | ||||
|             description: 'নীচে ডায়াগ্রাম JSON পেস্ট করুন:', | ||||
|             cancel: 'বাতিল করুন', | ||||
|             import: 'আমদানি করুন', | ||||
|             error: { | ||||
|                 title: 'চিত্র আমদানিতে ত্রুটি', | ||||
|                 description: | ||||
|                     'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।', | ||||
|             }, | ||||
|         }, | ||||
|         // TODO: Translate | ||||
|         import_dbml_dialog: { | ||||
|             title: 'Import DBML', | ||||
|             description: 'Import a database schema from DBML format.', | ||||
|             import: 'Import', | ||||
|             cancel: 'Cancel', | ||||
|             error: { | ||||
|                 title: 'Error', | ||||
|                 description: 'Failed to parse DBML. Please check the syntax.', | ||||
|             }, | ||||
|         }, | ||||
|         relationship_type: { | ||||
|             one_to_one: 'এক থেকে এক', | ||||
|             one_to_many: 'এক থেকে অনেক', | ||||
|             many_to_one: 'অনেক থেকে এক', | ||||
|             many_to_many: 'অনেক থেকে অনেক', | ||||
|         }, | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'নতুন টেবিল', | ||||
|             new_relationship: 'নতুন সম্পর্ক', | ||||
|         }, | ||||
|  | ||||
|         table_node_context_menu: { | ||||
|             edit_table: 'টেবিল সম্পাদনা করুন', | ||||
|             duplicate_table: 'টেবিল নকল করুন', | ||||
|             delete_table: 'টেবিল মুছে ফেলুন', | ||||
|             add_relationship: 'Add Relationship', // TODO: Translate | ||||
|         }, | ||||
|  | ||||
|         snap_to_grid_tooltip: 'গ্রিডে স্ন্যাপ করুন (অবস্থান {{key}})', | ||||
|  | ||||
|         tool_tips: { | ||||
|             double_click_to_edit: 'সম্পাদনা করতে ডাবল-ক্লিক করুন', | ||||
|         }, | ||||
|  | ||||
|         language_select: { | ||||
|             change_language: 'ভাষা পরিবর্তন করুন', | ||||
|         }, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const bnMetadata: LanguageMetadata = { | ||||
|     name: 'Bengali', | ||||
|     nativeName: 'বাংলা', | ||||
|     code: 'bn', | ||||
| }; | ||||
| @@ -8,7 +8,7 @@ export const de: LanguageTranslation = { | ||||
|                 new: 'Neu', | ||||
|                 open: 'Öffnen', | ||||
|                 save: 'Speichern', | ||||
|                 import_database: 'Datenbank importieren', | ||||
|                 import: 'Datenbank importieren', | ||||
|                 export_sql: 'SQL exportieren', | ||||
|                 export_as: 'Exportieren als', | ||||
|                 delete_diagram: 'Diagramm löschen', | ||||
| @@ -30,6 +30,9 @@ export const de: LanguageTranslation = { | ||||
|                 theme: 'Stil', | ||||
|                 show_dependencies: 'Abhängigkeiten anzeigen', | ||||
|                 hide_dependencies: 'Abhängigkeiten ausblenden', | ||||
|                 // TODO: Translate | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             // TODO: Translate | ||||
|             share: { | ||||
| @@ -78,6 +81,18 @@ export const de: LanguageTranslation = { | ||||
|             none: 'Keine', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Kopieren fehlgeschlagen', | ||||
|                 description: 'Zwischenablage nicht unterstützt', | ||||
|             }, | ||||
|             failed: { | ||||
|                 title: 'Kopieren fehlgeschlagen', | ||||
|                 description: | ||||
|                     'Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         theme: { | ||||
|             system: 'System', | ||||
|             light: 'Hell', | ||||
| @@ -91,7 +106,6 @@ export const de: LanguageTranslation = { | ||||
|  | ||||
|         last_saved: 'Zuletzt gespeichert', | ||||
|         saved: 'Gespeichert', | ||||
|         diagrams: 'Diagramme', | ||||
|         loading_diagram: 'Diagramm wird geladen...', | ||||
|         deselect_all: 'Alles abwählen', | ||||
|         select_all: 'Alles auswählen', | ||||
| @@ -112,6 +126,12 @@ export const de: LanguageTranslation = { | ||||
|                 add_table: 'Tabelle hinzufügen', | ||||
|                 filter: 'Filter', | ||||
|                 collapse: 'Alle einklappen', | ||||
|                 // TODO: Translate | ||||
|                 clear: 'Clear Filter', | ||||
|                 no_results: 'No tables found matching your filter.', | ||||
|                 // TODO: Translate | ||||
|                 show_list: 'Show Table List', | ||||
|                 show_dbml: 'Show DBML Editor', | ||||
|  | ||||
|                 table: { | ||||
|                     fields: 'Felder', | ||||
| @@ -362,6 +382,17 @@ export const de: LanguageTranslation = { | ||||
|                     'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com', | ||||
|             }, | ||||
|         }, | ||||
|         // TODO: Translate | ||||
|         import_dbml_dialog: { | ||||
|             title: 'Import DBML', | ||||
|             description: 'Import a database schema from DBML format.', | ||||
|             import: 'Import', | ||||
|             cancel: 'Cancel', | ||||
|             error: { | ||||
|                 title: 'Error', | ||||
|                 description: 'Failed to parse DBML. Please check the syntax.', | ||||
|             }, | ||||
|         }, | ||||
|         relationship_type: { | ||||
|             one_to_one: 'Ein zu Eins (1:1)', | ||||
|             one_to_many: 'Ein zu Viele (1:n)', | ||||
| @@ -378,6 +409,7 @@ export const de: LanguageTranslation = { | ||||
|             edit_table: 'Tabelle bearbeiten', | ||||
|             duplicate_table: 'Duplicate Table', // TODO: Translate | ||||
|             delete_table: 'Tabelle löschen', | ||||
|             add_relationship: 'Add Relationship', // TODO: Translate | ||||
|         }, | ||||
|  | ||||
|         // TODO: Add translations | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export const en = { | ||||
|                 new: 'New', | ||||
|                 open: 'Open', | ||||
|                 save: 'Save', | ||||
|                 import_database: 'Import Database', | ||||
|                 import: 'Import', | ||||
|                 export_sql: 'Export SQL', | ||||
|                 export_as: 'Export as', | ||||
|                 delete_diagram: 'Delete Diagram', | ||||
| @@ -30,6 +30,8 @@ export const en = { | ||||
|                 theme: 'Theme', | ||||
|                 show_dependencies: 'Show Dependencies', | ||||
|                 hide_dependencies: 'Hide Dependencies', | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             share: { | ||||
|                 share: 'Share', | ||||
| @@ -77,6 +79,17 @@ export const en = { | ||||
|             none: 'none', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Copy failed', | ||||
|                 description: 'Clipboard not supported.', | ||||
|             }, | ||||
|             failed: { | ||||
|                 title: 'Copy failed', | ||||
|                 description: 'Something went wrong. Please try again.', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         theme: { | ||||
|             system: 'System', | ||||
|             light: 'Light', | ||||
| @@ -90,7 +103,6 @@ export const en = { | ||||
|  | ||||
|         last_saved: 'Last saved', | ||||
|         saved: 'Saved', | ||||
|         diagrams: 'Diagrams', | ||||
|         loading_diagram: 'Loading diagram...', | ||||
|         deselect_all: 'Deselect All', | ||||
|         select_all: 'Select All', | ||||
| @@ -111,6 +123,10 @@ export const en = { | ||||
|                 add_table: 'Add Table', | ||||
|                 filter: 'Filter', | ||||
|                 collapse: 'Collapse All', | ||||
|                 clear: 'Clear Filter', | ||||
|                 no_results: 'No tables found matching your filter.', | ||||
|                 show_list: 'Show Table List', | ||||
|                 show_dbml: 'Show DBML Editor', | ||||
|  | ||||
|                 table: { | ||||
|                     fields: 'Fields', | ||||
| @@ -349,7 +365,7 @@ export const en = { | ||||
|  | ||||
|         import_diagram_dialog: { | ||||
|             title: 'Import Diagram', | ||||
|             description: 'Paste the diagram JSON below:', | ||||
|             description: 'Import a diagram from a JSON file.', | ||||
|             cancel: 'Cancel', | ||||
|             import: 'Import', | ||||
|             error: { | ||||
| @@ -358,6 +374,17 @@ export const en = { | ||||
|                     'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         import_dbml_dialog: { | ||||
|             title: 'Import DBML', | ||||
|             description: 'Import a database schema from DBML format.', | ||||
|             import: 'Import', | ||||
|             cancel: 'Cancel', | ||||
|             error: { | ||||
|                 title: 'Error importing DBML', | ||||
|                 description: 'Failed to parse DBML. Please check the syntax.', | ||||
|             }, | ||||
|         }, | ||||
|         relationship_type: { | ||||
|             one_to_one: 'One to One', | ||||
|             one_to_many: 'One to Many', | ||||
| @@ -374,6 +401,7 @@ export const en = { | ||||
|             edit_table: 'Edit Table', | ||||
|             duplicate_table: 'Duplicate Table', | ||||
|             delete_table: 'Delete Table', | ||||
|             add_relationship: 'Add Relationship', | ||||
|         }, | ||||
|  | ||||
|         snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})', | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export const es: LanguageTranslation = { | ||||
|                 new: 'Nuevo', | ||||
|                 open: 'Abrir', | ||||
|                 save: 'Guardar', | ||||
|                 import_database: 'Importar Base de Datos', | ||||
|                 import: 'Importar Base de Datos', | ||||
|                 export_sql: 'Exportar SQL', | ||||
|                 export_as: 'Exportar como', | ||||
|                 delete_diagram: 'Eliminar Diagrama', | ||||
| @@ -30,6 +30,9 @@ export const es: LanguageTranslation = { | ||||
|                 theme: 'Tema', | ||||
|                 show_dependencies: 'Mostrar dependencias', | ||||
|                 hide_dependencies: 'Ocultar dependencias', | ||||
|                 // TODO: Translate | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             // TODO: Translate | ||||
|             share: { | ||||
| @@ -69,6 +72,17 @@ export const es: LanguageTranslation = { | ||||
|             cancel: 'Cancelar', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Copia fallida', | ||||
|                 description: 'Portapapeles no soportado', | ||||
|             }, | ||||
|             failed: { | ||||
|                 title: 'Copia fallida', | ||||
|                 description: 'Algo salió mal. Por favor, inténtelo de nuevo.', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         theme: { | ||||
|             system: 'Sistema', | ||||
|             light: 'Claro', | ||||
| @@ -82,7 +96,6 @@ export const es: LanguageTranslation = { | ||||
|  | ||||
|         last_saved: 'Último guardado', | ||||
|         saved: 'Guardado', | ||||
|         diagrams: 'Diagramas', | ||||
|         loading_diagram: 'Cargando diagrama...', | ||||
|         deselect_all: 'Deseleccionar todo', | ||||
|         select_all: 'Seleccionar todo', | ||||
| @@ -103,6 +116,12 @@ export const es: LanguageTranslation = { | ||||
|                 add_table: 'Agregar Tabla', | ||||
|                 filter: 'Filtrar', | ||||
|                 collapse: 'Colapsar Todo', | ||||
|                 // TODO: Translate | ||||
|                 clear: 'Clear Filter', | ||||
|                 no_results: 'No tables found matching your filter.', | ||||
|                 // TODO: Translate | ||||
|                 show_list: 'Show Table List', | ||||
|                 show_dbml: 'Show DBML Editor', | ||||
|  | ||||
|                 table: { | ||||
|                     fields: 'Campos', | ||||
| @@ -362,6 +381,17 @@ export const es: LanguageTranslation = { | ||||
|                     'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com', | ||||
|             }, | ||||
|         }, | ||||
|         // TODO: Translate | ||||
|         import_dbml_dialog: { | ||||
|             title: 'Import DBML', | ||||
|             description: 'Import a database schema from DBML format.', | ||||
|             import: 'Import', | ||||
|             cancel: 'Cancel', | ||||
|             error: { | ||||
|                 title: 'Error', | ||||
|                 description: 'Failed to parse DBML. Please check the syntax.', | ||||
|             }, | ||||
|         }, | ||||
|         relationship_type: { | ||||
|             one_to_one: 'Uno a Uno', | ||||
|             one_to_many: 'Uno a Muchos', | ||||
| @@ -378,6 +408,7 @@ export const es: LanguageTranslation = { | ||||
|             edit_table: 'Editar Tabla', | ||||
|             duplicate_table: 'Duplicate Table', // TODO: Translate | ||||
|             delete_table: 'Eliminar Tabla', | ||||
|             add_relationship: 'Add Relationship', // TODO: Translate | ||||
|         }, | ||||
|  | ||||
|         // TODO: Add translations | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export const fr: LanguageTranslation = { | ||||
|                 new: 'Nouveau', | ||||
|                 open: 'Ouvrir', | ||||
|                 save: 'Enregistrer', | ||||
|                 import_database: 'Importer Base de Données', | ||||
|                 import: 'Importer Base de Données', | ||||
|                 export_sql: 'Exporter SQL', | ||||
|                 export_as: 'Exporter en tant que', | ||||
|                 delete_diagram: 'Supprimer le Diagramme', | ||||
| @@ -30,6 +30,9 @@ export const fr: LanguageTranslation = { | ||||
|                 theme: 'Thème', | ||||
|                 show_dependencies: 'Afficher les Dépendances', | ||||
|                 hide_dependencies: 'Masquer les Dépendances', | ||||
|                 // TODO: Translate | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             share: { | ||||
|                 share: 'Partage', | ||||
| @@ -68,6 +71,17 @@ export const fr: LanguageTranslation = { | ||||
|             cancel: 'Annuler', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Échec de la copie', | ||||
|                 description: 'Presse-papiers non pris en charge', | ||||
|             }, | ||||
|             failed: { | ||||
|                 title: 'Échec de la copie', | ||||
|                 description: 'Quelque chose a mal tourné. Veuillez réessayer.', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         theme: { | ||||
|             system: 'Système', | ||||
|             light: 'Clair', | ||||
| @@ -81,7 +95,6 @@ export const fr: LanguageTranslation = { | ||||
|  | ||||
|         last_saved: 'Dernière sauvegarde', | ||||
|         saved: 'Enregistré', | ||||
|         diagrams: 'Diagrammes', | ||||
|         loading_diagram: 'Chargement du diagramme...', | ||||
|         deselect_all: 'Tout désélectionner', | ||||
|         select_all: 'Tout sélectionner', | ||||
| @@ -103,6 +116,12 @@ export const fr: LanguageTranslation = { | ||||
|                 add_table: 'Ajouter une Table', | ||||
|                 filter: 'Filtrer', | ||||
|                 collapse: 'Réduire Tout', | ||||
|                 // TODO: Translate | ||||
|                 clear: 'Clear Filter', | ||||
|                 no_results: 'No tables found matching your filter.', | ||||
|                 // TODO: Translate | ||||
|                 show_list: 'Show Table List', | ||||
|                 show_dbml: 'Show DBML Editor', | ||||
|  | ||||
|                 table: { | ||||
|                     fields: 'Champs', | ||||
| @@ -364,6 +383,17 @@ export const fr: LanguageTranslation = { | ||||
|                     'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com', | ||||
|             }, | ||||
|         }, | ||||
|         // TODO: Translate | ||||
|         import_dbml_dialog: { | ||||
|             title: 'Import DBML', | ||||
|             description: 'Import a database schema from DBML format.', | ||||
|             import: 'Import', | ||||
|             cancel: 'Cancel', | ||||
|             error: { | ||||
|                 title: 'Error', | ||||
|                 description: 'Failed to parse DBML. Please check the syntax.', | ||||
|             }, | ||||
|         }, | ||||
|         relationship_type: { | ||||
|             one_to_one: 'Un à Un', | ||||
|             one_to_many: 'Un à Plusieurs', | ||||
| @@ -380,6 +410,7 @@ export const fr: LanguageTranslation = { | ||||
|             edit_table: 'Éditer la Table', | ||||
|             duplicate_table: 'Duplicate Table', // TODO: Translate | ||||
|             delete_table: 'Supprimer la Table', | ||||
|             add_relationship: 'Add Relationship', // TODO: Translate | ||||
|         }, | ||||
|  | ||||
|         // TODO: Add translations | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export const gu: LanguageTranslation = { | ||||
|                 new: 'નવું', | ||||
|                 open: 'ખોલો', | ||||
|                 save: 'સાચવો', | ||||
|                 import_database: 'ડેટાબેસ આયાત કરો', | ||||
|                 import: 'ડેટાબેસ આયાત કરો', | ||||
|                 export_sql: 'SQL નિકાસ કરો', | ||||
|                 export_as: 'રૂપે નિકાસ કરો', | ||||
|                 delete_diagram: 'ડાયાગ્રામ કાઢી નાખો', | ||||
| @@ -30,6 +30,9 @@ export const gu: LanguageTranslation = { | ||||
|                 theme: 'થિમ', | ||||
|                 show_dependencies: 'નિર્ભરતાઓ બતાવો', | ||||
|                 hide_dependencies: 'નિર્ભરતાઓ છુપાવો', | ||||
|                 // TODO: Translate | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|  | ||||
|             share: { | ||||
| @@ -78,6 +81,17 @@ export const gu: LanguageTranslation = { | ||||
|             none: 'કઈ નહીં', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'નકલ નિષ્ફળ', | ||||
|                 description: 'ક્લિપબોર્ડ આધારિત નથી', | ||||
|             }, | ||||
|             failed: { | ||||
|                 title: 'નકલ નિષ્ફળ', | ||||
|                 description: 'કંઈક ખોટું થયું છે. કૃપા કરીને ફરી પ્રયાસ કરો.', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         theme: { | ||||
|             system: 'સિસ્ટમ', | ||||
|             light: 'હલકો', | ||||
| @@ -91,7 +105,6 @@ export const gu: LanguageTranslation = { | ||||
|  | ||||
|         last_saved: 'છેલ્લે સાચવ્યું', | ||||
|         saved: 'સાચવ્યું', | ||||
|         diagrams: 'ડાયાગ્રામ', | ||||
|         loading_diagram: 'ડાયાગ્રામ લોડ થઈ રહ્યું છે...', | ||||
|         deselect_all: 'બધાને ડીસેલેક્ટ કરો', | ||||
|         select_all: 'બધા પસંદ કરો', | ||||
| @@ -112,6 +125,12 @@ export const gu: LanguageTranslation = { | ||||
|                 add_table: 'ટેબલ ઉમેરો', | ||||
|                 filter: 'ફિલ્ટર', | ||||
|                 collapse: 'બધાને સકુચિત કરો', | ||||
|                 // TODO: Translate | ||||
|                 clear: 'Clear Filter', | ||||
|                 no_results: 'No tables found matching your filter.', | ||||
|                 // TODO: Translate | ||||
|                 show_list: 'Show Table List', | ||||
|                 show_dbml: 'Show DBML Editor', | ||||
|  | ||||
|                 table: { | ||||
|                     fields: 'ફીલ્ડ્સ', | ||||
| @@ -360,6 +379,17 @@ export const gu: LanguageTranslation = { | ||||
|                     'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.', | ||||
|             }, | ||||
|         }, | ||||
|         // TODO: Translate | ||||
|         import_dbml_dialog: { | ||||
|             title: 'Import DBML', | ||||
|             description: 'Import a database schema from DBML format.', | ||||
|             import: 'Import', | ||||
|             cancel: 'Cancel', | ||||
|             error: { | ||||
|                 title: 'Error', | ||||
|                 description: 'Failed to parse DBML. Please check the syntax.', | ||||
|             }, | ||||
|         }, | ||||
|         relationship_type: { | ||||
|             one_to_one: 'એકથી એક', | ||||
|             one_to_many: 'એકથી ઘણા', | ||||
| @@ -376,6 +406,7 @@ export const gu: LanguageTranslation = { | ||||
|             edit_table: 'ટેબલ સંપાદિત કરો', | ||||
|             duplicate_table: 'ટેબલ નકલ કરો', | ||||
|             delete_table: 'ટેબલ કાઢી નાખો', | ||||
|             add_relationship: 'Add Relationship', // TODO: Translate | ||||
|         }, | ||||
|  | ||||
|         snap_to_grid_tooltip: 'ગ્રિડ પર સ્નેપ કરો (જમાવટ {{key}})', | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export const hi: LanguageTranslation = { | ||||
|                 new: 'नया', | ||||
|                 open: 'खोलें', | ||||
|                 save: 'सहेजें', | ||||
|                 import_database: 'डेटाबेस आयात करें', | ||||
|                 import: 'डेटाबेस आयात करें', | ||||
|                 export_sql: 'SQL निर्यात करें', | ||||
|                 export_as: 'के रूप में निर्यात करें', | ||||
|                 delete_diagram: 'आरेख हटाएँ', | ||||
| @@ -30,6 +30,9 @@ export const hi: LanguageTranslation = { | ||||
|                 theme: 'थीम', | ||||
|                 show_dependencies: 'निर्भरता दिखाएँ', | ||||
|                 hide_dependencies: 'निर्भरता छिपाएँ', | ||||
|                 // TODO: Translate | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             // TODO: Translate | ||||
|             share: { | ||||
| @@ -78,6 +81,17 @@ export const hi: LanguageTranslation = { | ||||
|             none: 'कोई नहीं', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'कॉपी असफल', | ||||
|                 description: 'क्लिपबोर्ड समर्थित नहीं है', | ||||
|             }, | ||||
|             failed: { | ||||
|                 title: 'कॉपी असफल', | ||||
|                 description: 'कुछ गलत हो गया। कृपया पुनः प्रयास करें।', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         theme: { | ||||
|             system: 'सिस्टम', | ||||
|             light: 'हल्का', | ||||
| @@ -91,7 +105,6 @@ export const hi: LanguageTranslation = { | ||||
|  | ||||
|         last_saved: 'अंतिम सहेजा गया', | ||||
|         saved: 'सहेजा गया', | ||||
|         diagrams: 'आरेख', | ||||
|         loading_diagram: 'आरेख लोड हो रहा है...', | ||||
|         deselect_all: 'सभी को अचयनित करें', | ||||
|         select_all: 'सभी को चुनें', | ||||
| @@ -113,6 +126,12 @@ export const hi: LanguageTranslation = { | ||||
|                 add_table: 'तालिका जोड़ें', | ||||
|                 filter: 'फ़िल्टर', | ||||
|                 collapse: 'सभी को संक्षिप्त करें', | ||||
|                 // TODO: Translate | ||||
|                 clear: 'Clear Filter', | ||||
|                 no_results: 'No tables found matching your filter.', | ||||
|                 // TODO: Translate | ||||
|                 show_list: 'Show Table List', | ||||
|                 show_dbml: 'Show DBML Editor', | ||||
|  | ||||
|                 table: { | ||||
|                     fields: 'फ़ील्ड्स', | ||||
| @@ -364,6 +383,17 @@ export const hi: LanguageTranslation = { | ||||
|                     'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com', | ||||
|             }, | ||||
|         }, | ||||
|         // TODO: Translate | ||||
|         import_dbml_dialog: { | ||||
|             title: 'Import DBML', | ||||
|             description: 'Import a database schema from DBML format.', | ||||
|             import: 'Import', | ||||
|             cancel: 'Cancel', | ||||
|             error: { | ||||
|                 title: 'Error', | ||||
|                 description: 'Failed to parse DBML. Please check the syntax.', | ||||
|             }, | ||||
|         }, | ||||
|         relationship_type: { | ||||
|             one_to_one: 'एक से एक', | ||||
|             one_to_many: 'एक से कई', | ||||
| @@ -380,6 +410,7 @@ export const hi: LanguageTranslation = { | ||||
|             edit_table: 'तालिका संपादित करें', | ||||
|             duplicate_table: 'Duplicate Table', // TODO: Translate | ||||
|             delete_table: 'तालिका हटाएँ', | ||||
|             add_relationship: 'Add Relationship', // TODO: Translate | ||||
|         }, | ||||
|  | ||||
|         // TODO: Add translations | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export const id_ID: LanguageTranslation = { | ||||
|                 new: 'Buat Baru', | ||||
|                 open: 'Buka', | ||||
|                 save: 'Simpan', | ||||
|                 import_database: 'Impor Database', | ||||
|                 import: 'Impor Database', | ||||
|                 export_sql: 'Ekspor SQL', | ||||
|                 export_as: 'Ekspor Sebagai', | ||||
|                 delete_diagram: 'Hapus Diagram', | ||||
| @@ -30,12 +30,14 @@ export const id_ID: LanguageTranslation = { | ||||
|                 theme: 'Tema', | ||||
|                 show_dependencies: 'Tampilkan Dependensi', | ||||
|                 hide_dependencies: 'Sembunyikan Dependensi', | ||||
|                 // TODO: Translate | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             // TODO: Translate | ||||
|             share: { | ||||
|                 share: 'Share', | ||||
|                 export_diagram: 'Export Diagram', | ||||
|                 import_diagram: 'Import Diagram', | ||||
|                 share: 'Bagikan', | ||||
|                 export_diagram: 'Ekspor Diagram', | ||||
|                 import_diagram: 'Impor Diagram', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'Bantuan', | ||||
| @@ -78,6 +80,17 @@ export const id_ID: LanguageTranslation = { | ||||
|             none: 'Tidak ada', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Gagal menyalin', | ||||
|                 description: 'Clipboard tidak didukung', | ||||
|             }, | ||||
|             failed: { | ||||
|                 title: 'Gagal menyalin', | ||||
|                 description: 'Ada yang salah. Silakan coba lagi.', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         theme: { | ||||
|             system: 'Sistem', | ||||
|             light: 'Terang', | ||||
| @@ -91,7 +104,6 @@ export const id_ID: LanguageTranslation = { | ||||
|  | ||||
|         last_saved: 'Terakhir disimpan', | ||||
|         saved: 'Tersimpan', | ||||
|         diagrams: 'Diagram', | ||||
|         loading_diagram: 'Memuat diagram...', | ||||
|         deselect_all: 'Batalkan Semua', | ||||
|         select_all: 'Pilih Semua', | ||||
| @@ -112,6 +124,12 @@ export const id_ID: LanguageTranslation = { | ||||
|                 add_table: 'Tambah Tabel', | ||||
|                 filter: 'Saring', | ||||
|                 collapse: 'Lipat Semua', | ||||
|                 // TODO: Translate | ||||
|                 clear: 'Clear Filter', | ||||
|                 no_results: 'No tables found matching your filter.', | ||||
|                 // TODO: Translate | ||||
|                 show_list: 'Show Table List', | ||||
|                 show_dbml: 'Show DBML Editor', | ||||
|  | ||||
|                 table: { | ||||
|                     fields: 'Kolom', | ||||
| @@ -335,30 +353,39 @@ export const id_ID: LanguageTranslation = { | ||||
|             confirm: 'Tentu saja!', | ||||
|         }, | ||||
|  | ||||
|         // TODO: Translate | ||||
|         export_diagram_dialog: { | ||||
|             title: 'Export Diagram', | ||||
|             description: 'Choose the format for export:', | ||||
|             title: 'Ekspor Diagram', | ||||
|             description: 'Pilih format untuk ekspor:', | ||||
|             format_json: 'JSON', | ||||
|             cancel: 'Cancel', | ||||
|             export: 'Export', | ||||
|             cancel: 'Batal', | ||||
|             export: 'Ekspor', | ||||
|             error: { | ||||
|                 title: 'Error exporting diagram', | ||||
|                 title: 'Error ekspor diagram', | ||||
|                 description: | ||||
|                     'Something went wrong. Need help? chartdb.io@gmail.com', | ||||
|                     'Sesuatu yang salah. Butuh bantuan? chartdb.io@gmail.com', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         // TODO: Translate | ||||
|         import_diagram_dialog: { | ||||
|             title: 'Import Diagram', | ||||
|             description: 'Paste the diagram JSON below:', | ||||
|             cancel: 'Cancel', | ||||
|             import: 'Import', | ||||
|             title: 'Impor Diagram', | ||||
|             description: 'Tempel diagram JSON di bawah:', | ||||
|             cancel: 'Batal', | ||||
|             import: 'Impor', | ||||
|             error: { | ||||
|                 title: 'Error importing diagram', | ||||
|                 title: 'Error impor diagram', | ||||
|                 description: | ||||
|                     'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com', | ||||
|                     'Diagram JSON tidak valid. Silakan cek JSON dan coba lagi. Butuh bantuan? chartdb.io@gmail.com', | ||||
|             }, | ||||
|         }, | ||||
|         // TODO: Translate | ||||
|         import_dbml_dialog: { | ||||
|             title: 'Import DBML', | ||||
|             description: 'Import a database schema from DBML format.', | ||||
|             import: 'Import', | ||||
|             cancel: 'Cancel', | ||||
|             error: { | ||||
|                 title: 'Error', | ||||
|                 description: 'Failed to parse DBML. Please check the syntax.', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
| @@ -377,16 +404,14 @@ export const id_ID: LanguageTranslation = { | ||||
|         table_node_context_menu: { | ||||
|             edit_table: 'Ubah Tabel', | ||||
|             delete_table: 'Hapus Tabel', | ||||
|             // TODO: Translate | ||||
|             duplicate_table: 'Duplicate Table', | ||||
|             duplicate_table: 'Duplikat Tabel', | ||||
|             add_relationship: 'Add Relationship', // TODO: Translate | ||||
|         }, | ||||
|  | ||||
|         // TODO: Translate | ||||
|         snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})', | ||||
|         snap_to_grid_tooltip: 'Snap ke Kisi (Tahan {{key}})', | ||||
|  | ||||
|         // TODO: Translate | ||||
|         tool_tips: { | ||||
|             double_click_to_edit: 'Double-click to edit', | ||||
|             double_click_to_edit: 'Klik ganda untuk mengedit', | ||||
|         }, | ||||
|  | ||||
|         language_select: { | ||||
|   | ||||
| @@ -8,7 +8,7 @@ export const ja: LanguageTranslation = { | ||||
|                 new: '新規', | ||||
|                 open: '開く', | ||||
|                 save: '保存', | ||||
|                 import_database: 'データベースをインポート', | ||||
|                 import: 'データベースをインポート', | ||||
|                 export_sql: 'SQLをエクスポート', | ||||
|                 export_as: '形式を指定してエクスポート', | ||||
|                 delete_diagram: 'ダイアグラムを削除', | ||||
| @@ -31,6 +31,9 @@ export const ja: LanguageTranslation = { | ||||
|                 // TODO: Translate | ||||
|                 show_dependencies: 'Show Dependencies', | ||||
|                 hide_dependencies: 'Hide Dependencies', | ||||
|                 // TODO: Translate | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             // TODO: Translate | ||||
|             share: { | ||||
| @@ -79,6 +82,18 @@ export const ja: LanguageTranslation = { | ||||
|             none: 'なし', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'コピー失敗', | ||||
|                 description: 'クリップボードがサポートされていません', | ||||
|             }, | ||||
|             failed: { | ||||
|                 title: 'コピー失敗', | ||||
|                 description: | ||||
|                     '何かがうまくいきませんでした。もう一度お試しください。', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         theme: { | ||||
|             system: 'システム', | ||||
|             light: 'ライト', | ||||
| @@ -92,7 +107,6 @@ export const ja: LanguageTranslation = { | ||||
|  | ||||
|         last_saved: '最後に保存された', | ||||
|         saved: '保存されました', | ||||
|         diagrams: 'ダイアグラム', | ||||
|         loading_diagram: 'ダイアグラムを読み込み中...', | ||||
|         deselect_all: 'すべての選択を解除', | ||||
|         select_all: 'すべてを選択', | ||||
| @@ -114,6 +128,12 @@ export const ja: LanguageTranslation = { | ||||
|                 add_table: 'テーブルを追加', | ||||
|                 filter: 'フィルタ', | ||||
|                 collapse: 'すべて折りたたむ', | ||||
|                 // TODO: Translate | ||||
|                 clear: 'Clear Filter', | ||||
|                 no_results: 'No tables found matching your filter.', | ||||
|                 // TODO: Translate | ||||
|                 show_list: 'Show Table List', | ||||
|                 show_dbml: 'Show DBML Editor', | ||||
|  | ||||
|                 table: { | ||||
|                     fields: 'フィールド', | ||||
| @@ -366,6 +386,17 @@ export const ja: LanguageTranslation = { | ||||
|                     'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com', | ||||
|             }, | ||||
|         }, | ||||
|         // TODO: Translate | ||||
|         import_dbml_dialog: { | ||||
|             title: 'Import DBML', | ||||
|             description: 'Import a database schema from DBML format.', | ||||
|             import: 'Import', | ||||
|             cancel: 'Cancel', | ||||
|             error: { | ||||
|                 title: 'Error', | ||||
|                 description: 'Failed to parse DBML. Please check the syntax.', | ||||
|             }, | ||||
|         }, | ||||
|         relationship_type: { | ||||
|             one_to_one: '1対1', | ||||
|             one_to_many: '1対多', | ||||
| @@ -382,6 +413,7 @@ export const ja: LanguageTranslation = { | ||||
|             edit_table: 'テーブルを編集', | ||||
|             duplicate_table: 'Duplicate Table', // TODO: Translate | ||||
|             delete_table: 'テーブルを削除', | ||||
|             add_relationship: 'Add Relationship', // TODO: Translate | ||||
|         }, | ||||
|  | ||||
|         // TODO: Add translations | ||||
|   | ||||