mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-10-31 12:03:51 +00:00 
			
		
		
		
	Compare commits
	
		
			111 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 1ce265781b | ||
|  | 60c5675cbf | ||
|  | 66b086378c | ||
|  | abd2a6ccbe | ||
|  | 459c5f1ce3 | ||
|  | 44be48ff3a | ||
|  | ad8e34483f | ||
|  | 215d57979d | ||
|  | ec3719ebce | ||
|  | 0a5874a69b | ||
|  | 7e0fdd1595 | ||
|  | 2531a7023f | ||
|  | 73daf0df21 | ||
|  | c77c983989 | ||
|  | 0aaa451479 | ||
|  | b697e26170 | ||
|  | 04d91c67b1 | ||
|  | d0dee84970 | ||
|  | b4ccfcdcde | ||
|  | 1759b0b9f2 | ||
|  | ab4845c772 | ||
|  | 0545b41140 | ||
|  | 4520f8b1f7 | ||
|  | 712bdf5b95 | ||
|  | d7c9536272 | ||
|  | 815a52f192 | ||
|  | f1a4298362 | ||
|  | b8f2141bd2 | ||
|  | eaebe34768 | ||
|  | 0d623a86b1 | ||
|  | 19fd94c6bd | ||
|  | 0da3caeeac | ||
|  | cb2ba66233 | ||
|  | 8a2267281b | ||
|  | 41ba251377 | ||
|  | e9c5442d9d | ||
|  | 4f1d3295c0 | ||
|  | 5936500ca0 | ||
|  | 43fc1d7fc2 | ||
|  | 8dfa7cc62e | ||
|  | 23e93bfd01 | ||
|  | 16f9f4671e | ||
|  | 0c300e5e72 | ||
|  | b9a1e78b53 | ||
|  | 337f7cdab4 | ||
|  | 1b0390f0b7 | ||
|  | bc52933b58 | ||
|  | 2fdad2344c | ||
|  | 0c7eaa2df2 | ||
|  | a5f8e56b3c | ||
|  | 8ffde62c1a | ||
|  | 39247b77a2 | ||
|  | 984b2aeee2 | ||
|  | eed104be5b | ||
|  | 00bd535b3c | ||
|  | 18e914242f | ||
|  | e68837a34a | ||
|  | b30162d98b | ||
|  | dba372d25a | ||
|  | 2eb48e75d3 | ||
|  | 867903cd5f | ||
|  | 8aeb1df0ad | ||
|  | 6bea827293 | ||
|  | a119854da7 | ||
|  | bfbfd7b843 | ||
|  | 0ca7008735 | ||
|  | 4bc71c52ff | ||
|  | 8f27f10dec | ||
|  | a93ec2cab9 | ||
|  | 386e40a0bf | ||
|  | bda150d4b6 | ||
|  | 87836e53d1 | ||
|  | 7e0483f1a5 | ||
|  | 309ee9cb0f | ||
|  | 79b885502e | ||
|  | 745bdee86d | ||
|  | 08eb9cc55f | ||
|  | 778f85d492 | ||
|  | fb92be7d3e | ||
|  | 6df588f40e | ||
|  | b46ed58dff | ||
|  | 0d9f57a9c9 | ||
|  | b7dbe54c83 | ||
|  | 43d1dfff71 | ||
|  | 9949a46ee3 | ||
|  | dfbcf05b2f | ||
|  | f56fab9876 | ||
|  | c9ea7da092 | ||
|  | 22d46e1e90 | ||
|  | 6af94afc56 | ||
|  | f7f92903de | ||
|  | b35e17526b | ||
|  | bf32c08d37 | ||
|  | 5d337409d6 | ||
|  | 67f5ac303e | ||
|  | 578546a171 | ||
|  | aa0b629a3e | ||
|  | 69beaa0a83 | ||
|  | 4fcc49d49a | ||
|  | d15985e399 | ||
|  | d429128e65 | ||
|  | 2fce8326b6 | ||
|  | 433c68a33d | ||
|  | 58acb65f12 | ||
|  | 7978955819 | ||
|  | c6118e0cdb | ||
|  | 7d063b905f | ||
|  | e0ff198c3f | ||
|  | 8b86e1c229 | ||
|  | 24be28a662 | ||
|  | c6788b4917 | 
							
								
								
									
										5
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -24,4 +24,7 @@ jobs: | ||||
|         run: npm run lint | ||||
|  | ||||
|       - name: Build | ||||
|         run: npm run build | ||||
|         run: npm run build | ||||
|  | ||||
|       - name: Run tests | ||||
|         run: npm run test:ci | ||||
							
								
								
									
										2
									
								
								.github/workflows/cla.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/cla.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -7,7 +7,7 @@ on: | ||||
|  | ||||
| permissions: | ||||
|   actions: write | ||||
|   contents: write # this can be 'read' if the signatures are in remote repository | ||||
|   contents: read | ||||
|   pull-requests: write | ||||
|   statuses: write | ||||
|  | ||||
|   | ||||
							
								
								
									
										136
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										136
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,5 +1,141 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## [1.15.1](https://github.com/chartdb/chartdb/compare/v1.15.0...v1.15.1) (2025-08-27) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * add actions menu to diagram list + add duplicate diagram ([#876](https://github.com/chartdb/chartdb/issues/876)) ([abd2a6c](https://github.com/chartdb/chartdb/commit/abd2a6ccbe1aa63db44ec28b3eff525cc5d3f8b0)) | ||||
| * **custom-types:** Make schema optional ([#866](https://github.com/chartdb/chartdb/issues/866)) ([60c5675](https://github.com/chartdb/chartdb/commit/60c5675cbfe205859d2d0c9848d8345a0a854671)) | ||||
| * handle quoted identifiers with special characters in SQL import/export and DBML generation ([#877](https://github.com/chartdb/chartdb/issues/877)) ([66b0863](https://github.com/chartdb/chartdb/commit/66b086378cd63347acab5fc7f13db7db4feaa872)) | ||||
|  | ||||
| ## [1.15.0](https://github.com/chartdb/chartdb/compare/v1.14.0...v1.15.0) (2025-08-26) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * add auto increment support for fields with database-specific export ([#851](https://github.com/chartdb/chartdb/issues/851)) ([c77c983](https://github.com/chartdb/chartdb/commit/c77c983989ae38a6b1139dd9015f4f3178d4e103)) | ||||
| * **filter:** filter tables by areas ([#836](https://github.com/chartdb/chartdb/issues/836)) ([e9c5442](https://github.com/chartdb/chartdb/commit/e9c5442d9df2beadad78187da3363bb6406636c4)) | ||||
| * include foreign keys inline in SQLite CREATE TABLE statements ([#833](https://github.com/chartdb/chartdb/issues/833)) ([43fc1d7](https://github.com/chartdb/chartdb/commit/43fc1d7fc26876b22c61405f6c3df89fc66b7992)) | ||||
| * **postgres:** add support hash index types ([#812](https://github.com/chartdb/chartdb/issues/812)) ([0d623a8](https://github.com/chartdb/chartdb/commit/0d623a86b1cb7cbd223e10ad23d09fc0e106c006)) | ||||
| * support create views ([#868](https://github.com/chartdb/chartdb/issues/868)) ([0a5874a](https://github.com/chartdb/chartdb/commit/0a5874a69b6323145430c1fb4e3482ac7da4916c)) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * area filter logic ([#861](https://github.com/chartdb/chartdb/issues/861)) ([73daf0d](https://github.com/chartdb/chartdb/commit/73daf0df2142a29c2eeebe60b43198bcca869026)) | ||||
| * **area filter:** fix dragging tables over filtered areas ([#842](https://github.com/chartdb/chartdb/issues/842)) ([19fd94c](https://github.com/chartdb/chartdb/commit/19fd94c6bde3a9ec749cd1ccacbedb6abc96d037)) | ||||
| * **canvas:** delete table + area together bug ([#859](https://github.com/chartdb/chartdb/issues/859)) ([b697e26](https://github.com/chartdb/chartdb/commit/b697e26170da95dcb427ff6907b6f663c98ba59f)) | ||||
| * **cla:** Harden action ([#867](https://github.com/chartdb/chartdb/issues/867)) ([ad8e344](https://github.com/chartdb/chartdb/commit/ad8e34483fdf4226de76c9e7768bc2ba9bf154de)) | ||||
| * DBML export error with multi-line table comments for SQL Server ([#852](https://github.com/chartdb/chartdb/issues/852)) ([0545b41](https://github.com/chartdb/chartdb/commit/0545b411407b2449220d10981a04c3e368a90ca3)) | ||||
| * filter to default schema on load new diagram ([#849](https://github.com/chartdb/chartdb/issues/849)) ([712bdf5](https://github.com/chartdb/chartdb/commit/712bdf5b958919d940c4f2a1c3b7c7e969990f02)) | ||||
| * **filter:** filter toggle issues with no schemas dbs ([#856](https://github.com/chartdb/chartdb/issues/856)) ([d0dee84](https://github.com/chartdb/chartdb/commit/d0dee849702161d979b4f589a7e6579fbaade22d)) | ||||
| * **filters:** refactor diagram filters - remove schema filter ([#832](https://github.com/chartdb/chartdb/issues/832)) ([4f1d329](https://github.com/chartdb/chartdb/commit/4f1d3295c09782ab46d82ce21b662032aa094f22)) | ||||
| * for sqlite import - add more types & include type parameters ([#834](https://github.com/chartdb/chartdb/issues/834)) ([5936500](https://github.com/chartdb/chartdb/commit/5936500ca00a57b3f161616264c26152a13c36d2)) | ||||
| * improve creating view to table dependency ([#874](https://github.com/chartdb/chartdb/issues/874)) ([44be48f](https://github.com/chartdb/chartdb/commit/44be48ff3ad1361279331c17364090b13af471a1)) | ||||
| * initially show filter when filter active ([#853](https://github.com/chartdb/chartdb/issues/853)) ([ab4845c](https://github.com/chartdb/chartdb/commit/ab4845c7728e6e0b2d852f8005921fd90630eef9)) | ||||
| * **menu:** clear file menu ([#843](https://github.com/chartdb/chartdb/issues/843)) ([eaebe34](https://github.com/chartdb/chartdb/commit/eaebe3476824af779214a354b3e991923a22f195)) | ||||
| * merge relationship & dependency sections to ref section ([#870](https://github.com/chartdb/chartdb/issues/870)) ([ec3719e](https://github.com/chartdb/chartdb/commit/ec3719ebce4664b2aa6e3322fb3337e72bc21015)) | ||||
| * move dbml into sections menu ([#862](https://github.com/chartdb/chartdb/issues/862)) ([2531a70](https://github.com/chartdb/chartdb/commit/2531a7023f36ef29e67c0da6bca4fd0346b18a51)) | ||||
| * open filter by default ([#863](https://github.com/chartdb/chartdb/issues/863)) ([7e0fdd1](https://github.com/chartdb/chartdb/commit/7e0fdd1595bffe29e769d29602d04f42edfe417e)) | ||||
| * preserve composite primary key constraint names across import/export workflows ([#869](https://github.com/chartdb/chartdb/issues/869)) ([215d579](https://github.com/chartdb/chartdb/commit/215d57979df2e91fa61988acff590daad2f4e771)) | ||||
| * prevent false change detection in DBML editor by stripping public schema on import ([#858](https://github.com/chartdb/chartdb/issues/858)) ([0aaa451](https://github.com/chartdb/chartdb/commit/0aaa451479911d047e4cc83f063afa68a122ba9b)) | ||||
| * remove unnecessary space ([#845](https://github.com/chartdb/chartdb/issues/845)) ([f1a4298](https://github.com/chartdb/chartdb/commit/f1a429836221aacdda73b91665bf33ffb011164c)) | ||||
| * reorder with areas ([#846](https://github.com/chartdb/chartdb/issues/846)) ([d7c9536](https://github.com/chartdb/chartdb/commit/d7c9536272cf1d42104b7064ea448d128d091a20)) | ||||
| * **select-box:** fix select box issue in dialog ([#840](https://github.com/chartdb/chartdb/issues/840)) ([cb2ba66](https://github.com/chartdb/chartdb/commit/cb2ba66233c8c04e2d963cf2d210499d8512a268)) | ||||
| * set default filter only if has more than 1 schemas ([#855](https://github.com/chartdb/chartdb/issues/855)) ([b4ccfcd](https://github.com/chartdb/chartdb/commit/b4ccfcdcde2f3565b0d3bbc46fa1715feb6cd925)) | ||||
| * show default schema first ([#854](https://github.com/chartdb/chartdb/issues/854)) ([1759b0b](https://github.com/chartdb/chartdb/commit/1759b0b9f271ed25f7c71f26c344e3f1d97bc5fb)) | ||||
| * **sidebar:** add titles to sidebar ([#844](https://github.com/chartdb/chartdb/issues/844)) ([b8f2141](https://github.com/chartdb/chartdb/commit/b8f2141bd2e67272030896fb4009a7925f9f09e4)) | ||||
| * **sql-import:** fix SQL Server foreign key parsing for tables without schema prefix ([#857](https://github.com/chartdb/chartdb/issues/857)) ([04d91c6](https://github.com/chartdb/chartdb/commit/04d91c67b1075e94948f75186878e633df7abbca)) | ||||
| * **table colors:** switch to default table color ([#841](https://github.com/chartdb/chartdb/issues/841)) ([0da3cae](https://github.com/chartdb/chartdb/commit/0da3caeeac37926dd22f38d98423611f39c0412a)) | ||||
| * update filter on adding table ([#838](https://github.com/chartdb/chartdb/issues/838)) ([41ba251](https://github.com/chartdb/chartdb/commit/41ba25137789dda25266178cd7c96ecbb37e62a4)) | ||||
|  | ||||
| ## [1.14.0](https://github.com/chartdb/chartdb/compare/v1.13.2...v1.14.0) (2025-08-04) | ||||
|  | ||||
|  | ||||
| ### Features | ||||
|  | ||||
| * add floating "Show All" button when tables are out of view ([#787](https://github.com/chartdb/chartdb/issues/787)) ([bda150d](https://github.com/chartdb/chartdb/commit/bda150d4b6d6fb90beb423efba69349d21a037a5)) | ||||
| * add table selection for large database imports ([#776](https://github.com/chartdb/chartdb/issues/776)) ([0d9f57a](https://github.com/chartdb/chartdb/commit/0d9f57a9c969a67e350d6bf25e07c3a9ef5bba39)) | ||||
| * **canvas:** Add filter tables on canvas ([#774](https://github.com/chartdb/chartdb/issues/774)) ([dfbcf05](https://github.com/chartdb/chartdb/commit/dfbcf05b2f595f5b7b77dd61abf77e6e07acaf8f)) | ||||
| * **custom-types:** add highlight fields option for custom types ([#726](https://github.com/chartdb/chartdb/issues/726)) ([7e0483f](https://github.com/chartdb/chartdb/commit/7e0483f1a5512a6a737baf61caf7513e043f2e96)) | ||||
| * **datatypes:** Add decimal / numeric attribute support + organize field row ([#715](https://github.com/chartdb/chartdb/issues/715)) ([778f85d](https://github.com/chartdb/chartdb/commit/778f85d49214232a39710e47bb5d4ec41b75d427)) | ||||
| * **dbml:** Edit Diagram Directly from DBML ([#819](https://github.com/chartdb/chartdb/issues/819)) ([1b0390f](https://github.com/chartdb/chartdb/commit/1b0390f0b7652fe415540b7942cf53ec87143f08)) | ||||
| * **default value:** add default value option to table field settings ([#770](https://github.com/chartdb/chartdb/issues/770)) ([c9ea7da](https://github.com/chartdb/chartdb/commit/c9ea7da0923ff991cb936235674d9a52b8186137)) | ||||
| * enhance primary key and unique field handling logic ([#817](https://github.com/chartdb/chartdb/issues/817)) ([39247b7](https://github.com/chartdb/chartdb/commit/39247b77a299caa4f29ea434af3028155c6d37ed)) | ||||
| * implement area grouping with parent-child relationships ([#762](https://github.com/chartdb/chartdb/issues/762)) ([b35e175](https://github.com/chartdb/chartdb/commit/b35e17526b3c9b918928ae5f3f89711ea7b2529c)) | ||||
| * **schema:** support create new schema ([#801](https://github.com/chartdb/chartdb/issues/801)) ([867903c](https://github.com/chartdb/chartdb/commit/867903cd5f24d96ce1fe718dc9b562e2f2b75276)) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * add open and create diagram to side menu ([#757](https://github.com/chartdb/chartdb/issues/757)) ([67f5ac3](https://github.com/chartdb/chartdb/commit/67f5ac303ebf5ada97d5c80fb08a2815ca205a91)) | ||||
| * add PostgreSQL tests and fix parsing SQL ([#760](https://github.com/chartdb/chartdb/issues/760)) ([5d33740](https://github.com/chartdb/chartdb/commit/5d337409d64d1078b538350016982a98e684c06c)) | ||||
| * area resizers size ([#830](https://github.com/chartdb/chartdb/issues/830)) ([23e93bf](https://github.com/chartdb/chartdb/commit/23e93bfd01d741dd3d11aa5c479cef97e1a86fa6)) | ||||
| * **area:** redo/undo after dragging an area with tables ([#767](https://github.com/chartdb/chartdb/issues/767)) ([6af94af](https://github.com/chartdb/chartdb/commit/6af94afc56cf8987b8fc9e3f0a9bfa966de35408)) | ||||
| * **canvas filter:** improve scroller on canvas filter ([#799](https://github.com/chartdb/chartdb/issues/799)) ([6bea827](https://github.com/chartdb/chartdb/commit/6bea82729362a8c7b73dc089ddd9e52bae176aa2)) | ||||
| * **canvas:** fix filter eye button ([#780](https://github.com/chartdb/chartdb/issues/780)) ([b7dbe54](https://github.com/chartdb/chartdb/commit/b7dbe54c83c75cfe3c556f7a162055dcfe2de23d)) | ||||
| * clone of custom types ([#804](https://github.com/chartdb/chartdb/issues/804)) ([b30162d](https://github.com/chartdb/chartdb/commit/b30162d98bc659a61aae023cdeaead4ce25c7ae9)) | ||||
| * **cockroachdb:** support schema creation for cockroachdb ([#803](https://github.com/chartdb/chartdb/issues/803)) ([dba372d](https://github.com/chartdb/chartdb/commit/dba372d25a8c642baf8600d05aa154882729d446)) | ||||
| * **dbml actions:** set dbml tooltips side ([#798](https://github.com/chartdb/chartdb/issues/798)) ([a119854](https://github.com/chartdb/chartdb/commit/a119854da7c935eb595984ea9398e04136ce60c4)) | ||||
| * **dbml editor:** move tooltips button to be on the right ([#797](https://github.com/chartdb/chartdb/issues/797)) ([bfbfd7b](https://github.com/chartdb/chartdb/commit/bfbfd7b843f96c894b1966ad95393b866c927466)) | ||||
| * **dbml export:** fix handle tables with same name under different schemas ([#807](https://github.com/chartdb/chartdb/issues/807)) ([18e9142](https://github.com/chartdb/chartdb/commit/18e914242faccd6376fe5a7cd5a4478667f065ee)) | ||||
| * **dbml export:** handle tables with same name under different schemas ([#806](https://github.com/chartdb/chartdb/issues/806)) ([e68837a](https://github.com/chartdb/chartdb/commit/e68837a34aa635fb6fc02c7f1289495e5c448242)) | ||||
| * **dbml field comments:** support export field comments in dbml ([#796](https://github.com/chartdb/chartdb/issues/796)) ([0ca7008](https://github.com/chartdb/chartdb/commit/0ca700873577bbfbf1dd3f8088c258fc89b10c53)) | ||||
| * **dbml import:** fix dbml import types + schemas ([#808](https://github.com/chartdb/chartdb/issues/808)) ([00bd535](https://github.com/chartdb/chartdb/commit/00bd535b3c62d26d25a6276d52beb10e26afad76)) | ||||
| * **dbml-export:** merge field attributes into single brackets and fix schema syntax ([#790](https://github.com/chartdb/chartdb/issues/790)) ([309ee9c](https://github.com/chartdb/chartdb/commit/309ee9cb0ff1f5a68ed183e3919e1a11a8410909)) | ||||
| * **dbml-import:** handle unsupported DBML features and add comprehensive tests ([#766](https://github.com/chartdb/chartdb/issues/766)) ([22d46e1](https://github.com/chartdb/chartdb/commit/22d46e1e90729730cc25dd6961bfe8c3d2ae0c98)) | ||||
| * **dbml:** dbml indentation ([#829](https://github.com/chartdb/chartdb/issues/829)) ([16f9f46](https://github.com/chartdb/chartdb/commit/16f9f4671e011eb66ba9594bed47570eda3eed66)) | ||||
| * **dbml:** dbml note syntax ([#826](https://github.com/chartdb/chartdb/issues/826)) ([337f7cd](https://github.com/chartdb/chartdb/commit/337f7cdab4759d15cb4d25a8c0e9394e99ba33d4)) | ||||
| * **dbml:** fix dbml output format ([#815](https://github.com/chartdb/chartdb/issues/815)) ([eed104b](https://github.com/chartdb/chartdb/commit/eed104be5ba2b7d9940ffac38e7877722ad764fc)) | ||||
| * **dbml:** fix schemas with same table names ([#828](https://github.com/chartdb/chartdb/issues/828)) ([0c300e5](https://github.com/chartdb/chartdb/commit/0c300e5e72cc5ff22cac42f8dbaed167061157c6)) | ||||
| * **dbml:** import dbml notes (table + fields) ([#827](https://github.com/chartdb/chartdb/issues/827)) ([b9a1e78](https://github.com/chartdb/chartdb/commit/b9a1e78b53c932c0b1a12ee38b62494a5c2f9348)) | ||||
| * **dbml:** support multiple relationships on same field in inline DBML ([#822](https://github.com/chartdb/chartdb/issues/822)) ([a5f8e56](https://github.com/chartdb/chartdb/commit/a5f8e56b3ca97b851b6953481644d3a3ff7ce882)) | ||||
| * **dbml:** support spaces in names ([#794](https://github.com/chartdb/chartdb/issues/794)) ([8f27f10](https://github.com/chartdb/chartdb/commit/8f27f10dec96af400dc2c12a30b22b3a346803a9)) | ||||
| * fix hotkeys on form elements ([#778](https://github.com/chartdb/chartdb/issues/778)) ([43d1dff](https://github.com/chartdb/chartdb/commit/43d1dfff71f2b960358a79b0112b78d11df91fb7)) | ||||
| * fix screen freeze after schema select ([#800](https://github.com/chartdb/chartdb/issues/800)) ([8aeb1df](https://github.com/chartdb/chartdb/commit/8aeb1df0ad353c49e91243453f24bfa5921a89ab)) | ||||
| * **i18n:** add Croatian (hr) language support ([#802](https://github.com/chartdb/chartdb/issues/802)) ([2eb48e7](https://github.com/chartdb/chartdb/commit/2eb48e75d303d622f51327d22502a6f78e7fb32d)) | ||||
| * improve SQL export formatting and add schema-aware FK grouping ([#783](https://github.com/chartdb/chartdb/issues/783)) ([6df588f](https://github.com/chartdb/chartdb/commit/6df588f40e6e7066da6125413b94466429d48767)) | ||||
| * lost in canvas button animation ([#793](https://github.com/chartdb/chartdb/issues/793)) ([a93ec2c](https://github.com/chartdb/chartdb/commit/a93ec2cab906d0e4431d8d1668adcf2dbfc3c80f)) | ||||
| * **readonly:** fix zoom out on readonly ([#818](https://github.com/chartdb/chartdb/issues/818)) ([8ffde62](https://github.com/chartdb/chartdb/commit/8ffde62c1a00893c4bf6b4dd39068df530375416)) | ||||
| * remove error lag after autofix ([#764](https://github.com/chartdb/chartdb/issues/764)) ([bf32c08](https://github.com/chartdb/chartdb/commit/bf32c08d37c02ee6d7946a41633bb97b2271fcb7)) | ||||
| * remove unnecessary import ([#791](https://github.com/chartdb/chartdb/issues/791)) ([87836e5](https://github.com/chartdb/chartdb/commit/87836e53d145b825f9c4f80abca72f418df50e6c)) | ||||
| * **scroll:** disable scroll x behavior ([#795](https://github.com/chartdb/chartdb/issues/795)) ([4bc71c5](https://github.com/chartdb/chartdb/commit/4bc71c52ff5c462800d8530b72a5aadb7d7f85ed)) | ||||
| * set focus on filter search ([#775](https://github.com/chartdb/chartdb/issues/775)) ([9949a46](https://github.com/chartdb/chartdb/commit/9949a46ee3ba7f46a2ea7f2c0d7101cc9336df4f)) | ||||
| * solve issue with multiple render of tables ([#823](https://github.com/chartdb/chartdb/issues/823)) ([0c7eaa2](https://github.com/chartdb/chartdb/commit/0c7eaa2df20cfb6994b7e6251c760a2d4581c879)) | ||||
| * **sql-export:** escape newlines and quotes in multi-line comments ([#765](https://github.com/chartdb/chartdb/issues/765)) ([f7f9290](https://github.com/chartdb/chartdb/commit/f7f92903def84a94ac0c66f625f96a6681383945)) | ||||
| * **sql-server:** improvment for sql-server import via sql script ([#789](https://github.com/chartdb/chartdb/issues/789)) ([79b8855](https://github.com/chartdb/chartdb/commit/79b885502e3385e996a52093a3ccd5f6e469993a)) | ||||
| * **table-node:** fix comment icon on field ([#786](https://github.com/chartdb/chartdb/issues/786)) ([745bdee](https://github.com/chartdb/chartdb/commit/745bdee86d07f1e9c3a2d24237c48c25b9a8eeea)) | ||||
| * **table-node:** improve field spacing ([#785](https://github.com/chartdb/chartdb/issues/785)) ([08eb9cc](https://github.com/chartdb/chartdb/commit/08eb9cc55f0077f53afea6f9ce720341e1a583c2)) | ||||
| * **table-select:** add loading indication for import ([#782](https://github.com/chartdb/chartdb/issues/782)) ([b46ed58](https://github.com/chartdb/chartdb/commit/b46ed58dff1ec74579fb1544dba46b0f77730c52)) | ||||
| * **ui:** reduce spacing between primary key icon and short field types ([#816](https://github.com/chartdb/chartdb/issues/816)) ([984b2ae](https://github.com/chartdb/chartdb/commit/984b2aeee22c43cb9bda77df2c22087973079af4)) | ||||
| * update MariaDB database import smart query ([#792](https://github.com/chartdb/chartdb/issues/792)) ([386e40a](https://github.com/chartdb/chartdb/commit/386e40a0bf93d9aef1486bb1e729d8f485e675eb)) | ||||
| * update multiple schemas toast to require user action ([#771](https://github.com/chartdb/chartdb/issues/771)) ([f56fab9](https://github.com/chartdb/chartdb/commit/f56fab9876fb9fc46c6c708231324a90d8a7851d)) | ||||
| * update relationship when table width changes via expand/shrink ([#825](https://github.com/chartdb/chartdb/issues/825)) ([bc52933](https://github.com/chartdb/chartdb/commit/bc52933b58bfe6bc73779d9401128254cbf497d5)) | ||||
|  | ||||
| ## [1.13.2](https://github.com/chartdb/chartdb/compare/v1.13.1...v1.13.2) (2025-07-06) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * add DISABLE_ANALYTICS flag to opt-out of Fathom analytics ([#750](https://github.com/chartdb/chartdb/issues/750)) ([aa0b629](https://github.com/chartdb/chartdb/commit/aa0b629a3eaf8e8b60473ea3f28f769270c7714c)) | ||||
|  | ||||
| ## [1.13.1](https://github.com/chartdb/chartdb/compare/v1.13.0...v1.13.1) (2025-07-04) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * **custom_types:** fix display custom types in select box ([#737](https://github.com/chartdb/chartdb/issues/737)) ([24be28a](https://github.com/chartdb/chartdb/commit/24be28a662c48fc5bc62e76446b9669d83d7d3e0)) | ||||
| * **dbml-editor:** for some cases that the dbml had issues ([#739](https://github.com/chartdb/chartdb/issues/739)) ([e0ff198](https://github.com/chartdb/chartdb/commit/e0ff198c3fd416498dac5680bb323ec88c54b65c)) | ||||
| * **dbml:** Filter duplicate tables at diagram level before export dbml ([#746](https://github.com/chartdb/chartdb/issues/746)) ([d429128](https://github.com/chartdb/chartdb/commit/d429128e65aa28c500eac2487356e4869506e948)) | ||||
| * **export-sql:** conditionally show generic option and reorder by diagram type ([#708](https://github.com/chartdb/chartdb/issues/708)) ([c6118e0](https://github.com/chartdb/chartdb/commit/c6118e0cdb0e5caaf73447d33db2fde1a98efe60)) | ||||
| * general performance improvements on canvas ([#751](https://github.com/chartdb/chartdb/issues/751)) ([4fcc49d](https://github.com/chartdb/chartdb/commit/4fcc49d49a76a4b886ffd6cf0b40cf2fc49952ec)) | ||||
| * **import-database:** for custom types query to import supabase & timescale ([#745](https://github.com/chartdb/chartdb/issues/745)) ([2fce832](https://github.com/chartdb/chartdb/commit/2fce8326b67b751d38dd34f409fea574449d0298)) | ||||
| * **import-db:** fix mariadb import ([#740](https://github.com/chartdb/chartdb/issues/740)) ([7d063b9](https://github.com/chartdb/chartdb/commit/7d063b905f19f51501468bd0bd794a25cf65e1be)) | ||||
| * **performance:** improve storage provider performance ([#734](https://github.com/chartdb/chartdb/issues/734)) ([c6788b4](https://github.com/chartdb/chartdb/commit/c6788b49173d9cce23571daeb460285cb7cffb11)) | ||||
| * resolve unresponsive cursor and input glitches when editing field comments ([#749](https://github.com/chartdb/chartdb/issues/749)) ([d15985e](https://github.com/chartdb/chartdb/commit/d15985e3999a0cd54213b2fb08c55d48a1b8b3b2)) | ||||
| * **table name:** updates table name value when its updated from canvas/sidebar ([#716](https://github.com/chartdb/chartdb/issues/716)) ([8b86e1c](https://github.com/chartdb/chartdb/commit/8b86e1c22992aaadcce7ad5fc1d267c5a57a99f0)) | ||||
|  | ||||
| ## [1.13.0](https://github.com/chartdb/chartdb/compare/v1.12.0...v1.13.0) (2025-05-28) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,8 @@ 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 | ||||
| ARG VITE_HIDE_CHARTDB_CLOUD | ||||
| ARG VITE_DISABLE_ANALYTICS | ||||
|  | ||||
| WORKDIR /usr/src/app | ||||
|  | ||||
| @@ -16,7 +17,8 @@ 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  | ||||
|     echo "VITE_HIDE_CHARTDB_CLOUD=${VITE_HIDE_CHARTDB_CLOUD}" >> .env && \ | ||||
|     echo "VITE_DISABLE_ANALYTICS=${VITE_DISABLE_ANALYTICS}" >> .env | ||||
|  | ||||
| RUN npm run build | ||||
|  | ||||
|   | ||||
| @@ -125,6 +125,8 @@ docker run \ | ||||
|   -p 8080:80 chartdb | ||||
| ``` | ||||
|  | ||||
| > **Privacy Note:** ChartDB includes privacy-focused analytics via Fathom Analytics. You can disable this by adding `-e DISABLE_ANALYTICS=true` to the run command or `--build-arg VITE_DISABLE_ANALYTICS=true` when building. | ||||
|  | ||||
| > **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`. | ||||
|   | ||||
| @@ -10,11 +10,12 @@ server { | ||||
|  | ||||
|     location /config.js { | ||||
|         default_type application/javascript; | ||||
|         return 200 "window.env = {  | ||||
|         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\" | ||||
|             HIDE_CHARTDB_CLOUD: \"$HIDE_CHARTDB_CLOUD\", | ||||
|             DISABLE_ANALYTICS: \"$DISABLE_ANALYTICS\" | ||||
|         };"; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| #!/bin/sh | ||||
|  | ||||
| # Replace placeholders in nginx.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 | ||||
| envsubst '${OPENAI_API_KEY} ${OPENAI_API_ENDPOINT} ${LLM_MODEL_NAME} ${HIDE_CHARTDB_CLOUD} ${DISABLE_ANALYTICS}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf | ||||
|  | ||||
| # Start Nginx | ||||
| nginx -g "daemon off;" | ||||
|   | ||||
							
								
								
									
										28
									
								
								index.html
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								index.html
									
									
									
									
									
								
							| @@ -4,8 +4,9 @@ | ||||
|         <meta charset="UTF-8" /> | ||||
|         <link rel="icon" type="image/svg+xml" href="/favicon.ico" /> | ||||
|         <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
|         <meta name="robots" content="max-image-preview:large" /> | ||||
|         <meta name="robots" content="noindex, max-image-preview:large" /> | ||||
|         <title>ChartDB - Create & Visualize Database Schema Diagrams</title> | ||||
|         <link rel="canonical" href="https://chartdb.io" /> | ||||
|         <link rel="preconnect" href="https://fonts.googleapis.com" /> | ||||
|         <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> | ||||
|         <link | ||||
| @@ -13,11 +14,26 @@ | ||||
|             rel="stylesheet" | ||||
|         /> | ||||
|         <script src="/config.js"></script> | ||||
|         <script | ||||
|             src="https://cdn.usefathom.com/script.js" | ||||
|             data-site="PRHIVBNN" | ||||
|             defer | ||||
|         ></script> | ||||
|         <script> | ||||
|             // Load analytics only if not disabled | ||||
|             (function () { | ||||
|                 const disableAnalytics = | ||||
|                     (window.env && window.env.DISABLE_ANALYTICS === 'true') || | ||||
|                     (typeof process !== 'undefined' && | ||||
|                         process.env && | ||||
|                         process.env.VITE_DISABLE_ANALYTICS === 'true'); | ||||
|  | ||||
|                 if (!disableAnalytics) { | ||||
|                     const script = document.createElement('script'); | ||||
|                     script.src = 'https://cdn.usefathom.com/script.js'; | ||||
|                     script.setAttribute('data-site', 'PRHIVBNN'); | ||||
|                     script.setAttribute('data-canonical', 'false'); | ||||
|                     script.setAttribute('data-spa', 'auto'); | ||||
|                     script.defer = true; | ||||
|                     document.head.appendChild(script); | ||||
|                 } | ||||
|             })(); | ||||
|         </script> | ||||
|     </head> | ||||
|     <body> | ||||
|         <div id="root"></div> | ||||
|   | ||||
							
								
								
									
										1610
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1610
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										31
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										31
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|     "name": "chartdb", | ||||
|     "private": true, | ||||
|     "version": "1.13.0", | ||||
|     "version": "1.15.1", | ||||
|     "type": "module", | ||||
|     "scripts": { | ||||
|         "dev": "vite", | ||||
| @@ -9,7 +9,11 @@ | ||||
|         "lint": "eslint . --report-unused-disable-directives --max-warnings 0", | ||||
|         "lint:fix": "npm run lint -- --fix", | ||||
|         "preview": "vite preview", | ||||
|         "prepare": "husky" | ||||
|         "prepare": "husky", | ||||
|         "test": "vitest", | ||||
|         "test:ci": "vitest run --reporter=verbose --bail=1", | ||||
|         "test:ui": "vitest --ui", | ||||
|         "test:coverage": "vitest --coverage" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@ai-sdk/openai": "^0.0.51", | ||||
| @@ -22,24 +26,24 @@ | ||||
|         "@radix-ui/react-checkbox": "^1.1.1", | ||||
|         "@radix-ui/react-collapsible": "^1.1.0", | ||||
|         "@radix-ui/react-context-menu": "^2.2.1", | ||||
|         "@radix-ui/react-dialog": "^1.1.6", | ||||
|         "@radix-ui/react-dialog": "^1.1.14", | ||||
|         "@radix-ui/react-dropdown-menu": "^2.1.1", | ||||
|         "@radix-ui/react-hover-card": "^1.1.1", | ||||
|         "@radix-ui/react-icons": "^1.3.0", | ||||
|         "@radix-ui/react-icons": "^1.3.2", | ||||
|         "@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.2.0", | ||||
|         "@radix-ui/react-select": "^2.1.1", | ||||
|         "@radix-ui/react-separator": "^1.1.2", | ||||
|         "@radix-ui/react-slot": "^1.1.2", | ||||
|         "@radix-ui/react-separator": "^1.1.7", | ||||
|         "@radix-ui/react-slot": "^1.2.3", | ||||
|         "@radix-ui/react-tabs": "^1.1.0", | ||||
|         "@radix-ui/react-toast": "^1.2.1", | ||||
|         "@radix-ui/react-toggle": "^1.1.0", | ||||
|         "@radix-ui/react-toggle-group": "^1.1.0", | ||||
|         "@radix-ui/react-tooltip": "^1.1.8", | ||||
|         "@radix-ui/react-tooltip": "^1.2.7", | ||||
|         "@uidotdev/usehooks": "^2.4.1", | ||||
|         "@xyflow/react": "^12.3.1", | ||||
|         "@xyflow/react": "^12.8.2", | ||||
|         "ahooks": "^3.8.1", | ||||
|         "ai": "^3.3.14", | ||||
|         "class-variance-authority": "^0.7.1", | ||||
| @@ -50,8 +54,9 @@ | ||||
|         "html-to-image": "^1.11.11", | ||||
|         "i18next": "^23.14.0", | ||||
|         "i18next-browser-languagedetector": "^8.0.0", | ||||
|         "lucide-react": "^0.441.0", | ||||
|         "lucide-react": "^0.525.0", | ||||
|         "monaco-editor": "^0.52.0", | ||||
|         "motion": "^12.23.6", | ||||
|         "nanoid": "^5.0.7", | ||||
|         "node-sql-parser": "^5.3.2", | ||||
|         "react": "^18.3.1", | ||||
| @@ -73,12 +78,16 @@ | ||||
|         "@eslint/compat": "^1.2.4", | ||||
|         "@eslint/eslintrc": "^3.2.0", | ||||
|         "@eslint/js": "^9.16.0", | ||||
|         "@testing-library/jest-dom": "^6.6.3", | ||||
|         "@testing-library/react": "^16.3.0", | ||||
|         "@testing-library/user-event": "^14.6.1", | ||||
|         "@types/node": "^22.1.0", | ||||
|         "@types/react": "^18.3.3", | ||||
|         "@types/react-dom": "^18.3.0", | ||||
|         "@typescript-eslint/eslint-plugin": "^8.18.0", | ||||
|         "@typescript-eslint/parser": "^8.18.0", | ||||
|         "@vitejs/plugin-react": "^4.3.1", | ||||
|         "@vitest/ui": "^3.2.4", | ||||
|         "autoprefixer": "^10.4.20", | ||||
|         "eslint": "^9.16.0", | ||||
|         "eslint-config-prettier": "^9.1.0", | ||||
| @@ -90,6 +99,7 @@ | ||||
|         "eslint-plugin-react-refresh": "^0.4.7", | ||||
|         "eslint-plugin-tailwindcss": "^3.17.4", | ||||
|         "globals": "^15.13.0", | ||||
|         "happy-dom": "^18.0.1", | ||||
|         "husky": "^9.1.5", | ||||
|         "postcss": "^8.4.40", | ||||
|         "prettier": "^3.3.3", | ||||
| @@ -97,6 +107,7 @@ | ||||
|         "tailwindcss": "^3.4.7", | ||||
|         "typescript": "^5.2.2", | ||||
|         "unplugin-inject-preload": "^3.0.0", | ||||
|         "vite": "^5.3.4" | ||||
|         "vite": "^5.3.4", | ||||
|         "vitest": "^3.2.4" | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| User-agent: * | ||||
| Allow: / | ||||
| Disallow: / | ||||
|  | ||||
| Sitemap: https://app.chartdb.io/sitemap.xml | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { cva } from 'class-variance-authority'; | ||||
|  | ||||
| export const buttonVariants = cva( | ||||
|     'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50', | ||||
|     'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', | ||||
|     { | ||||
|         variants: { | ||||
|             variant: { | ||||
|   | ||||
							
								
								
									
										112
									
								
								src/components/button/button-with-alternatives.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/components/button/button-with-alternatives.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| import React from 'react'; | ||||
| import { ChevronDownIcon } from '@radix-ui/react-icons'; | ||||
| import { Slot } from '@radix-ui/react-slot'; | ||||
| import { type VariantProps } from 'class-variance-authority'; | ||||
|  | ||||
| import { cn } from '@/lib/utils'; | ||||
| import { buttonVariants } from './button-variants'; | ||||
| import { | ||||
|     DropdownMenu, | ||||
|     DropdownMenuContent, | ||||
|     DropdownMenuItem, | ||||
|     DropdownMenuTrigger, | ||||
| } from '@/components/dropdown-menu/dropdown-menu'; | ||||
|  | ||||
| export interface ButtonWithAlternativesProps | ||||
|     extends React.ButtonHTMLAttributes<HTMLButtonElement>, | ||||
|         VariantProps<typeof buttonVariants> { | ||||
|     asChild?: boolean; | ||||
|     alternatives: Array<{ | ||||
|         label: string; | ||||
|         onClick: () => void; | ||||
|         disabled?: boolean; | ||||
|         icon?: React.ReactNode; | ||||
|         className?: string; | ||||
|     }>; | ||||
|     dropdownTriggerClassName?: string; | ||||
|     chevronDownIconClassName?: string; | ||||
| } | ||||
|  | ||||
| const ButtonWithAlternatives = React.forwardRef< | ||||
|     HTMLButtonElement, | ||||
|     ButtonWithAlternativesProps | ||||
| >( | ||||
|     ( | ||||
|         { | ||||
|             className, | ||||
|             variant, | ||||
|             size, | ||||
|             asChild = false, | ||||
|             alternatives, | ||||
|             children, | ||||
|             onClick, | ||||
|             dropdownTriggerClassName, | ||||
|             chevronDownIconClassName, | ||||
|             ...props | ||||
|         }, | ||||
|         ref | ||||
|     ) => { | ||||
|         const Comp = asChild ? Slot : 'button'; | ||||
|         const hasAlternatives = (alternatives?.length ?? 0) > 0; | ||||
|  | ||||
|         return ( | ||||
|             <div className="inline-flex items-stretch"> | ||||
|                 <Comp | ||||
|                     className={cn( | ||||
|                         buttonVariants({ variant, size }), | ||||
|                         { 'rounded-r-none': hasAlternatives }, | ||||
|                         className | ||||
|                     )} | ||||
|                     ref={ref} | ||||
|                     onClick={onClick} | ||||
|                     {...props} | ||||
|                 > | ||||
|                     {children} | ||||
|                 </Comp> | ||||
|                 {hasAlternatives ? ( | ||||
|                     <DropdownMenu> | ||||
|                         <DropdownMenuTrigger asChild> | ||||
|                             <button | ||||
|                                 className={cn( | ||||
|                                     buttonVariants({ variant, size }), | ||||
|                                     'rounded-l-none border-l border-l-primary/5 px-2 min-w-0', | ||||
|                                     className?.includes('h-') && | ||||
|                                         className.match(/h-\d+/)?.[0], | ||||
|                                     className?.includes('text-') && | ||||
|                                         className.match(/text-\w+/)?.[0], | ||||
|                                     dropdownTriggerClassName | ||||
|                                 )} | ||||
|                                 type="button" | ||||
|                             > | ||||
|                                 <ChevronDownIcon | ||||
|                                     className={cn( | ||||
|                                         'size-4 shrink-0', | ||||
|                                         chevronDownIconClassName | ||||
|                                     )} | ||||
|                                 /> | ||||
|                             </button> | ||||
|                         </DropdownMenuTrigger> | ||||
|                         <DropdownMenuContent align="end"> | ||||
|                             {alternatives.map((alternative, index) => ( | ||||
|                                 <DropdownMenuItem | ||||
|                                     key={index} | ||||
|                                     onClick={alternative.onClick} | ||||
|                                     disabled={alternative.disabled} | ||||
|                                     className={cn(alternative.className)} | ||||
|                                 > | ||||
|                                     <span className="flex w-full items-center justify-between gap-2"> | ||||
|                                         {alternative.label} | ||||
|                                         {alternative.icon} | ||||
|                                     </span> | ||||
|                                 </DropdownMenuItem> | ||||
|                             ))} | ||||
|                         </DropdownMenuContent> | ||||
|                     </DropdownMenu> | ||||
|                 ) : null} | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
| ); | ||||
| ButtonWithAlternatives.displayName = 'ButtonWithAlternatives'; | ||||
|  | ||||
| export { ButtonWithAlternatives }; | ||||
| @@ -31,6 +31,7 @@ export interface CodeSnippetAction { | ||||
|     label: string; | ||||
|     icon: LucideIcon; | ||||
|     onClick: () => void; | ||||
|     className?: string; | ||||
| } | ||||
|  | ||||
| export interface CodeSnippetProps { | ||||
| @@ -43,6 +44,8 @@ export interface CodeSnippetProps { | ||||
|     isComplete?: boolean; | ||||
|     editorProps?: React.ComponentProps<EditorType>; | ||||
|     actions?: CodeSnippetAction[]; | ||||
|     actionsTooltipSide?: 'top' | 'right' | 'bottom' | 'left'; | ||||
|     allowCopy?: boolean; | ||||
| } | ||||
|  | ||||
| export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | ||||
| @@ -56,6 +59,8 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | ||||
|         isComplete = true, | ||||
|         editorProps, | ||||
|         actions, | ||||
|         actionsTooltipSide, | ||||
|         allowCopy = true, | ||||
|     }) => { | ||||
|         const { t } = useTranslation(); | ||||
|         const monaco = useMonaco(); | ||||
| @@ -129,33 +134,37 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | ||||
|                     <Suspense fallback={<Spinner />}> | ||||
|                         {isComplete ? ( | ||||
|                             <div className="absolute right-1 top-1 z-10 flex flex-col gap-1"> | ||||
|                                 <Tooltip | ||||
|                                     onOpenChange={setTooltipOpen} | ||||
|                                     open={isCopied || tooltipOpen} | ||||
|                                 > | ||||
|                                     <TooltipTrigger asChild> | ||||
|                                         <span> | ||||
|                                             <Button | ||||
|                                                 className="h-fit p-1.5" | ||||
|                                                 variant="outline" | ||||
|                                                 onClick={copyToClipboard} | ||||
|                                             > | ||||
|                                                 {isCopied ? ( | ||||
|                                                     <CopyCheck size={16} /> | ||||
|                                                 ) : ( | ||||
|                                                     <Copy size={16} /> | ||||
|                                                 )} | ||||
|                                             </Button> | ||||
|                                         </span> | ||||
|                                     </TooltipTrigger> | ||||
|                                     <TooltipContent> | ||||
|                                         {t( | ||||
|                                             isCopied | ||||
|                                                 ? 'copied' | ||||
|                                                 : 'copy_to_clipboard' | ||||
|                                         )} | ||||
|                                     </TooltipContent> | ||||
|                                 </Tooltip> | ||||
|                                 {allowCopy ? ( | ||||
|                                     <Tooltip | ||||
|                                         onOpenChange={setTooltipOpen} | ||||
|                                         open={isCopied || tooltipOpen} | ||||
|                                     > | ||||
|                                         <TooltipTrigger asChild> | ||||
|                                             <span> | ||||
|                                                 <Button | ||||
|                                                     className="h-fit p-1.5" | ||||
|                                                     variant="outline" | ||||
|                                                     onClick={copyToClipboard} | ||||
|                                                 > | ||||
|                                                     {isCopied ? ( | ||||
|                                                         <CopyCheck size={16} /> | ||||
|                                                     ) : ( | ||||
|                                                         <Copy size={16} /> | ||||
|                                                     )} | ||||
|                                                 </Button> | ||||
|                                             </span> | ||||
|                                         </TooltipTrigger> | ||||
|                                         <TooltipContent | ||||
|                                             side={actionsTooltipSide} | ||||
|                                         > | ||||
|                                             {t( | ||||
|                                                 isCopied | ||||
|                                                     ? 'copied' | ||||
|                                                     : 'copy_to_clipboard' | ||||
|                                             )} | ||||
|                                         </TooltipContent> | ||||
|                                     </Tooltip> | ||||
|                                 ) : null} | ||||
|  | ||||
|                                 {actions && | ||||
|                                     actions.length > 0 && | ||||
| @@ -164,7 +173,10 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | ||||
|                                             <TooltipTrigger asChild> | ||||
|                                                 <span> | ||||
|                                                     <Button | ||||
|                                                         className="h-fit p-1.5" | ||||
|                                                         className={cn( | ||||
|                                                             'h-fit p-1.5', | ||||
|                                                             action.className | ||||
|                                                         )} | ||||
|                                                         variant="outline" | ||||
|                                                         onClick={action.onClick} | ||||
|                                                     > | ||||
| @@ -174,7 +186,9 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo( | ||||
|                                                     </Button> | ||||
|                                                 </span> | ||||
|                                             </TooltipTrigger> | ||||
|                                             <TooltipContent> | ||||
|                                             <TooltipContent | ||||
|                                                 side={actionsTooltipSide} | ||||
|                                             > | ||||
|                                                 {action.label} | ||||
|                                             </TooltipContent> | ||||
|                                         </Tooltip> | ||||
|   | ||||
							
								
								
									
										51
									
								
								src/components/code-snippet/dbml/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/components/code-snippet/dbml/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| import type { DBMLError } from '@/lib/dbml/dbml-import/dbml-import-error'; | ||||
| import * as monaco from 'monaco-editor'; | ||||
|  | ||||
| export const highlightErrorLine = ({ | ||||
|     error, | ||||
|     model, | ||||
|     editorDecorationsCollection, | ||||
| }: { | ||||
|     error: DBMLError; | ||||
|     model?: monaco.editor.ITextModel | null; | ||||
|     editorDecorationsCollection: | ||||
|         | monaco.editor.IEditorDecorationsCollection | ||||
|         | undefined; | ||||
| }) => { | ||||
|     if (!model) return; | ||||
|     if (!editorDecorationsCollection) 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', | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|     ]; | ||||
|  | ||||
|     editorDecorationsCollection?.set(decorations); | ||||
| }; | ||||
|  | ||||
| export const clearErrorHighlight = ( | ||||
|     editorDecorationsCollection: | ||||
|         | monaco.editor.IEditorDecorationsCollection | ||||
|         | undefined | ||||
| ) => { | ||||
|     if (editorDecorationsCollection) { | ||||
|         editorDecorationsCollection.clear(); | ||||
|     } | ||||
| }; | ||||
| @@ -37,18 +37,28 @@ export const setupDBMLLanguage = (monaco: Monaco) => { | ||||
|     const datatypePattern = dataTypesNames.join('|'); | ||||
|  | ||||
|     monaco.languages.setMonarchTokensProvider('dbml', { | ||||
|         keywords: ['Table', 'Ref', 'Indexes'], | ||||
|         keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum'], | ||||
|         datatypes: dataTypesNames, | ||||
|         tokenizer: { | ||||
|             root: [ | ||||
|                 [/\b(Table|Ref|Indexes)\b/, 'keyword'], | ||||
|                 [ | ||||
|                     /\b([Tt][Aa][Bb][Ll][Ee]|[Ee][Nn][Uu][Mm]|[Rr][Ee][Ff]|[Ii][Nn][Dd][Ee][Xx][Ee][Ss]|[Nn][Oo][Tt][Ee])\b/, | ||||
|                     'keyword', | ||||
|                 ], | ||||
|                 [/\[.*?\]/, 'annotation'], | ||||
|                 [/'''/, 'string', '@tripleQuoteString'], | ||||
|                 [/".*?"/, 'string'], | ||||
|                 [/'.*?'/, 'string'], | ||||
|                 [/`.*?`/, 'string'], | ||||
|                 [/[{}]/, 'delimiter'], | ||||
|                 [/[<>]/, 'operator'], | ||||
|                 [new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching | ||||
|             ], | ||||
|             tripleQuoteString: [ | ||||
|                 [/[^']+/, 'string'], | ||||
|                 [/'''/, 'string', '@pop'], | ||||
|                 [/'/, 'string'], | ||||
|             ], | ||||
|         }, | ||||
|     }); | ||||
| }; | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import { Cross2Icon } from '@radix-ui/react-icons'; | ||||
|  | ||||
| import { cn } from '@/lib/utils'; | ||||
| import { ScrollArea } from '../scroll-area/scroll-area'; | ||||
| import { ChevronLeft } from 'lucide-react'; | ||||
|  | ||||
| const Dialog = DialogPrimitive.Root; | ||||
|  | ||||
| @@ -32,28 +33,75 @@ const DialogContent = React.forwardRef< | ||||
|     React.ElementRef<typeof DialogPrimitive.Content>, | ||||
|     React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & { | ||||
|         showClose?: boolean; | ||||
|         showBack?: boolean; | ||||
|         backButtonClassName?: string; | ||||
|         blurBackground?: boolean; | ||||
|         forceOverlay?: boolean; | ||||
|         onBackClick?: () => void; | ||||
|     } | ||||
| >(({ className, children, showClose, ...props }, ref) => ( | ||||
|     <DialogPortal> | ||||
|         <DialogOverlay /> | ||||
|         <DialogPrimitive.Content | ||||
|             ref={ref} | ||||
|             className={cn( | ||||
|                 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', | ||||
|                 className | ||||
|             )} | ||||
|             {...props} | ||||
|         > | ||||
|             {children} | ||||
|             {showClose && ( | ||||
|                 <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> | ||||
|                     <Cross2Icon className="size-4" /> | ||||
|                     <span className="sr-only">Close</span> | ||||
|                 </DialogPrimitive.Close> | ||||
|             )} | ||||
|         </DialogPrimitive.Content> | ||||
|     </DialogPortal> | ||||
| )); | ||||
| >( | ||||
|     ( | ||||
|         { | ||||
|             className, | ||||
|             children, | ||||
|             showClose, | ||||
|             showBack, | ||||
|             onBackClick, | ||||
|             backButtonClassName, | ||||
|             blurBackground, | ||||
|             forceOverlay, | ||||
|             ...props | ||||
|         }, | ||||
|         ref | ||||
|     ) => ( | ||||
|         <DialogPortal> | ||||
|             {forceOverlay ? ( | ||||
|                 <div | ||||
|                     className={cn( | ||||
|                         'fixed inset-0 z-50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', | ||||
|                         { | ||||
|                             'bg-black/80': !blurBackground, | ||||
|                             'bg-black/30 backdrop-blur-sm': blurBackground, | ||||
|                         } | ||||
|                     )} | ||||
|                     data-state="open" | ||||
|                 /> | ||||
|             ) : null} | ||||
|             <DialogOverlay | ||||
|                 className={cn({ | ||||
|                     'bg-black/30 backdrop-blur-sm': blurBackground, | ||||
|                 })} | ||||
|             /> | ||||
|             <DialogPrimitive.Content | ||||
|                 ref={ref} | ||||
|                 className={cn( | ||||
|                     'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg', | ||||
|                     className | ||||
|                 )} | ||||
|                 {...props} | ||||
|             > | ||||
|                 {children} | ||||
|                 {showBack && ( | ||||
|                     <button | ||||
|                         onClick={() => onBackClick?.()} | ||||
|                         className={cn( | ||||
|                             'absolute left-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground', | ||||
|                             backButtonClassName | ||||
|                         )} | ||||
|                     > | ||||
|                         <ChevronLeft className="size-4" /> | ||||
|                     </button> | ||||
|                 )} | ||||
|                 {showClose && ( | ||||
|                     <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> | ||||
|                         <Cross2Icon className="size-4" /> | ||||
|                         <span className="sr-only">Close</span> | ||||
|                     </DialogPrimitive.Close> | ||||
|                 )} | ||||
|             </DialogPrimitive.Content> | ||||
|         </DialogPortal> | ||||
|     ) | ||||
| ); | ||||
| DialogContent.displayName = DialogPrimitive.Content.displayName; | ||||
|  | ||||
| const DialogHeader = ({ | ||||
|   | ||||
| @@ -52,7 +52,7 @@ export const EmptyState = forwardRef< | ||||
|                 </Label> | ||||
|                 <Label | ||||
|                     className={cn( | ||||
|                         'text-sm font-normal text-muted-foreground', | ||||
|                         'text-sm text-center font-normal text-muted-foreground', | ||||
|                         descriptionClassName | ||||
|                     )} | ||||
|                 > | ||||
|   | ||||
| @@ -2,16 +2,13 @@ import React from 'react'; | ||||
|  | ||||
| import { cn } from '@/lib/utils'; | ||||
|  | ||||
| export interface InputProps | ||||
|     extends React.InputHTMLAttributes<HTMLInputElement> {} | ||||
|  | ||||
| const Input = React.forwardRef<HTMLInputElement, InputProps>( | ||||
| const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>( | ||||
|     ({ className, type, ...props }, ref) => { | ||||
|         return ( | ||||
|             <input | ||||
|                 type={type} | ||||
|                 className={cn( | ||||
|                     'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', | ||||
|                     'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', | ||||
|                     className | ||||
|                 )} | ||||
|                 ref={ref} | ||||
|   | ||||
							
								
								
									
										121
									
								
								src/components/pagination/pagination.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								src/components/pagination/pagination.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| import React from 'react'; | ||||
| import { cn } from '@/lib/utils'; | ||||
| import type { ButtonProps } from '../button/button'; | ||||
| import { buttonVariants } from '../button/button-variants'; | ||||
| import { | ||||
|     ChevronLeftIcon, | ||||
|     ChevronRightIcon, | ||||
|     DotsHorizontalIcon, | ||||
| } from '@radix-ui/react-icons'; | ||||
|  | ||||
| const Pagination = ({ className, ...props }: React.ComponentProps<'nav'>) => ( | ||||
|     <nav | ||||
|         role="navigation" | ||||
|         aria-label="pagination" | ||||
|         className={cn('mx-auto flex w-full justify-center', className)} | ||||
|         {...props} | ||||
|     /> | ||||
| ); | ||||
| Pagination.displayName = 'Pagination'; | ||||
|  | ||||
| const PaginationContent = React.forwardRef< | ||||
|     HTMLUListElement, | ||||
|     React.ComponentProps<'ul'> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|     <ul | ||||
|         ref={ref} | ||||
|         className={cn('flex flex-row items-center gap-1', className)} | ||||
|         {...props} | ||||
|     /> | ||||
| )); | ||||
| PaginationContent.displayName = 'PaginationContent'; | ||||
|  | ||||
| const PaginationItem = React.forwardRef< | ||||
|     HTMLLIElement, | ||||
|     React.ComponentProps<'li'> | ||||
| >(({ className, ...props }, ref) => ( | ||||
|     <li ref={ref} className={cn('', className)} {...props} /> | ||||
| )); | ||||
| PaginationItem.displayName = 'PaginationItem'; | ||||
|  | ||||
| type PaginationLinkProps = { | ||||
|     isActive?: boolean; | ||||
| } & Pick<ButtonProps, 'size'> & | ||||
|     React.ComponentProps<'a'>; | ||||
|  | ||||
| const PaginationLink = ({ | ||||
|     className, | ||||
|     isActive, | ||||
|     size = 'icon', | ||||
|     ...props | ||||
| }: PaginationLinkProps) => ( | ||||
|     <a | ||||
|         aria-current={isActive ? 'page' : undefined} | ||||
|         className={cn( | ||||
|             buttonVariants({ | ||||
|                 variant: isActive ? 'outline' : 'ghost', | ||||
|                 size, | ||||
|             }), | ||||
|             className | ||||
|         )} | ||||
|         {...props} | ||||
|     /> | ||||
| ); | ||||
| PaginationLink.displayName = 'PaginationLink'; | ||||
|  | ||||
| const PaginationPrevious = ({ | ||||
|     className, | ||||
|     ...props | ||||
| }: React.ComponentProps<typeof PaginationLink>) => ( | ||||
|     <PaginationLink | ||||
|         aria-label="Go to previous page" | ||||
|         size="default" | ||||
|         className={cn('gap-1 pl-2.5', className)} | ||||
|         {...props} | ||||
|     > | ||||
|         <ChevronLeftIcon className="size-4" /> | ||||
|         <span>Previous</span> | ||||
|     </PaginationLink> | ||||
| ); | ||||
| PaginationPrevious.displayName = 'PaginationPrevious'; | ||||
|  | ||||
| const PaginationNext = ({ | ||||
|     className, | ||||
|     ...props | ||||
| }: React.ComponentProps<typeof PaginationLink>) => ( | ||||
|     <PaginationLink | ||||
|         aria-label="Go to next page" | ||||
|         size="default" | ||||
|         className={cn('gap-1 pr-2.5', className)} | ||||
|         {...props} | ||||
|     > | ||||
|         <span>Next</span> | ||||
|         <ChevronRightIcon className="size-4" /> | ||||
|     </PaginationLink> | ||||
| ); | ||||
| PaginationNext.displayName = 'PaginationNext'; | ||||
|  | ||||
| const PaginationEllipsis = ({ | ||||
|     className, | ||||
|     ...props | ||||
| }: React.ComponentProps<'span'>) => ( | ||||
|     <span | ||||
|         aria-hidden | ||||
|         className={cn('flex h-9 w-9 items-center justify-center', className)} | ||||
|         {...props} | ||||
|     > | ||||
|         <DotsHorizontalIcon className="size-4" /> | ||||
|         <span className="sr-only">More pages</span> | ||||
|     </span> | ||||
| ); | ||||
| PaginationEllipsis.displayName = 'PaginationEllipsis'; | ||||
|  | ||||
| export { | ||||
|     Pagination, | ||||
|     PaginationContent, | ||||
|     PaginationLink, | ||||
|     PaginationItem, | ||||
|     PaginationPrevious, | ||||
|     PaginationNext, | ||||
|     PaginationEllipsis, | ||||
| }; | ||||
| @@ -93,6 +93,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | ||||
|             (isOpen: boolean) => { | ||||
|                 setOpen?.(isOpen); | ||||
|                 setIsOpen(isOpen); | ||||
|  | ||||
|                 if (isOpen) { | ||||
|                     setSearchTerm(''); | ||||
|                 } | ||||
|  | ||||
|                 setTimeout(() => (document.body.style.pointerEvents = ''), 500); | ||||
|             }, | ||||
|             [setOpen] | ||||
|         ); | ||||
| @@ -227,7 +233,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | ||||
|                         onSelect={() => | ||||
|                             handleSelect( | ||||
|                                 option.value, | ||||
|                                 matches?.map((match) => match.toString()) | ||||
|                                 matches?.map((match) => match?.toString()) | ||||
|                             ) | ||||
|                         } | ||||
|                     > | ||||
| @@ -418,27 +424,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>( | ||||
|  | ||||
|                         <ScrollArea> | ||||
|                             <div className="max-h-64 w-full"> | ||||
|                                 <CommandGroup> | ||||
|                                     <CommandList className="max-h-fit w-full"> | ||||
|                                         {hasGroups | ||||
|                                             ? Object.entries(groups).map( | ||||
|                                                   ([ | ||||
|                                                       groupName, | ||||
|                                                       groupOptions, | ||||
|                                                   ]) => ( | ||||
|                                                       <CommandGroup | ||||
|                                                           key={groupName} | ||||
|                                                           heading={groupName} | ||||
|                                                       > | ||||
|                                                           {groupOptions.map( | ||||
|                                                               renderOption | ||||
|                                                           )} | ||||
|                                                       </CommandGroup> | ||||
|                                                   ) | ||||
|                                 <CommandList className="max-h-fit w-full"> | ||||
|                                     {hasGroups | ||||
|                                         ? Object.entries(groups).map( | ||||
|                                               ([groupName, groupOptions]) => ( | ||||
|                                                   <CommandGroup | ||||
|                                                       key={groupName} | ||||
|                                                       heading={groupName} | ||||
|                                                   > | ||||
|                                                       {groupOptions.map( | ||||
|                                                           renderOption | ||||
|                                                       )} | ||||
|                                                   </CommandGroup> | ||||
|                                               ) | ||||
|                                             : options.map(renderOption)} | ||||
|                                     </CommandList> | ||||
|                                 </CommandGroup> | ||||
|                                           ) | ||||
|                                         : options.map(renderOption)} | ||||
|                                 </CommandList> | ||||
|                             </div> | ||||
|                         </ScrollArea> | ||||
|                     </Command> | ||||
|   | ||||
| @@ -29,6 +29,7 @@ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; | ||||
| const SIDEBAR_WIDTH = '16rem'; | ||||
| const SIDEBAR_WIDTH_MOBILE = '18rem'; | ||||
| const SIDEBAR_WIDTH_ICON = '3rem'; | ||||
| const SIDEBAR_WIDTH_ICON_EXTENDED = '4rem'; | ||||
| const SIDEBAR_KEYBOARD_SHORTCUT = 'b'; | ||||
|  | ||||
| type SidebarContext = { | ||||
| @@ -142,6 +143,8 @@ const SidebarProvider = React.forwardRef< | ||||
|                             { | ||||
|                                 '--sidebar-width': SIDEBAR_WIDTH, | ||||
|                                 '--sidebar-width-icon': SIDEBAR_WIDTH_ICON, | ||||
|                                 '--sidebar-width-icon-extended': | ||||
|                                     SIDEBAR_WIDTH_ICON_EXTENDED, | ||||
|                                 ...style, | ||||
|                             } as React.CSSProperties | ||||
|                         } | ||||
| @@ -166,7 +169,7 @@ const Sidebar = React.forwardRef< | ||||
|     React.ComponentProps<'div'> & { | ||||
|         side?: 'left' | 'right'; | ||||
|         variant?: 'sidebar' | 'floating' | 'inset'; | ||||
|         collapsible?: 'offcanvas' | 'icon' | 'none'; | ||||
|         collapsible?: 'offcanvas' | 'icon' | 'icon-extended' | 'none'; | ||||
|     } | ||||
| >( | ||||
|     ( | ||||
| @@ -245,8 +248,8 @@ const Sidebar = React.forwardRef< | ||||
|                         'group-data-[collapsible=offcanvas]:w-0', | ||||
|                         'group-data-[side=right]:rotate-180', | ||||
|                         variant === 'floating' || variant === 'inset' | ||||
|                             ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]' | ||||
|                             : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon]' | ||||
|                             ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))] group-data-[collapsible=icon-extended]:w-[calc(var(--sidebar-width-icon-extended)_+_theme(spacing.4))]' | ||||
|                             : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[collapsible=icon-extended]:w-[--sidebar-width-icon-extended]' | ||||
|                     )} | ||||
|                 /> | ||||
|                 <div | ||||
| @@ -257,8 +260,8 @@ const Sidebar = React.forwardRef< | ||||
|                             : 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]', | ||||
|                         // Adjust the padding for floating and inset variants. | ||||
|                         variant === 'floating' || variant === 'inset' | ||||
|                             ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]' | ||||
|                             : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l', | ||||
|                             ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)] group-data-[collapsible=icon-extended]:w-[calc(var(--sidebar-width-icon-extended)_+_theme(spacing.4)_+2px)]' | ||||
|                             : 'group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[collapsible=icon-extended]:w-[--sidebar-width-icon-extended] group-data-[side=left]:border-r group-data-[side=right]:border-l', | ||||
|                         className | ||||
|                     )} | ||||
|                     {...props} | ||||
| @@ -421,7 +424,7 @@ const SidebarContent = React.forwardRef< | ||||
|             ref={ref} | ||||
|             data-sidebar="content" | ||||
|             className={cn( | ||||
|                 'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden', | ||||
|                 'flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden group-data-[collapsible=icon-extended]:overflow-hidden', | ||||
|                 className | ||||
|             )} | ||||
|             {...props} | ||||
| @@ -461,6 +464,7 @@ const SidebarGroupLabel = React.forwardRef< | ||||
|             className={cn( | ||||
|                 'flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', | ||||
|                 'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0', | ||||
|                 'group-data-[collapsible=icon-extended]:-mt-8 group-data-[collapsible=icon-extended]:opacity-0', | ||||
|                 className | ||||
|             )} | ||||
|             {...props} | ||||
| @@ -483,7 +487,7 @@ const SidebarGroupAction = React.forwardRef< | ||||
|                 'absolute right-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', | ||||
|                 // Increases the hit area of the button on mobile. | ||||
|                 'after:absolute after:-inset-2 after:md:hidden', | ||||
|                 'group-data-[collapsible=icon]:hidden', | ||||
|                 'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden', | ||||
|                 className | ||||
|             )} | ||||
|             {...props} | ||||
| @@ -532,7 +536,7 @@ const SidebarMenuItem = React.forwardRef< | ||||
| SidebarMenuItem.displayName = 'SidebarMenuItem'; | ||||
|  | ||||
| const sidebarMenuButtonVariants = cva( | ||||
|     'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', | ||||
|     'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon-extended]:h-auto group-data-[collapsible=icon-extended]:flex-col group-data-[collapsible=icon-extended]:gap-1 group-data-[collapsible=icon-extended]:p-2 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate group-data-[collapsible=icon-extended]:[&>span]:w-full group-data-[collapsible=icon-extended]:[&>span]:text-center group-data-[collapsible=icon-extended]:[&>span]:text-[10px] group-data-[collapsible=icon-extended]:[&>span]:leading-tight [&>svg]:size-4 [&>svg]:shrink-0', | ||||
|     { | ||||
|         variants: { | ||||
|             variant: { | ||||
| @@ -636,7 +640,7 @@ const SidebarMenuAction = React.forwardRef< | ||||
|                 'peer-data-[size=sm]/menu-button:top-1', | ||||
|                 'peer-data-[size=default]/menu-button:top-1.5', | ||||
|                 'peer-data-[size=lg]/menu-button:top-2.5', | ||||
|                 'group-data-[collapsible=icon]:hidden', | ||||
|                 'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden', | ||||
|                 showOnHover && | ||||
|                     'group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0', | ||||
|                 className | ||||
| @@ -753,7 +757,7 @@ const SidebarMenuSubButton = React.forwardRef< | ||||
|                 'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground', | ||||
|                 size === 'sm' && 'text-xs', | ||||
|                 size === 'md' && 'text-sm', | ||||
|                 'group-data-[collapsible=icon]:hidden', | ||||
|                 'group-data-[collapsible=icon]:hidden group-data-[collapsible=icon-extended]:hidden', | ||||
|                 className | ||||
|             )} | ||||
|             {...props} | ||||
|   | ||||
| @@ -20,6 +20,7 @@ export function Toaster() { | ||||
|                 description, | ||||
|                 action, | ||||
|                 layout = 'row', | ||||
|                 hideCloseButton = false, | ||||
|                 ...props | ||||
|             }) { | ||||
|                 return ( | ||||
| @@ -38,7 +39,7 @@ export function Toaster() { | ||||
|                             ) : null} | ||||
|                         </div> | ||||
|                         {layout === 'row' ? action : null} | ||||
|                         <ToastClose /> | ||||
|                         {!hideCloseButton ? <ToastClose /> : null} | ||||
|                     </Toast> | ||||
|                 ); | ||||
|             })} | ||||
|   | ||||
| @@ -12,6 +12,7 @@ type ToasterToast = ToastProps & { | ||||
|     description?: React.ReactNode; | ||||
|     action?: ToastActionElement; | ||||
|     layout?: 'row' | 'column'; | ||||
|     hideCloseButton?: boolean; | ||||
| }; | ||||
|  | ||||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
|   | ||||
| @@ -13,15 +13,17 @@ const TooltipContent = React.forwardRef< | ||||
|     React.ElementRef<typeof TooltipPrimitive.Content>, | ||||
|     React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> | ||||
| >(({ className, sideOffset = 4, ...props }, ref) => ( | ||||
|     // <TooltipPrimitive.Portal> | ||||
|     <TooltipPrimitive.Content | ||||
|         ref={ref} | ||||
|         sideOffset={sideOffset} | ||||
|         className={cn( | ||||
|             'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', | ||||
|             'z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]', | ||||
|             className | ||||
|         )} | ||||
|         {...props} | ||||
|     /> | ||||
|     // </TooltipPrimitive.Portal> | ||||
| )); | ||||
| TooltipContent.displayName = TooltipPrimitive.Content.displayName; | ||||
|  | ||||
|   | ||||
							
								
								
									
										17
									
								
								src/components/tree-view/tree-item-skeleton.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/components/tree-view/tree-item-skeleton.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| import React from 'react'; | ||||
| import { Skeleton } from '../skeleton/skeleton'; | ||||
| import { cn } from '@/lib/utils'; | ||||
|  | ||||
| export interface TreeItemSkeletonProps | ||||
|     extends React.HTMLAttributes<HTMLDivElement> {} | ||||
|  | ||||
| export const TreeItemSkeleton: React.FC<TreeItemSkeletonProps> = ({ | ||||
|     className, | ||||
|     style, | ||||
| }) => { | ||||
|     return ( | ||||
|         <div className={cn('px-2 py-1', className)} style={style}> | ||||
|             <Skeleton className="h-3.5 w-full rounded-sm" /> | ||||
|         </div> | ||||
|     ); | ||||
| }; | ||||
							
								
								
									
										461
									
								
								src/components/tree-view/tree-view.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										461
									
								
								src/components/tree-view/tree-view.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,461 @@ | ||||
| import { | ||||
|     ChevronRight, | ||||
|     File, | ||||
|     Folder, | ||||
|     Loader2, | ||||
|     type LucideIcon, | ||||
| } from 'lucide-react'; | ||||
| import { motion, AnimatePresence } from 'framer-motion'; | ||||
| import { cn } from '@/lib/utils'; | ||||
| import { Button } from '@/components/button/button'; | ||||
| import type { | ||||
|     TreeNode, | ||||
|     FetchChildrenFunction, | ||||
|     SelectableTreeProps, | ||||
| } from './tree'; | ||||
| import type { ExpandedState } from './use-tree'; | ||||
| import { useTree } from './use-tree'; | ||||
| import type { Dispatch, ReactNode, SetStateAction } from 'react'; | ||||
| import React, { useCallback, useEffect, useMemo, useState } from 'react'; | ||||
| import { TreeItemSkeleton } from './tree-item-skeleton'; | ||||
| import { | ||||
|     Tooltip, | ||||
|     TooltipContent, | ||||
|     TooltipTrigger, | ||||
| } from '@/components/tooltip/tooltip'; | ||||
|  | ||||
| interface TreeViewProps< | ||||
|     Type extends string, | ||||
|     Context extends Record<Type, unknown>, | ||||
| > { | ||||
|     data: TreeNode<Type, Context>[]; | ||||
|     fetchChildren?: FetchChildrenFunction<Type, Context>; | ||||
|     onNodeClick?: (node: TreeNode<Type, Context>) => void; | ||||
|     className?: string; | ||||
|     defaultIcon?: LucideIcon; | ||||
|     defaultFolderIcon?: LucideIcon; | ||||
|     defaultIconProps?: React.ComponentProps<LucideIcon>; | ||||
|     defaultFolderIconProps?: React.ComponentProps<LucideIcon>; | ||||
|     selectable?: SelectableTreeProps<Type, Context>; | ||||
|     expanded?: ExpandedState; | ||||
|     setExpanded?: Dispatch<SetStateAction<ExpandedState>>; | ||||
|     renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode; | ||||
|     renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode; | ||||
|     loadingNodeIds?: string[]; | ||||
| } | ||||
|  | ||||
| export function TreeView< | ||||
|     Type extends string, | ||||
|     Context extends Record<Type, unknown>, | ||||
| >({ | ||||
|     data, | ||||
|     fetchChildren, | ||||
|     onNodeClick, | ||||
|     className, | ||||
|     defaultIcon = File, | ||||
|     defaultFolderIcon = Folder, | ||||
|     defaultIconProps, | ||||
|     defaultFolderIconProps, | ||||
|     selectable, | ||||
|     expanded: expandedProp, | ||||
|     setExpanded: setExpandedProp, | ||||
|     renderHoverComponent, | ||||
|     renderActionsComponent, | ||||
|     loadingNodeIds, | ||||
| }: TreeViewProps<Type, Context>) { | ||||
|     const { expanded, loading, loadedChildren, hasMoreChildren, toggleNode } = | ||||
|         useTree({ | ||||
|             fetchChildren, | ||||
|             expanded: expandedProp, | ||||
|             setExpanded: setExpandedProp, | ||||
|         }); | ||||
|     const [selectedIdInternal, setSelectedIdInternal] = React.useState< | ||||
|         string | undefined | ||||
|     >(selectable?.defaultSelectedId); | ||||
|  | ||||
|     const selectedId = useMemo(() => { | ||||
|         return selectable?.selectedId ?? selectedIdInternal; | ||||
|     }, [selectable?.selectedId, selectedIdInternal]); | ||||
|  | ||||
|     const setSelectedId = useCallback( | ||||
|         (value: SetStateAction<string | undefined>) => { | ||||
|             if (selectable?.setSelectedId) { | ||||
|                 selectable.setSelectedId(value); | ||||
|             } else { | ||||
|                 setSelectedIdInternal(value); | ||||
|             } | ||||
|         }, | ||||
|         [selectable, setSelectedIdInternal] | ||||
|     ); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (selectable?.enabled && selectable.defaultSelectedId) { | ||||
|             if (selectable.defaultSelectedId === selectedId) return; | ||||
|             setSelectedId(selectable.defaultSelectedId); | ||||
|             const { node, path } = findNodeById( | ||||
|                 data, | ||||
|                 selectable.defaultSelectedId | ||||
|             ); | ||||
|  | ||||
|             if (node) { | ||||
|                 selectable.onSelectedChange?.(node); | ||||
|  | ||||
|                 // Expand all parent nodes | ||||
|                 for (const parent of path) { | ||||
|                     if (expanded[parent.id]) continue; | ||||
|                     toggleNode( | ||||
|                         parent.id, | ||||
|                         parent.type, | ||||
|                         parent.context, | ||||
|                         parent.children | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     }, [selectable, toggleNode, selectedId, data, expanded, setSelectedId]); | ||||
|  | ||||
|     const handleNodeSelect = (node: TreeNode<Type, Context>) => { | ||||
|         if (selectable?.enabled) { | ||||
|             setSelectedId(node.id); | ||||
|             selectable.onSelectedChange?.(node); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|         <div className={cn('w-full', className)}> | ||||
|             {data.map((node, index) => ( | ||||
|                 <TreeNode | ||||
|                     key={node.id} | ||||
|                     node={node} | ||||
|                     level={0} | ||||
|                     expanded={expanded} | ||||
|                     loading={loading} | ||||
|                     loadedChildren={loadedChildren} | ||||
|                     hasMoreChildren={hasMoreChildren} | ||||
|                     onToggle={toggleNode} | ||||
|                     onNodeClick={onNodeClick} | ||||
|                     defaultIcon={defaultIcon} | ||||
|                     defaultFolderIcon={defaultFolderIcon} | ||||
|                     defaultIconProps={defaultIconProps} | ||||
|                     defaultFolderIconProps={defaultFolderIconProps} | ||||
|                     selectable={selectable?.enabled} | ||||
|                     selectedId={selectedId} | ||||
|                     onSelect={handleNodeSelect} | ||||
|                     className={index > 0 ? 'mt-0.5' : ''} | ||||
|                     renderHoverComponent={renderHoverComponent} | ||||
|                     renderActionsComponent={renderActionsComponent} | ||||
|                     loadingNodeIds={loadingNodeIds} | ||||
|                 /> | ||||
|             ))} | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| interface TreeNodeProps< | ||||
|     Type extends string, | ||||
|     Context extends Record<Type, unknown>, | ||||
| > { | ||||
|     node: TreeNode<Type, Context>; | ||||
|     level: number; | ||||
|     expanded: Record<string, boolean>; | ||||
|     loading: Record<string, boolean>; | ||||
|     loadedChildren: Record<string, TreeNode<Type, Context>[]>; | ||||
|     hasMoreChildren: Record<string, boolean>; | ||||
|     onToggle: ( | ||||
|         nodeId: string, | ||||
|         nodeType: Type, | ||||
|         nodeContext: Context[Type], | ||||
|         staticChildren?: TreeNode<Type, Context>[] | ||||
|     ) => void; | ||||
|     onNodeClick?: (node: TreeNode<Type, Context>) => void; | ||||
|     defaultIcon: LucideIcon; | ||||
|     defaultFolderIcon: LucideIcon; | ||||
|     defaultIconProps?: React.ComponentProps<LucideIcon>; | ||||
|     defaultFolderIconProps?: React.ComponentProps<LucideIcon>; | ||||
|     selectable?: boolean; | ||||
|     selectedId?: string; | ||||
|     onSelect: (node: TreeNode<Type, Context>) => void; | ||||
|     className?: string; | ||||
|     renderHoverComponent?: (node: TreeNode<Type, Context>) => ReactNode; | ||||
|     renderActionsComponent?: (node: TreeNode<Type, Context>) => ReactNode; | ||||
|     loadingNodeIds?: string[]; | ||||
| } | ||||
|  | ||||
| function TreeNode<Type extends string, Context extends Record<Type, unknown>>({ | ||||
|     node, | ||||
|     level, | ||||
|     expanded, | ||||
|     loading, | ||||
|     loadedChildren, | ||||
|     hasMoreChildren, | ||||
|     onToggle, | ||||
|     onNodeClick, | ||||
|     defaultIcon: DefaultIcon, | ||||
|     defaultFolderIcon: DefaultFolderIcon, | ||||
|     defaultIconProps, | ||||
|     defaultFolderIconProps, | ||||
|     selectable, | ||||
|     selectedId, | ||||
|     onSelect, | ||||
|     className, | ||||
|     renderHoverComponent, | ||||
|     renderActionsComponent, | ||||
|     loadingNodeIds, | ||||
| }: TreeNodeProps<Type, Context>) { | ||||
|     const [isHovered, setIsHovered] = useState(false); | ||||
|     const isExpanded = expanded[node.id]; | ||||
|     const isLoading = loading[node.id]; | ||||
|     const children = loadedChildren[node.id] || node.children; | ||||
|     const isSelected = selectedId === node.id; | ||||
|  | ||||
|     const IconComponent = | ||||
|         node.icon || (node.isFolder ? DefaultFolderIcon : DefaultIcon); | ||||
|     const iconProps: React.ComponentProps<LucideIcon> = { | ||||
|         strokeWidth: isSelected ? 2.5 : 2, | ||||
|         ...(node.isFolder ? defaultFolderIconProps : defaultIconProps), | ||||
|         ...node.iconProps, | ||||
|         className: cn( | ||||
|             'h-3.5 w-3.5 text-muted-foreground flex-none', | ||||
|             isSelected && 'text-primary text-white', | ||||
|             node.iconProps?.className | ||||
|         ), | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|         <div className={cn(className)}> | ||||
|             <div | ||||
|                 className={cn( | ||||
|                     'flex items-center gap-1.5 px-2 py-1 rounded-lg cursor-pointer group h-6', | ||||
|                     'transition-colors duration-200', | ||||
|                     isSelected | ||||
|                         ? 'bg-sky-500 border border-sky-600 border dark:bg-sky-600 dark:border-sky-700' | ||||
|                         : 'hover:bg-gray-200/50 border border-transparent dark:hover:bg-gray-700/50', | ||||
|                     node.className | ||||
|                 )} | ||||
|                 {...(isSelected ? { 'data-selected': true } : {})} | ||||
|                 style={{ paddingLeft: `${level * 16 + 8}px` }} | ||||
|                 onMouseEnter={() => setIsHovered(true)} | ||||
|                 onMouseLeave={() => setIsHovered(false)} | ||||
|                 onClick={(e) => { | ||||
|                     e.stopPropagation(); | ||||
|                     if (selectable && !node.unselectable) { | ||||
|                         onSelect(node); | ||||
|                     } | ||||
|                     // if (node.isFolder) { | ||||
|                     //     onToggle(node.id, node.children); | ||||
|                     // } | ||||
|  | ||||
|                     // called only once in case of double click | ||||
|                     if (e.detail !== 2) { | ||||
|                         onNodeClick?.(node); | ||||
|                     } | ||||
|                 }} | ||||
|                 onDoubleClick={(e) => { | ||||
|                     e.stopPropagation(); | ||||
|                     if (node.isFolder) { | ||||
|                         onToggle( | ||||
|                             node.id, | ||||
|                             node.type, | ||||
|                             node.context, | ||||
|                             node.children | ||||
|                         ); | ||||
|                     } | ||||
|                 }} | ||||
|             > | ||||
|                 <div className="flex flex-none items-center gap-1.5"> | ||||
|                     <Button | ||||
|                         variant="ghost" | ||||
|                         size="icon" | ||||
|                         className={cn( | ||||
|                             'h-3.5 w-3.5 p-0 hover:bg-transparent flex-none', | ||||
|                             isExpanded && 'rotate-90', | ||||
|                             'transition-transform duration-200' | ||||
|                         )} | ||||
|                         onClick={(e) => { | ||||
|                             e.stopPropagation(); | ||||
|                             if (node.isFolder) { | ||||
|                                 onToggle( | ||||
|                                     node.id, | ||||
|                                     node.type, | ||||
|                                     node.context, | ||||
|                                     node.children | ||||
|                                 ); | ||||
|                             } | ||||
|                         }} | ||||
|                     > | ||||
|                         {node.isFolder && | ||||
|                             (isLoading ? ( | ||||
|                                 <Loader2 | ||||
|                                     className={cn('size-3.5 animate-spin', { | ||||
|                                         'text-white': isSelected, | ||||
|                                     })} | ||||
|                                 /> | ||||
|                             ) : ( | ||||
|                                 <ChevronRight | ||||
|                                     className={cn('size-3.5', { | ||||
|                                         'text-white': isSelected, | ||||
|                                     })} | ||||
|                                     strokeWidth={2} | ||||
|                                 /> | ||||
|                             ))} | ||||
|                     </Button> | ||||
|  | ||||
|                     {node.tooltip ? ( | ||||
|                         <Tooltip> | ||||
|                             <TooltipTrigger asChild> | ||||
|                                 {loadingNodeIds?.includes(node.id) ? ( | ||||
|                                     <Loader2 | ||||
|                                         className={cn('size-3.5 animate-spin', { | ||||
|                                             'text-white': isSelected, | ||||
|                                         })} | ||||
|                                     /> | ||||
|                                 ) : ( | ||||
|                                     <IconComponent | ||||
|                                         {...(isSelected | ||||
|                                             ? { 'data-selected': true } | ||||
|                                             : {})} | ||||
|                                         {...iconProps} | ||||
|                                     /> | ||||
|                                 )} | ||||
|                             </TooltipTrigger> | ||||
|                             <TooltipContent | ||||
|                                 align="center" | ||||
|                                 className="max-w-[400px]" | ||||
|                             > | ||||
|                                 {node.tooltip} | ||||
|                             </TooltipContent> | ||||
|                         </Tooltip> | ||||
|                     ) : node.empty ? null : loadingNodeIds?.includes( | ||||
|                           node.id | ||||
|                       ) ? ( | ||||
|                         <Loader2 | ||||
|                             className={cn('size-3.5 animate-spin', { | ||||
|                                 // 'text-white': isSelected, | ||||
|                             })} | ||||
|                         /> | ||||
|                     ) : ( | ||||
|                         <IconComponent | ||||
|                             {...(isSelected ? { 'data-selected': true } : {})} | ||||
|                             {...iconProps} | ||||
|                         /> | ||||
|                     )} | ||||
|                 </div> | ||||
|  | ||||
|                 <span | ||||
|                     {...node.labelProps} | ||||
|                     className={cn( | ||||
|                         'text-xs truncate min-w-0 flex-1 w-0', | ||||
|                         isSelected && 'font-medium text-primary text-white', | ||||
|                         node.labelProps?.className | ||||
|                     )} | ||||
|                     {...(isSelected ? { 'data-selected': true } : {})} | ||||
|                 > | ||||
|                     {node.empty ? '' : node.name} | ||||
|                 </span> | ||||
|                 {renderActionsComponent && renderActionsComponent(node)} | ||||
|                 {isHovered && renderHoverComponent | ||||
|                     ? renderHoverComponent(node) | ||||
|                     : null} | ||||
|             </div> | ||||
|  | ||||
|             <AnimatePresence initial={false}> | ||||
|                 {isExpanded && children && ( | ||||
|                     <motion.div | ||||
|                         initial={{ height: 0, opacity: 0 }} | ||||
|                         animate={{ | ||||
|                             height: 'auto', | ||||
|                             opacity: 1, | ||||
|                             transition: { | ||||
|                                 height: { | ||||
|                                     duration: Math.min( | ||||
|                                         0.3 + children.length * 0.018, | ||||
|                                         0.7 | ||||
|                                     ), | ||||
|                                     ease: 'easeInOut', | ||||
|                                 }, | ||||
|                                 opacity: { | ||||
|                                     duration: Math.min( | ||||
|                                         0.2 + children.length * 0.012, | ||||
|                                         0.4 | ||||
|                                     ), | ||||
|                                     ease: 'easeInOut', | ||||
|                                 }, | ||||
|                             }, | ||||
|                         }} | ||||
|                         exit={{ | ||||
|                             height: 0, | ||||
|                             opacity: 0, | ||||
|                             transition: { | ||||
|                                 height: { | ||||
|                                     duration: Math.min( | ||||
|                                         0.2 + children.length * 0.01, | ||||
|                                         0.45 | ||||
|                                     ), | ||||
|                                     ease: 'easeInOut', | ||||
|                                 }, | ||||
|                                 opacity: { | ||||
|                                     duration: 0.1, | ||||
|                                     ease: 'easeOut', | ||||
|                                 }, | ||||
|                             }, | ||||
|                         }} | ||||
|                         style={{ overflow: 'hidden' }} | ||||
|                     > | ||||
|                         {children.map((child) => ( | ||||
|                             <TreeNode | ||||
|                                 key={child.id} | ||||
|                                 node={child} | ||||
|                                 level={level + 1} | ||||
|                                 expanded={expanded} | ||||
|                                 loading={loading} | ||||
|                                 loadedChildren={loadedChildren} | ||||
|                                 hasMoreChildren={hasMoreChildren} | ||||
|                                 onToggle={onToggle} | ||||
|                                 onNodeClick={onNodeClick} | ||||
|                                 defaultIcon={DefaultIcon} | ||||
|                                 defaultFolderIcon={DefaultFolderIcon} | ||||
|                                 defaultIconProps={defaultIconProps} | ||||
|                                 defaultFolderIconProps={defaultFolderIconProps} | ||||
|                                 selectable={selectable} | ||||
|                                 selectedId={selectedId} | ||||
|                                 onSelect={onSelect} | ||||
|                                 className="mt-0.5" | ||||
|                                 renderHoverComponent={renderHoverComponent} | ||||
|                                 renderActionsComponent={renderActionsComponent} | ||||
|                                 loadingNodeIds={loadingNodeIds} | ||||
|                             /> | ||||
|                         ))} | ||||
|                         {isLoading ? ( | ||||
|                             <TreeItemSkeleton | ||||
|                                 style={{ | ||||
|                                     paddingLeft: `${level + 2 * 16 + 8}px`, | ||||
|                                 }} | ||||
|                             /> | ||||
|                         ) : null} | ||||
|                     </motion.div> | ||||
|                 )} | ||||
|             </AnimatePresence> | ||||
|         </div> | ||||
|     ); | ||||
| } | ||||
|  | ||||
| function findNodeById< | ||||
|     Type extends string, | ||||
|     Context extends Record<Type, unknown>, | ||||
| >( | ||||
|     nodes: TreeNode<Type, Context>[], | ||||
|     id: string, | ||||
|     initialPath: TreeNode<Type, Context>[] = [] | ||||
| ): { node: TreeNode<Type, Context> | null; path: TreeNode<Type, Context>[] } { | ||||
|     const path: TreeNode<Type, Context>[] = [...initialPath]; | ||||
|     for (const node of nodes) { | ||||
|         if (node.id === id) return { node, path }; | ||||
|         if (node.children) { | ||||
|             const found = findNodeById(node.children, id, [...path, node]); | ||||
|             if (found.node) { | ||||
|                 return found; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     return { node: null, path }; | ||||
| } | ||||
							
								
								
									
										41
									
								
								src/components/tree-view/tree.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/components/tree-view/tree.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| import type { LucideIcon } from 'lucide-react'; | ||||
| import type React from 'react'; | ||||
|  | ||||
| export interface TreeNode< | ||||
|     Type extends string, | ||||
|     Context extends Record<Type, unknown>, | ||||
| > { | ||||
|     id: string; | ||||
|     name: string; | ||||
|     isFolder?: boolean; | ||||
|     children?: TreeNode<Type, Context>[]; | ||||
|     icon?: LucideIcon; | ||||
|     iconProps?: React.ComponentProps<LucideIcon>; | ||||
|     labelProps?: React.ComponentProps<'span'>; | ||||
|     type: Type; | ||||
|     unselectable?: boolean; | ||||
|     tooltip?: string; | ||||
|     context: Context[Type]; | ||||
|     empty?: boolean; | ||||
|     className?: string; | ||||
| } | ||||
|  | ||||
| export type FetchChildrenFunction< | ||||
|     Type extends string, | ||||
|     Context extends Record<Type, unknown>, | ||||
| > = ( | ||||
|     nodeId: string, | ||||
|     nodeType: Type, | ||||
|     nodeContext: Context[Type] | ||||
| ) => Promise<TreeNode<Type, Context>[]>; | ||||
|  | ||||
| export interface SelectableTreeProps< | ||||
|     Type extends string, | ||||
|     Context extends Record<Type, unknown>, | ||||
| > { | ||||
|     enabled: boolean; | ||||
|     defaultSelectedId?: string; | ||||
|     onSelectedChange?: (node: TreeNode<Type, Context>) => void; | ||||
|     selectedId?: string; | ||||
|     setSelectedId?: React.Dispatch<React.SetStateAction<string | undefined>>; | ||||
| } | ||||
							
								
								
									
										153
									
								
								src/components/tree-view/use-tree.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								src/components/tree-view/use-tree.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,153 @@ | ||||
| import type { Dispatch, SetStateAction } from 'react'; | ||||
| import { useState, useCallback, useMemo } from 'react'; | ||||
| import type { TreeNode, FetchChildrenFunction } from './tree'; | ||||
|  | ||||
| export interface ExpandedState { | ||||
|     [key: string]: boolean; | ||||
| } | ||||
|  | ||||
| interface LoadingState { | ||||
|     [key: string]: boolean; | ||||
| } | ||||
|  | ||||
| interface LoadedChildren< | ||||
|     Type extends string, | ||||
|     Context extends Record<Type, unknown>, | ||||
| > { | ||||
|     [key: string]: TreeNode<Type, Context>[]; | ||||
| } | ||||
|  | ||||
| interface HasMoreChildrenState { | ||||
|     [key: string]: boolean; | ||||
| } | ||||
|  | ||||
| export function useTree< | ||||
|     Type extends string, | ||||
|     Context extends Record<Type, unknown>, | ||||
| >({ | ||||
|     fetchChildren, | ||||
|     expanded: expandedProp, | ||||
|     setExpanded: setExpandedProp, | ||||
| }: { | ||||
|     fetchChildren?: FetchChildrenFunction<Type, Context>; | ||||
|     expanded?: ExpandedState; | ||||
|     setExpanded?: Dispatch<SetStateAction<ExpandedState>>; | ||||
| }) { | ||||
|     const [expandedInternal, setExpandedInternal] = useState<ExpandedState>({}); | ||||
|  | ||||
|     const expanded = useMemo( | ||||
|         () => expandedProp ?? expandedInternal, | ||||
|         [expandedProp, expandedInternal] | ||||
|     ); | ||||
|     const setExpanded = useCallback( | ||||
|         (value: SetStateAction<ExpandedState>) => { | ||||
|             if (setExpandedProp) { | ||||
|                 setExpandedProp(value); | ||||
|             } else { | ||||
|                 setExpandedInternal(value); | ||||
|             } | ||||
|         }, | ||||
|         [setExpandedProp, setExpandedInternal] | ||||
|     ); | ||||
|  | ||||
|     const [loading, setLoading] = useState<LoadingState>({}); | ||||
|     const [loadedChildren, setLoadedChildren] = useState< | ||||
|         LoadedChildren<Type, Context> | ||||
|     >({}); | ||||
|     const [hasMoreChildren, setHasMoreChildren] = | ||||
|         useState<HasMoreChildrenState>({}); | ||||
|  | ||||
|     const mergeChildren = useCallback( | ||||
|         ( | ||||
|             staticChildren: TreeNode<Type, Context>[] = [], | ||||
|             fetchedChildren: TreeNode<Type, Context>[] = [] | ||||
|         ) => { | ||||
|             const fetchedChildrenIds = new Set( | ||||
|                 fetchedChildren.map((child) => child.id) | ||||
|             ); | ||||
|             const uniqueStaticChildren = staticChildren.filter( | ||||
|                 (child) => !fetchedChildrenIds.has(child.id) | ||||
|             ); | ||||
|             return [...uniqueStaticChildren, ...fetchedChildren]; | ||||
|         }, | ||||
|         [] | ||||
|     ); | ||||
|  | ||||
|     const toggleNode = useCallback( | ||||
|         async ( | ||||
|             nodeId: string, | ||||
|             nodeType: Type, | ||||
|             nodeContext: Context[Type], | ||||
|             staticChildren?: TreeNode<Type, Context>[] | ||||
|         ) => { | ||||
|             if (expanded[nodeId]) { | ||||
|                 // If we're collapsing, just update expanded state | ||||
|                 setExpanded((prev) => ({ ...prev, [nodeId]: false })); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             // Get any previously fetched children | ||||
|             const previouslyFetchedChildren = loadedChildren[nodeId] || []; | ||||
|  | ||||
|             // If we have static children, merge them with any previously fetched children | ||||
|             if (staticChildren?.length) { | ||||
|                 const mergedChildren = mergeChildren( | ||||
|                     staticChildren, | ||||
|                     previouslyFetchedChildren | ||||
|                 ); | ||||
|                 setLoadedChildren((prev) => ({ | ||||
|                     ...prev, | ||||
|                     [nodeId]: mergedChildren, | ||||
|                 })); | ||||
|  | ||||
|                 // Only show "more loading" if we haven't fetched children before | ||||
|                 setHasMoreChildren((prev) => ({ | ||||
|                     ...prev, | ||||
|                     [nodeId]: !previouslyFetchedChildren.length, | ||||
|                 })); | ||||
|             } | ||||
|  | ||||
|             // Set expanded state immediately to show static/previously fetched children | ||||
|             setExpanded((prev) => ({ ...prev, [nodeId]: true })); | ||||
|  | ||||
|             // If we haven't loaded dynamic children yet | ||||
|             if (!previouslyFetchedChildren.length) { | ||||
|                 setLoading((prev) => ({ ...prev, [nodeId]: true })); | ||||
|                 try { | ||||
|                     const fetchedChildren = await fetchChildren?.( | ||||
|                         nodeId, | ||||
|                         nodeType, | ||||
|                         nodeContext | ||||
|                     ); | ||||
|                     // Merge static and newly fetched children | ||||
|                     const allChildren = mergeChildren( | ||||
|                         staticChildren || [], | ||||
|                         fetchedChildren | ||||
|                     ); | ||||
|  | ||||
|                     setLoadedChildren((prev) => ({ | ||||
|                         ...prev, | ||||
|                         [nodeId]: allChildren, | ||||
|                     })); | ||||
|                     setHasMoreChildren((prev) => ({ | ||||
|                         ...prev, | ||||
|                         [nodeId]: false, | ||||
|                     })); | ||||
|                 } catch (error) { | ||||
|                     console.error('Error loading children:', error); | ||||
|                 } finally { | ||||
|                     setLoading((prev) => ({ ...prev, [nodeId]: false })); | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         [expanded, loadedChildren, fetchChildren, mergeChildren, setExpanded] | ||||
|     ); | ||||
|  | ||||
|     return { | ||||
|         expanded, | ||||
|         loading, | ||||
|         loadedChildren, | ||||
|         hasMoreChildren, | ||||
|         toggleNode, | ||||
|     }; | ||||
| } | ||||
| @@ -12,6 +12,8 @@ export interface CanvasContext { | ||||
|     }) => void; | ||||
|     setOverlapGraph: (graph: Graph<string>) => void; | ||||
|     overlapGraph: Graph<string>; | ||||
|     setShowFilter: React.Dispatch<React.SetStateAction<boolean>>; | ||||
|     showFilter: boolean; | ||||
| } | ||||
|  | ||||
| export const canvasContext = createContext<CanvasContext>({ | ||||
| @@ -19,4 +21,6 @@ export const canvasContext = createContext<CanvasContext>({ | ||||
|     fitView: emptyFn, | ||||
|     setOverlapGraph: emptyFn, | ||||
|     overlapGraph: createGraph(), | ||||
|     setShowFilter: emptyFn, | ||||
|     showFilter: false, | ||||
| }); | ||||
|   | ||||
| @@ -1,26 +1,56 @@ | ||||
| import React, { type ReactNode, useCallback, useState } from 'react'; | ||||
| import React, { | ||||
|     type ReactNode, | ||||
|     useCallback, | ||||
|     useState, | ||||
|     useEffect, | ||||
|     useRef, | ||||
| } from 'react'; | ||||
| import { canvasContext } from './canvas-context'; | ||||
| import { useChartDB } from '@/hooks/use-chartdb'; | ||||
| import { | ||||
|     adjustTablePositions, | ||||
|     shouldShowTablesBySchemaFilter, | ||||
| } from '@/lib/domain/db-table'; | ||||
| import { adjustTablePositions } 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'; | ||||
| import { useDiagramFilter } from '../diagram-filter-context/use-diagram-filter'; | ||||
| import { filterTable } from '@/lib/domain/diagram-filter/filter'; | ||||
| import { defaultSchemas } from '@/lib/data/default-schemas'; | ||||
|  | ||||
| interface CanvasProviderProps { | ||||
|     children: ReactNode; | ||||
| } | ||||
|  | ||||
| export const CanvasProvider = ({ children }: CanvasProviderProps) => { | ||||
|     const { tables, relationships, updateTablesState, filteredSchemas } = | ||||
|         useChartDB(); | ||||
|     const { | ||||
|         tables, | ||||
|         relationships, | ||||
|         updateTablesState, | ||||
|         databaseType, | ||||
|         areas, | ||||
|         diagramId, | ||||
|     } = useChartDB(); | ||||
|     const { filter, loading: filterLoading } = useDiagramFilter(); | ||||
|     const { fitView } = useReactFlow(); | ||||
|     const [overlapGraph, setOverlapGraph] = | ||||
|         useState<Graph<string>>(createGraph()); | ||||
|  | ||||
|     const [showFilter, setShowFilter] = useState(false); | ||||
|     const diagramIdActiveFilterRef = useRef<string>(); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (filterLoading) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (diagramIdActiveFilterRef.current === diagramId) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         diagramIdActiveFilterRef.current = diagramId; | ||||
|  | ||||
|         setShowFilter(true); | ||||
|     }, [filterLoading, diagramId]); | ||||
|  | ||||
|     const reorderTables = useCallback( | ||||
|         ( | ||||
|             options: { updateHistory?: boolean } = { | ||||
| @@ -30,9 +60,19 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => { | ||||
|             const newTables = adjustTablePositions({ | ||||
|                 relationships, | ||||
|                 tables: tables.filter((table) => | ||||
|                     shouldShowTablesBySchemaFilter(table, filteredSchemas) | ||||
|                     filterTable({ | ||||
|                         table: { | ||||
|                             id: table.id, | ||||
|                             schema: table.schema, | ||||
|                         }, | ||||
|                         filter, | ||||
|                         options: { | ||||
|                             defaultSchema: defaultSchemas[databaseType], | ||||
|                         }, | ||||
|                     }) | ||||
|                 ), | ||||
|                 mode: 'all', // Use 'all' mode for manual reordering | ||||
|                 areas, | ||||
|                 mode: 'all', | ||||
|             }); | ||||
|  | ||||
|             const updatedOverlapGraph = findOverlappingTables({ | ||||
| @@ -67,7 +107,15 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => { | ||||
|                 }); | ||||
|             }, 500); | ||||
|         }, | ||||
|         [filteredSchemas, relationships, tables, updateTablesState, fitView] | ||||
|         [ | ||||
|             filter, | ||||
|             relationships, | ||||
|             tables, | ||||
|             updateTablesState, | ||||
|             fitView, | ||||
|             databaseType, | ||||
|             areas, | ||||
|         ] | ||||
|     ); | ||||
|  | ||||
|     return ( | ||||
| @@ -77,6 +125,8 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => { | ||||
|                 fitView, | ||||
|                 setOverlapGraph, | ||||
|                 overlapGraph, | ||||
|                 setShowFilter, | ||||
|                 showFilter, | ||||
|             }} | ||||
|         > | ||||
|             {children} | ||||
|   | ||||
| @@ -78,8 +78,8 @@ export interface ChartDBContext { | ||||
|     events: EventEmitter<ChartDBEvent>; | ||||
|     readonly?: boolean; | ||||
|  | ||||
|     filteredSchemas?: string[]; | ||||
|     filterSchemas: (schemaIds: string[]) => void; | ||||
|     highlightedCustomType?: DBCustomType; | ||||
|     highlightCustomTypeId: (id?: string) => void; | ||||
|  | ||||
|     // General operations | ||||
|     updateDiagramId: (id: string) => Promise<void>; | ||||
| @@ -92,6 +92,10 @@ export interface ChartDBContext { | ||||
|     updateDiagramUpdatedAt: () => Promise<void>; | ||||
|     clearDiagramData: () => Promise<void>; | ||||
|     deleteDiagram: () => Promise<void>; | ||||
|     updateDiagramData: ( | ||||
|         diagram: Diagram, | ||||
|         options?: { forceUpdateStorage?: boolean } | ||||
|     ) => Promise<void>; | ||||
|  | ||||
|     // Database type operations | ||||
|     updateDatabaseType: (databaseType: DatabaseType) => Promise<void>; | ||||
| @@ -289,8 +293,7 @@ export const chartDBContext = createContext<ChartDBContext>({ | ||||
|     areas: [], | ||||
|     customTypes: [], | ||||
|     schemas: [], | ||||
|     filteredSchemas: [], | ||||
|     filterSchemas: emptyFn, | ||||
|     highlightCustomTypeId: emptyFn, | ||||
|     currentDiagram: { | ||||
|         id: '', | ||||
|         name: '', | ||||
| @@ -308,6 +311,7 @@ export const chartDBContext = createContext<ChartDBContext>({ | ||||
|     loadDiagramFromData: emptyFn, | ||||
|     clearDiagramData: emptyFn, | ||||
|     deleteDiagram: emptyFn, | ||||
|     updateDiagramData: emptyFn, | ||||
|  | ||||
|     // Database type operations | ||||
|     updateDatabaseType: emptyFn, | ||||
|   | ||||
| @@ -1,12 +1,15 @@ | ||||
| import React, { useCallback, useMemo, useState } from 'react'; | ||||
| import type { DBTable } from '@/lib/domain/db-table'; | ||||
| import { deepCopy, generateId } from '@/lib/utils'; | ||||
| import { randomColor } from '@/lib/colors'; | ||||
| import { defaultTableColor, defaultAreaColor, viewColor } from '@/lib/colors'; | ||||
| import type { ChartDBContext, ChartDBEvent } from './chartdb-context'; | ||||
| import { chartDBContext } from './chartdb-context'; | ||||
| import { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import type { DBField } from '@/lib/domain/db-field'; | ||||
| import type { DBIndex } from '@/lib/domain/db-index'; | ||||
| import { | ||||
|     getTableIndexesWithPrimaryKey, | ||||
|     type DBIndex, | ||||
| } from '@/lib/domain/db-index'; | ||||
| import type { DBRelationship } from '@/lib/domain/db-relationship'; | ||||
| import { useStorage } from '@/hooks/use-storage'; | ||||
| import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack'; | ||||
| @@ -17,7 +20,6 @@ import { | ||||
|     databasesWithSchemas, | ||||
|     schemaNameToSchemaId, | ||||
| } from '@/lib/domain/db-schema'; | ||||
| import { useLocalConfig } from '@/hooks/use-local-config'; | ||||
| import { defaultSchemas } from '@/lib/data/default-schemas'; | ||||
| import { useEventEmitter } from 'ahooks'; | ||||
| import type { DBDependency } from '@/lib/domain/db-dependency'; | ||||
| @@ -39,11 +41,12 @@ export const ChartDBProvider: React.FC< | ||||
|     React.PropsWithChildren<ChartDBProviderProps> | ||||
| > = ({ children, diagram, readonly: readonlyProp }) => { | ||||
|     const { hasDiff } = useDiff(); | ||||
|     let db = useStorage(); | ||||
|     const dbStorage = useStorage(); | ||||
|     let db = dbStorage; | ||||
|     const events = useEventEmitter<ChartDBEvent>(); | ||||
|     const { setSchemasFilter, schemasFilter } = useLocalConfig(); | ||||
|     const { addUndoAction, resetRedoStack, resetUndoStack } = | ||||
|         useRedoUndoStack(); | ||||
|  | ||||
|     const [diagramId, setDiagramId] = useState(''); | ||||
|     const [diagramName, setDiagramName] = useState(''); | ||||
|     const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date()); | ||||
| @@ -65,8 +68,12 @@ export const ChartDBProvider: React.FC< | ||||
|     const [customTypes, setCustomTypes] = useState<DBCustomType[]>( | ||||
|         diagram?.customTypes ?? [] | ||||
|     ); | ||||
|  | ||||
|     const { events: diffEvents } = useDiff(); | ||||
|  | ||||
|     const [highlightedCustomTypeId, setHighlightedCustomTypeId] = | ||||
|         useState<string>(); | ||||
|  | ||||
|     const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => { | ||||
|         const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data; | ||||
|         setTables((tables) => | ||||
| @@ -85,7 +92,10 @@ export const ChartDBProvider: React.FC< | ||||
|  | ||||
|     diffEvents.useSubscription(diffCalculatedHandler); | ||||
|  | ||||
|     const defaultSchemaName = defaultSchemas[databaseType]; | ||||
|     const defaultSchemaName = useMemo( | ||||
|         () => defaultSchemas[databaseType], | ||||
|         [databaseType] | ||||
|     ); | ||||
|  | ||||
|     const readonly = useMemo( | ||||
|         () => readonlyProp ?? hasDiff ?? false, | ||||
| @@ -106,9 +116,11 @@ export const ChartDBProvider: React.FC< | ||||
|                               .filter((schema) => !!schema) as string[] | ||||
|                       ), | ||||
|                   ] | ||||
|                       .sort((a, b) => | ||||
|                           a === defaultSchemaName ? -1 : a.localeCompare(b) | ||||
|                       ) | ||||
|                       .sort((a, b) => { | ||||
|                           if (a === defaultSchemaName) return -1; | ||||
|                           if (b === defaultSchemaName) return 1; | ||||
|                           return a.localeCompare(b); | ||||
|                       }) | ||||
|                       .map( | ||||
|                           (schema): DBSchema => ({ | ||||
|                               id: schemaNameToSchemaId(schema), | ||||
| @@ -122,34 +134,6 @@ export const ChartDBProvider: React.FC< | ||||
|         [tables, defaultSchemaName, databaseType] | ||||
|     ); | ||||
|  | ||||
|     const filterSchemas: ChartDBContext['filterSchemas'] = useCallback( | ||||
|         (schemaIds) => { | ||||
|             setSchemasFilter((prev) => ({ | ||||
|                 ...prev, | ||||
|                 [diagramId]: schemaIds, | ||||
|             })); | ||||
|         }, | ||||
|         [diagramId, setSchemasFilter] | ||||
|     ); | ||||
|  | ||||
|     const filteredSchemas: ChartDBContext['filteredSchemas'] = useMemo(() => { | ||||
|         if (schemas.length === 0) { | ||||
|             return undefined; | ||||
|         } | ||||
|  | ||||
|         const schemasFilterFromCache = | ||||
|             (schemasFilter[diagramId] ?? []).length === 0 | ||||
|                 ? undefined // in case of empty filter, skip cache | ||||
|                 : schemasFilter[diagramId]; | ||||
|  | ||||
|         return ( | ||||
|             schemasFilterFromCache ?? [ | ||||
|                 schemas.find((s) => s.name === defaultSchemaName)?.id ?? | ||||
|                     schemas[0]?.id, | ||||
|             ] | ||||
|         ); | ||||
|     }, [schemasFilter, diagramId, schemas, defaultSchemaName]); | ||||
|  | ||||
|     const currentDiagram: Diagram = useMemo( | ||||
|         () => ({ | ||||
|             id: diagramId, | ||||
| @@ -304,22 +288,27 @@ export const ChartDBProvider: React.FC< | ||||
|     ); | ||||
|  | ||||
|     const addTables: ChartDBContext['addTables'] = useCallback( | ||||
|         async (tables: DBTable[], options = { updateHistory: true }) => { | ||||
|             setTables((currentTables) => [...currentTables, ...tables]); | ||||
|         async (tablesToAdd: DBTable[], options = { updateHistory: true }) => { | ||||
|             setTables((currentTables) => [...currentTables, ...tablesToAdd]); | ||||
|             const updatedAt = new Date(); | ||||
|             setDiagramUpdatedAt(updatedAt); | ||||
|             await Promise.all([ | ||||
|                 db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), | ||||
|                 ...tables.map((table) => db.addTable({ diagramId, table })), | ||||
|                 ...tablesToAdd.map((table) => | ||||
|                     db.addTable({ diagramId, table }) | ||||
|                 ), | ||||
|             ]); | ||||
|  | ||||
|             events.emit({ action: 'add_tables', data: { tables } }); | ||||
|             events.emit({ | ||||
|                 action: 'add_tables', | ||||
|                 data: { tables: tablesToAdd }, | ||||
|             }); | ||||
|  | ||||
|             if (options.updateHistory) { | ||||
|                 addUndoAction({ | ||||
|                     action: 'addTables', | ||||
|                     redoData: { tables }, | ||||
|                     undoData: { tableIds: tables.map((t) => t.id) }, | ||||
|                     redoData: { tables: tablesToAdd }, | ||||
|                     undoData: { tableIds: tablesToAdd.map((t) => t.id) }, | ||||
|                 }); | ||||
|                 resetRedoStack(); | ||||
|             } | ||||
| @@ -356,12 +345,17 @@ export const ChartDBProvider: React.FC< | ||||
|                     }, | ||||
|                 ], | ||||
|                 indexes: [], | ||||
|                 color: randomColor(), | ||||
|                 color: attributes?.isView ? viewColor : defaultTableColor, | ||||
|                 createdAt: Date.now(), | ||||
|                 isView: false, | ||||
|                 order: tables.length, | ||||
|                 ...attributes, | ||||
|             }; | ||||
|  | ||||
|             table.indexes = getTableIndexesWithPrimaryKey({ | ||||
|                 table, | ||||
|             }); | ||||
|  | ||||
|             await addTable(table); | ||||
|  | ||||
|             return table; | ||||
| @@ -653,17 +647,30 @@ export const ChartDBProvider: React.FC< | ||||
|             options = { updateHistory: true } | ||||
|         ) => { | ||||
|             const prevField = getField(tableId, fieldId); | ||||
|  | ||||
|             const updateTableFn = (table: DBTable) => { | ||||
|                 const updatedTable: DBTable = { | ||||
|                     ...table, | ||||
|                     fields: table.fields.map((f) => | ||||
|                         f.id === fieldId ? { ...f, ...field } : f | ||||
|                     ), | ||||
|                 } satisfies DBTable; | ||||
|  | ||||
|                 updatedTable.indexes = getTableIndexesWithPrimaryKey({ | ||||
|                     table: updatedTable, | ||||
|                 }); | ||||
|  | ||||
|                 return updatedTable; | ||||
|             }; | ||||
|  | ||||
|             setTables((tables) => | ||||
|                 tables.map((table) => | ||||
|                     table.id === tableId | ||||
|                         ? { | ||||
|                               ...table, | ||||
|                               fields: table.fields.map((f) => | ||||
|                                   f.id === fieldId ? { ...f, ...field } : f | ||||
|                               ), | ||||
|                           } | ||||
|                         : table | ||||
|                 ) | ||||
|                 tables.map((table) => { | ||||
|                     if (table.id === tableId) { | ||||
|                         return updateTableFn(table); | ||||
|                     } | ||||
|  | ||||
|                     return table; | ||||
|                 }) | ||||
|             ); | ||||
|  | ||||
|             const table = await db.getTable({ diagramId, id: tableId }); | ||||
| @@ -678,10 +685,7 @@ export const ChartDBProvider: React.FC< | ||||
|                 db.updateTable({ | ||||
|                     id: tableId, | ||||
|                     attributes: { | ||||
|                         ...table, | ||||
|                         fields: table.fields.map((f) => | ||||
|                             f.id === fieldId ? { ...f, ...field } : f | ||||
|                         ), | ||||
|                         ...updateTableFn(table), | ||||
|                     }, | ||||
|                 }), | ||||
|             ]); | ||||
| @@ -708,19 +712,29 @@ export const ChartDBProvider: React.FC< | ||||
|             fieldId: string, | ||||
|             options = { updateHistory: true } | ||||
|         ) => { | ||||
|             const updateTableFn = (table: DBTable) => { | ||||
|                 const updatedTable: DBTable = { | ||||
|                     ...table, | ||||
|                     fields: table.fields.filter((f) => f.id !== fieldId), | ||||
|                 } satisfies DBTable; | ||||
|  | ||||
|                 updatedTable.indexes = getTableIndexesWithPrimaryKey({ | ||||
|                     table: updatedTable, | ||||
|                 }); | ||||
|  | ||||
|                 return updatedTable; | ||||
|             }; | ||||
|  | ||||
|             const fields = getTable(tableId)?.fields ?? []; | ||||
|             const prevField = getField(tableId, fieldId); | ||||
|             setTables((tables) => | ||||
|                 tables.map((table) => | ||||
|                     table.id === tableId | ||||
|                         ? { | ||||
|                               ...table, | ||||
|                               fields: table.fields.filter( | ||||
|                                   (f) => f.id !== fieldId | ||||
|                               ), | ||||
|                           } | ||||
|                         : table | ||||
|                 ) | ||||
|                 tables.map((table) => { | ||||
|                     if (table.id === tableId) { | ||||
|                         return updateTableFn(table); | ||||
|                     } | ||||
|  | ||||
|                     return table; | ||||
|                 }) | ||||
|             ); | ||||
|  | ||||
|             events.emit({ | ||||
| @@ -744,8 +758,7 @@ export const ChartDBProvider: React.FC< | ||||
|                 db.updateTable({ | ||||
|                     id: tableId, | ||||
|                     attributes: { | ||||
|                         ...table, | ||||
|                         fields: table.fields.filter((f) => f.id !== fieldId), | ||||
|                         ...updateTableFn(table), | ||||
|                     }, | ||||
|                 }), | ||||
|             ]); | ||||
| @@ -778,13 +791,23 @@ export const ChartDBProvider: React.FC< | ||||
|             options = { updateHistory: true } | ||||
|         ) => { | ||||
|             const fields = getTable(tableId)?.fields ?? []; | ||||
|             setTables((tables) => | ||||
|                 tables.map((table) => | ||||
|                     table.id === tableId | ||||
|                         ? { ...table, fields: [...table.fields, field] } | ||||
|                         : table | ||||
|                 ) | ||||
|             ); | ||||
|             setTables((tables) => { | ||||
|                 return tables.map((table) => { | ||||
|                     if (table.id === tableId) { | ||||
|                         db.updateTable({ | ||||
|                             id: tableId, | ||||
|                             attributes: { | ||||
|                                 ...table, | ||||
|                                 fields: [...table.fields, field], | ||||
|                             }, | ||||
|                         }); | ||||
|  | ||||
|                         return { ...table, fields: [...table.fields, field] }; | ||||
|                     } | ||||
|  | ||||
|                     return table; | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             events.emit({ | ||||
|                 action: 'add_field', | ||||
| @@ -805,13 +828,6 @@ export const ChartDBProvider: React.FC< | ||||
|             setDiagramUpdatedAt(updatedAt); | ||||
|             await Promise.all([ | ||||
|                 db.updateDiagram({ id: diagramId, attributes: { updatedAt } }), | ||||
|                 db.updateTable({ | ||||
|                     id: tableId, | ||||
|                     attributes: { | ||||
|                         ...table, | ||||
|                         fields: [...table.fields, field], | ||||
|                     }, | ||||
|                 }), | ||||
|             ]); | ||||
|  | ||||
|             if (options.updateHistory) { | ||||
| @@ -1098,12 +1114,15 @@ export const ChartDBProvider: React.FC< | ||||
|  | ||||
|                 const sourceFieldName = sourceField?.name ?? ''; | ||||
|  | ||||
|                 const targetTable = getTable(targetTableId); | ||||
|                 const targetTableSchema = targetTable?.schema; | ||||
|  | ||||
|                 const relationship: DBRelationship = { | ||||
|                     id: generateId(), | ||||
|                     name: `${sourceTableName}_${sourceFieldName}_fk`, | ||||
|                     sourceSchema: sourceTable?.schema, | ||||
|                     sourceTableId, | ||||
|                     targetSchema: sourceTable?.schema, | ||||
|                     targetSchema: targetTableSchema, | ||||
|                     targetTableId, | ||||
|                     sourceFieldId, | ||||
|                     targetFieldId, | ||||
| @@ -1425,7 +1444,7 @@ export const ChartDBProvider: React.FC< | ||||
|                 y: 0, | ||||
|                 width: 300, | ||||
|                 height: 200, | ||||
|                 color: randomColor(), | ||||
|                 color: defaultAreaColor, | ||||
|                 ...attributes, | ||||
|             }; | ||||
|  | ||||
| @@ -1508,22 +1527,37 @@ export const ChartDBProvider: React.FC< | ||||
|         [db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack] | ||||
|     ); | ||||
|  | ||||
|     const highlightCustomTypeId = useCallback( | ||||
|         (id?: string) => setHighlightedCustomTypeId(id), | ||||
|         [setHighlightedCustomTypeId] | ||||
|     ); | ||||
|  | ||||
|     const highlightedCustomType = useMemo(() => { | ||||
|         return highlightedCustomTypeId | ||||
|             ? customTypes.find((type) => type.id === highlightedCustomTypeId) | ||||
|             : undefined; | ||||
|     }, [highlightedCustomTypeId, customTypes]); | ||||
|  | ||||
|     const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] = | ||||
|         useCallback( | ||||
|             async (diagram) => { | ||||
|             (diagram) => { | ||||
|                 setDiagramId(diagram.id); | ||||
|                 setDiagramName(diagram.name); | ||||
|                 setDatabaseType(diagram.databaseType); | ||||
|                 setDatabaseEdition(diagram.databaseEdition); | ||||
|                 setTables(diagram?.tables ?? []); | ||||
|                 setRelationships(diagram?.relationships ?? []); | ||||
|                 setDependencies(diagram?.dependencies ?? []); | ||||
|                 setAreas(diagram?.areas ?? []); | ||||
|                 setCustomTypes(diagram?.customTypes ?? []); | ||||
|                 setTables(diagram.tables ?? []); | ||||
|                 setRelationships(diagram.relationships ?? []); | ||||
|                 setDependencies(diagram.dependencies ?? []); | ||||
|                 setAreas(diagram.areas ?? []); | ||||
|                 setCustomTypes(diagram.customTypes ?? []); | ||||
|                 setDiagramCreatedAt(diagram.createdAt); | ||||
|                 setDiagramUpdatedAt(diagram.updatedAt); | ||||
|                 setHighlightedCustomTypeId(undefined); | ||||
|  | ||||
|                 events.emit({ action: 'load_diagram', data: { diagram } }); | ||||
|  | ||||
|                 resetRedoStack(); | ||||
|                 resetUndoStack(); | ||||
|             }, | ||||
|             [ | ||||
|                 setDiagramId, | ||||
| @@ -1537,10 +1571,23 @@ export const ChartDBProvider: React.FC< | ||||
|                 setCustomTypes, | ||||
|                 setDiagramCreatedAt, | ||||
|                 setDiagramUpdatedAt, | ||||
|                 setHighlightedCustomTypeId, | ||||
|                 events, | ||||
|                 resetRedoStack, | ||||
|                 resetUndoStack, | ||||
|             ] | ||||
|         ); | ||||
|  | ||||
|     const updateDiagramData: ChartDBContext['updateDiagramData'] = useCallback( | ||||
|         async (diagram, options) => { | ||||
|             const st = options?.forceUpdateStorage ? dbStorage : db; | ||||
|             await st.deleteDiagram(diagram.id); | ||||
|             await st.addDiagram({ diagram }); | ||||
|             loadDiagramFromData(diagram); | ||||
|         }, | ||||
|         [db, dbStorage, loadDiagramFromData] | ||||
|     ); | ||||
|  | ||||
|     const loadDiagram: ChartDBContext['loadDiagram'] = useCallback( | ||||
|         async (diagramId: string) => { | ||||
|             const diagram = await db.getDiagram(diagramId, { | ||||
| @@ -1716,10 +1763,9 @@ export const ChartDBProvider: React.FC< | ||||
|                 areas, | ||||
|                 currentDiagram, | ||||
|                 schemas, | ||||
|                 filteredSchemas, | ||||
|                 events, | ||||
|                 readonly, | ||||
|                 filterSchemas, | ||||
|                 updateDiagramData, | ||||
|                 updateDiagramId, | ||||
|                 updateDiagramName, | ||||
|                 loadDiagram, | ||||
| @@ -1776,6 +1822,8 @@ export const ChartDBProvider: React.FC< | ||||
|                 removeCustomType, | ||||
|                 removeCustomTypes, | ||||
|                 updateCustomType, | ||||
|                 highlightCustomTypeId, | ||||
|                 highlightedCustomType, | ||||
|             }} | ||||
|         > | ||||
|             {children} | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import React, { useEffect } from 'react'; | ||||
| import React, { useEffect, useState } from 'react'; | ||||
| import { ConfigContext } from './config-context'; | ||||
|  | ||||
| import { useStorage } from '@/hooks/use-storage'; | ||||
| @@ -8,7 +8,7 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     children, | ||||
| }) => { | ||||
|     const { getConfig, updateConfig: updateDataConfig } = useStorage(); | ||||
|     const [config, setConfig] = React.useState<ChartDBConfig | undefined>(); | ||||
|     const [config, setConfig] = useState<ChartDBConfig | undefined>(); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         const loadConfig = async () => { | ||||
| @@ -45,7 +45,12 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|         <ConfigContext.Provider value={{ config, updateConfig }}> | ||||
|         <ConfigContext.Provider | ||||
|             value={{ | ||||
|                 config, | ||||
|                 updateConfig, | ||||
|             }} | ||||
|         > | ||||
|             {children} | ||||
|         </ConfigContext.Provider> | ||||
|     ); | ||||
|   | ||||
| @@ -0,0 +1,50 @@ | ||||
| import type { DBSchema } from '@/lib/domain'; | ||||
| import type { | ||||
|     DiagramFilter, | ||||
|     FilterTableInfo, | ||||
| } from '@/lib/domain/diagram-filter/diagram-filter'; | ||||
| import { emptyFn } from '@/lib/utils'; | ||||
| import { createContext } from 'react'; | ||||
|  | ||||
| export interface DiagramFilterContext { | ||||
|     filter?: DiagramFilter; | ||||
|     loading: boolean; | ||||
|  | ||||
|     hasActiveFilter: boolean; | ||||
|     schemasDisplayed: DBSchema[]; | ||||
|  | ||||
|     clearSchemaIdsFilter: () => void; | ||||
|     clearTableIdsFilter: () => void; | ||||
|  | ||||
|     setTableIdsFilterEmpty: () => void; | ||||
|  | ||||
|     // reset | ||||
|     resetFilter: () => void; | ||||
|  | ||||
|     toggleSchemaFilter: (schemaId: string) => void; | ||||
|     toggleTableFilter: (tableId: string) => void; | ||||
|     addSchemaToFilter: (schemaId: string) => void; | ||||
|     addTablesToFilter: (attrs: { | ||||
|         tableIds?: string[]; | ||||
|         filterCallback?: (table: FilterTableInfo) => boolean; | ||||
|     }) => void; | ||||
|     removeTablesFromFilter: (attrs: { | ||||
|         tableIds?: string[]; | ||||
|         filterCallback?: (table: FilterTableInfo) => boolean; | ||||
|     }) => void; | ||||
| } | ||||
|  | ||||
| export const diagramFilterContext = createContext<DiagramFilterContext>({ | ||||
|     hasActiveFilter: false, | ||||
|     clearSchemaIdsFilter: emptyFn, | ||||
|     clearTableIdsFilter: emptyFn, | ||||
|     setTableIdsFilterEmpty: emptyFn, | ||||
|     resetFilter: emptyFn, | ||||
|     toggleSchemaFilter: emptyFn, | ||||
|     toggleTableFilter: emptyFn, | ||||
|     addSchemaToFilter: emptyFn, | ||||
|     schemasDisplayed: [], | ||||
|     addTablesToFilter: emptyFn, | ||||
|     removeTablesFromFilter: emptyFn, | ||||
|     loading: false, | ||||
| }); | ||||
							
								
								
									
										559
									
								
								src/context/diagram-filter-context/diagram-filter-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										559
									
								
								src/context/diagram-filter-context/diagram-filter-provider.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,559 @@ | ||||
| import React, { | ||||
|     useCallback, | ||||
|     useEffect, | ||||
|     useMemo, | ||||
|     useRef, | ||||
|     useState, | ||||
| } from 'react'; | ||||
| import type { DiagramFilterContext } from './diagram-filter-context'; | ||||
| import { diagramFilterContext } from './diagram-filter-context'; | ||||
| import type { | ||||
|     DiagramFilter, | ||||
|     FilterTableInfo, | ||||
| } from '@/lib/domain/diagram-filter/diagram-filter'; | ||||
| import { | ||||
|     reduceFilter, | ||||
|     spreadFilterTables, | ||||
| } from '@/lib/domain/diagram-filter/diagram-filter'; | ||||
| import { useStorage } from '@/hooks/use-storage'; | ||||
| import { useChartDB } from '@/hooks/use-chartdb'; | ||||
| import { filterTable } from '@/lib/domain/diagram-filter/filter'; | ||||
| import { databasesWithSchemas, schemaNameToSchemaId } from '@/lib/domain'; | ||||
| import { defaultSchemas } from '@/lib/data/default-schemas'; | ||||
| import type { ChartDBEvent } from '../chartdb-context/chartdb-context'; | ||||
|  | ||||
| export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     children, | ||||
| }) => { | ||||
|     const { diagramId, tables, schemas, databaseType, events } = useChartDB(); | ||||
|     const { getDiagramFilter, updateDiagramFilter } = useStorage(); | ||||
|     const [filter, setFilter] = useState<DiagramFilter>({}); | ||||
|     const [loading, setLoading] = useState<boolean>(true); | ||||
|  | ||||
|     const allSchemasIds = useMemo(() => { | ||||
|         return schemas.map((schema) => schema.id); | ||||
|     }, [schemas]); | ||||
|  | ||||
|     const allTables: FilterTableInfo[] = useMemo(() => { | ||||
|         return tables.map( | ||||
|             (table) => | ||||
|                 ({ | ||||
|                     id: table.id, | ||||
|                     schemaId: table.schema | ||||
|                         ? schemaNameToSchemaId(table.schema) | ||||
|                         : defaultSchemas[databaseType], | ||||
|                     schema: table.schema ?? defaultSchemas[databaseType], | ||||
|                     areaId: table.parentAreaId ?? undefined, | ||||
|                 }) satisfies FilterTableInfo | ||||
|         ); | ||||
|     }, [tables, databaseType]); | ||||
|  | ||||
|     const diagramIdOfLoadedFilter = useRef<string | null>(null); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (diagramId && diagramId === diagramIdOfLoadedFilter.current) { | ||||
|             updateDiagramFilter(diagramId, filter); | ||||
|         } | ||||
|     }, [diagramId, filter, updateDiagramFilter]); | ||||
|  | ||||
|     // Reset filter when diagram changes | ||||
|     useEffect(() => { | ||||
|         if (diagramIdOfLoadedFilter.current === diagramId) { | ||||
|             // If the diagramId hasn't changed, do not reset the filter | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         setLoading(true); | ||||
|  | ||||
|         const loadFilterFromStorage = async (diagramId: string) => { | ||||
|             if (diagramId) { | ||||
|                 const storedFilter = await getDiagramFilter(diagramId); | ||||
|  | ||||
|                 let filterToSet = storedFilter; | ||||
|  | ||||
|                 if (!filterToSet) { | ||||
|                     // If no filter is stored, set default based on database type | ||||
|                     filterToSet = | ||||
|                         schemas.length > 1 | ||||
|                             ? { schemaIds: [schemas[0].id] } | ||||
|                             : {}; | ||||
|                 } | ||||
|  | ||||
|                 setFilter(filterToSet); | ||||
|             } | ||||
|  | ||||
|             setLoading(false); | ||||
|         }; | ||||
|  | ||||
|         setFilter({}); | ||||
|  | ||||
|         if (diagramId) { | ||||
|             loadFilterFromStorage(diagramId); | ||||
|             diagramIdOfLoadedFilter.current = diagramId; | ||||
|         } | ||||
|     }, [diagramId, getDiagramFilter, schemas]); | ||||
|  | ||||
|     const clearSchemaIds: DiagramFilterContext['clearSchemaIdsFilter'] = | ||||
|         useCallback(() => { | ||||
|             setFilter( | ||||
|                 (prev) => | ||||
|                     ({ | ||||
|                         ...prev, | ||||
|                         schemaIds: undefined, | ||||
|                     }) satisfies DiagramFilter | ||||
|             ); | ||||
|         }, []); | ||||
|  | ||||
|     const clearTableIds: DiagramFilterContext['clearTableIdsFilter'] = | ||||
|         useCallback(() => { | ||||
|             setFilter( | ||||
|                 (prev) => | ||||
|                     ({ | ||||
|                         ...prev, | ||||
|                         tableIds: undefined, | ||||
|                     }) satisfies DiagramFilter | ||||
|             ); | ||||
|         }, []); | ||||
|  | ||||
|     const setTableIdsEmpty: DiagramFilterContext['setTableIdsFilterEmpty'] = | ||||
|         useCallback(() => { | ||||
|             setFilter( | ||||
|                 (prev) => | ||||
|                     ({ | ||||
|                         ...prev, | ||||
|                         tableIds: [], | ||||
|                     }) satisfies DiagramFilter | ||||
|             ); | ||||
|         }, []); | ||||
|  | ||||
|     // Reset filter | ||||
|     const resetFilter: DiagramFilterContext['resetFilter'] = useCallback(() => { | ||||
|         setFilter({}); | ||||
|     }, []); | ||||
|  | ||||
|     const toggleSchemaFilter: DiagramFilterContext['toggleSchemaFilter'] = | ||||
|         useCallback( | ||||
|             (schemaId: string) => { | ||||
|                 setFilter((prev) => { | ||||
|                     const currentSchemaIds = prev.schemaIds; | ||||
|  | ||||
|                     // Check if schema is currently visible | ||||
|                     const isSchemaVisible = !allTables.some( | ||||
|                         (table) => | ||||
|                             table.schemaId === schemaId && | ||||
|                             filterTable({ | ||||
|                                 table: { | ||||
|                                     id: table.id, | ||||
|                                     schema: table.schema, | ||||
|                                 }, | ||||
|                                 filter: prev, | ||||
|                                 options: { | ||||
|                                     defaultSchema: defaultSchemas[databaseType], | ||||
|                                 }, | ||||
|                             }) === false | ||||
|                     ); | ||||
|  | ||||
|                     let newSchemaIds: string[] | undefined; | ||||
|                     let newTableIds: string[] | undefined = prev.tableIds; | ||||
|  | ||||
|                     if (isSchemaVisible) { | ||||
|                         // Schema is visible, make it not visible | ||||
|                         if (!currentSchemaIds) { | ||||
|                             // All schemas are visible, create filter with all except this one | ||||
|                             newSchemaIds = allSchemasIds.filter( | ||||
|                                 (id) => id !== schemaId | ||||
|                             ); | ||||
|                         } else { | ||||
|                             // Remove this schema from the filter | ||||
|                             newSchemaIds = currentSchemaIds.filter( | ||||
|                                 (id) => id !== schemaId | ||||
|                             ); | ||||
|                         } | ||||
|  | ||||
|                         // Remove tables from this schema from tableIds if present | ||||
|                         if (prev.tableIds) { | ||||
|                             const schemaTableIds = allTables | ||||
|                                 .filter((table) => table.schemaId === schemaId) | ||||
|                                 .map((table) => table.id); | ||||
|                             newTableIds = prev.tableIds.filter( | ||||
|                                 (id) => !schemaTableIds.includes(id) | ||||
|                             ); | ||||
|                         } | ||||
|                     } else { | ||||
|                         // Schema is not visible, make it visible | ||||
|                         newSchemaIds = [ | ||||
|                             ...new Set([...(currentSchemaIds || []), schemaId]), | ||||
|                         ]; | ||||
|  | ||||
|                         // Add tables from this schema to tableIds if tableIds is defined | ||||
|                         if (prev.tableIds) { | ||||
|                             const schemaTableIds = allTables | ||||
|                                 .filter((table) => table.schemaId === schemaId) | ||||
|                                 .map((table) => table.id); | ||||
|                             newTableIds = [ | ||||
|                                 ...new Set([ | ||||
|                                     ...prev.tableIds, | ||||
|                                     ...schemaTableIds, | ||||
|                                 ]), | ||||
|                             ]; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     // Use reduceFilter to optimize and handle edge cases | ||||
|                     return reduceFilter( | ||||
|                         { | ||||
|                             schemaIds: newSchemaIds, | ||||
|                             tableIds: newTableIds, | ||||
|                         }, | ||||
|                         allTables satisfies FilterTableInfo[], | ||||
|                         { | ||||
|                             databaseWithSchemas: | ||||
|                                 databasesWithSchemas.includes(databaseType), | ||||
|                         } | ||||
|                     ); | ||||
|                 }); | ||||
|             }, | ||||
|             [allSchemasIds, allTables, databaseType] | ||||
|         ); | ||||
|  | ||||
|     const toggleTableFilterForNoSchema = useCallback( | ||||
|         (tableId: string) => { | ||||
|             setFilter((prev) => { | ||||
|                 const currentTableIds = prev.tableIds; | ||||
|  | ||||
|                 // Check if table is currently visible | ||||
|                 const isTableVisible = filterTable({ | ||||
|                     table: { id: tableId, schema: undefined }, | ||||
|                     filter: prev, | ||||
|                     options: { defaultSchema: undefined }, | ||||
|                 }); | ||||
|  | ||||
|                 let newTableIds: string[] | undefined; | ||||
|  | ||||
|                 if (isTableVisible) { | ||||
|                     // Table is visible, make it not visible | ||||
|                     if (!currentTableIds) { | ||||
|                         // All tables are visible, create filter with all except this one | ||||
|                         newTableIds = allTables | ||||
|                             .filter((t) => t.id !== tableId) | ||||
|                             .map((t) => t.id); | ||||
|                     } else { | ||||
|                         // Remove this table from the filter | ||||
|                         newTableIds = currentTableIds.filter( | ||||
|                             (id) => id !== tableId | ||||
|                         ); | ||||
|                     } | ||||
|                 } else { | ||||
|                     // Table is not visible, make it visible | ||||
|                     newTableIds = [ | ||||
|                         ...new Set([...(currentTableIds || []), tableId]), | ||||
|                     ]; | ||||
|                 } | ||||
|  | ||||
|                 // Use reduceFilter to optimize and handle edge cases | ||||
|                 return reduceFilter( | ||||
|                     { | ||||
|                         schemaIds: undefined, | ||||
|                         tableIds: newTableIds, | ||||
|                     }, | ||||
|                     allTables satisfies FilterTableInfo[], | ||||
|                     { | ||||
|                         databaseWithSchemas: | ||||
|                             databasesWithSchemas.includes(databaseType), | ||||
|                     } | ||||
|                 ); | ||||
|             }); | ||||
|         }, | ||||
|         [allTables, databaseType] | ||||
|     ); | ||||
|  | ||||
|     const toggleTableFilter: DiagramFilterContext['toggleTableFilter'] = | ||||
|         useCallback( | ||||
|             (tableId: string) => { | ||||
|                 if (!databasesWithSchemas.includes(databaseType)) { | ||||
|                     // No schemas, toggle table filter without schema context | ||||
|                     toggleTableFilterForNoSchema(tableId); | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 setFilter((prev) => { | ||||
|                     // Find the table in the tables list | ||||
|                     const tableInfo = allTables.find((t) => t.id === tableId); | ||||
|  | ||||
|                     if (!tableInfo) { | ||||
|                         return prev; | ||||
|                     } | ||||
|  | ||||
|                     // Check if table is currently visible using filterTable | ||||
|                     const isTableVisible = filterTable({ | ||||
|                         table: { | ||||
|                             id: tableInfo.id, | ||||
|                             schema: tableInfo.schema, | ||||
|                         }, | ||||
|                         filter: prev, | ||||
|                         options: { | ||||
|                             defaultSchema: defaultSchemas[databaseType], | ||||
|                         }, | ||||
|                     }); | ||||
|  | ||||
|                     let newSchemaIds = prev.schemaIds; | ||||
|                     let newTableIds = prev.tableIds; | ||||
|  | ||||
|                     if (isTableVisible) { | ||||
|                         // Table is visible, make it not visible | ||||
|  | ||||
|                         // If the table is visible due to its schema being in schemaIds | ||||
|                         if ( | ||||
|                             tableInfo?.schemaId && | ||||
|                             prev.schemaIds?.includes(tableInfo.schemaId) | ||||
|                         ) { | ||||
|                             // Remove the schema from schemaIds and add all other tables from that schema to tableIds | ||||
|                             newSchemaIds = prev.schemaIds.filter( | ||||
|                                 (id) => id !== tableInfo.schemaId | ||||
|                             ); | ||||
|  | ||||
|                             // Get all other tables from this schema (except the one being toggled) | ||||
|                             const otherTablesFromSchema = allTables | ||||
|                                 .filter( | ||||
|                                     (t) => | ||||
|                                         t.schemaId === tableInfo.schemaId && | ||||
|                                         t.id !== tableId | ||||
|                                 ) | ||||
|                                 .map((t) => t.id); | ||||
|  | ||||
|                             // Add these tables to tableIds | ||||
|                             newTableIds = [ | ||||
|                                 ...(prev.tableIds || []), | ||||
|                                 ...otherTablesFromSchema, | ||||
|                             ]; | ||||
|                         } else if (prev.tableIds?.includes(tableId)) { | ||||
|                             // Table is visible because it's in tableIds, remove it | ||||
|                             newTableIds = prev.tableIds.filter( | ||||
|                                 (id) => id !== tableId | ||||
|                             ); | ||||
|                         } else if (!prev.tableIds && !prev.schemaIds) { | ||||
|                             // No filters = all visible, create filter with all tables except this one | ||||
|                             newTableIds = allTables | ||||
|                                 .filter((t) => t.id !== tableId) | ||||
|                                 .map((t) => t.id); | ||||
|                         } | ||||
|                     } else { | ||||
|                         // Table is not visible, make it visible by adding to tableIds | ||||
|                         newTableIds = [...(prev.tableIds || []), tableId]; | ||||
|                     } | ||||
|  | ||||
|                     // Use reduceFilter to optimize and handle edge cases | ||||
|                     return reduceFilter( | ||||
|                         { | ||||
|                             schemaIds: newSchemaIds, | ||||
|                             tableIds: newTableIds, | ||||
|                         }, | ||||
|                         allTables satisfies FilterTableInfo[], | ||||
|                         { | ||||
|                             databaseWithSchemas: | ||||
|                                 databasesWithSchemas.includes(databaseType), | ||||
|                         } | ||||
|                     ); | ||||
|                 }); | ||||
|             }, | ||||
|             [allTables, databaseType, toggleTableFilterForNoSchema] | ||||
|         ); | ||||
|  | ||||
|     const addSchemaToFilter: DiagramFilterContext['addSchemaToFilter'] = | ||||
|         useCallback( | ||||
|             (schemaId: string) => { | ||||
|                 setFilter((prev) => { | ||||
|                     const currentSchemaIds = prev.schemaIds; | ||||
|                     if (!currentSchemaIds) { | ||||
|                         // No schemas are filtered | ||||
|                         return prev; | ||||
|                     } | ||||
|  | ||||
|                     // If schema is already filtered, do nothing | ||||
|                     if (currentSchemaIds.includes(schemaId)) { | ||||
|                         return prev; | ||||
|                     } | ||||
|  | ||||
|                     // Add schema to the filter | ||||
|                     const newSchemaIds = [...currentSchemaIds, schemaId]; | ||||
|  | ||||
|                     if (newSchemaIds.length === allSchemasIds.length) { | ||||
|                         // All schemas are now filtered, set to undefined | ||||
|                         return { | ||||
|                             ...prev, | ||||
|                             schemaIds: undefined, | ||||
|                         } satisfies DiagramFilter; | ||||
|                     } | ||||
|                     return { | ||||
|                         ...prev, | ||||
|                         schemaIds: newSchemaIds, | ||||
|                     } satisfies DiagramFilter; | ||||
|                 }); | ||||
|             }, | ||||
|             [allSchemasIds.length] | ||||
|         ); | ||||
|  | ||||
|     const hasActiveFilter: boolean = useMemo(() => { | ||||
|         return !!filter.schemaIds || !!filter.tableIds; | ||||
|     }, [filter]); | ||||
|  | ||||
|     const schemasDisplayed: DiagramFilterContext['schemasDisplayed'] = | ||||
|         useMemo(() => { | ||||
|             if (!hasActiveFilter) { | ||||
|                 return schemas; | ||||
|             } | ||||
|  | ||||
|             const displayedSchemaIds = new Set<string>(); | ||||
|             for (const table of allTables) { | ||||
|                 if ( | ||||
|                     filterTable({ | ||||
|                         table: { | ||||
|                             id: table.id, | ||||
|                             schema: table.schema, | ||||
|                         }, | ||||
|                         filter, | ||||
|                         options: { | ||||
|                             defaultSchema: defaultSchemas[databaseType], | ||||
|                         }, | ||||
|                     }) | ||||
|                 ) { | ||||
|                     if (table.schemaId) { | ||||
|                         displayedSchemaIds.add(table.schemaId); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return schemas.filter((schema) => | ||||
|                 displayedSchemaIds.has(schema.id) | ||||
|             ); | ||||
|         }, [hasActiveFilter, schemas, allTables, filter, databaseType]); | ||||
|  | ||||
|     const addTablesToFilter: DiagramFilterContext['addTablesToFilter'] = | ||||
|         useCallback( | ||||
|             ({ tableIds, filterCallback }) => { | ||||
|                 setFilter((prev) => { | ||||
|                     let tableIdsToAdd: string[]; | ||||
|  | ||||
|                     if (tableIds) { | ||||
|                         // If tableIds are provided, use them directly | ||||
|                         tableIdsToAdd = tableIds; | ||||
|                     } else if (filterCallback) { | ||||
|                         // If filterCallback is provided, filter tables based on it | ||||
|                         tableIdsToAdd = allTables | ||||
|                             .filter(filterCallback) | ||||
|                             .map((table) => table.id); | ||||
|                     } else { | ||||
|                         // If neither is provided, do nothing | ||||
|                         return prev; | ||||
|                     } | ||||
|  | ||||
|                     const filterByTableIds = spreadFilterTables( | ||||
|                         prev, | ||||
|                         allTables satisfies FilterTableInfo[] | ||||
|                     ); | ||||
|  | ||||
|                     const currentTableIds = filterByTableIds.tableIds || []; | ||||
|                     const newTableIds = [ | ||||
|                         ...new Set([...currentTableIds, ...tableIdsToAdd]), | ||||
|                     ]; | ||||
|  | ||||
|                     return reduceFilter( | ||||
|                         { | ||||
|                             ...filterByTableIds, | ||||
|                             tableIds: newTableIds, | ||||
|                         }, | ||||
|                         allTables satisfies FilterTableInfo[], | ||||
|                         { | ||||
|                             databaseWithSchemas: | ||||
|                                 databasesWithSchemas.includes(databaseType), | ||||
|                         } | ||||
|                     ); | ||||
|                 }); | ||||
|             }, | ||||
|             [allTables, databaseType] | ||||
|         ); | ||||
|  | ||||
|     const removeTablesFromFilter: DiagramFilterContext['removeTablesFromFilter'] = | ||||
|         useCallback( | ||||
|             ({ tableIds, filterCallback }) => { | ||||
|                 setFilter((prev) => { | ||||
|                     let tableIdsToRemovoe: string[]; | ||||
|  | ||||
|                     if (tableIds) { | ||||
|                         // If tableIds are provided, use them directly | ||||
|                         tableIdsToRemovoe = tableIds; | ||||
|                     } else if (filterCallback) { | ||||
|                         // If filterCallback is provided, filter tables based on it | ||||
|                         tableIdsToRemovoe = allTables | ||||
|                             .filter(filterCallback) | ||||
|                             .map((table) => table.id); | ||||
|                     } else { | ||||
|                         // If neither is provided, do nothing | ||||
|                         return prev; | ||||
|                     } | ||||
|  | ||||
|                     const filterByTableIds = spreadFilterTables( | ||||
|                         prev, | ||||
|                         allTables satisfies FilterTableInfo[] | ||||
|                     ); | ||||
|  | ||||
|                     const currentTableIds = filterByTableIds.tableIds || []; | ||||
|                     const newTableIds = currentTableIds.filter( | ||||
|                         (id) => !tableIdsToRemovoe.includes(id) | ||||
|                     ); | ||||
|  | ||||
|                     return reduceFilter( | ||||
|                         { | ||||
|                             ...filterByTableIds, | ||||
|                             tableIds: newTableIds, | ||||
|                         }, | ||||
|                         allTables satisfies FilterTableInfo[], | ||||
|                         { | ||||
|                             databaseWithSchemas: | ||||
|                                 databasesWithSchemas.includes(databaseType), | ||||
|                         } | ||||
|                     ); | ||||
|                 }); | ||||
|             }, | ||||
|             [allTables, databaseType] | ||||
|         ); | ||||
|  | ||||
|     const eventConsumer = useCallback( | ||||
|         (event: ChartDBEvent) => { | ||||
|             if (!hasActiveFilter) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (event.action === 'add_tables') { | ||||
|                 addTablesToFilter({ | ||||
|                     tableIds: event.data.tables.map((table) => table.id), | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|         [hasActiveFilter, addTablesToFilter] | ||||
|     ); | ||||
|  | ||||
|     events.useSubscription(eventConsumer); | ||||
|  | ||||
|     const value: DiagramFilterContext = { | ||||
|         loading, | ||||
|         filter, | ||||
|         clearSchemaIdsFilter: clearSchemaIds, | ||||
|         setTableIdsFilterEmpty: setTableIdsEmpty, | ||||
|         clearTableIdsFilter: clearTableIds, | ||||
|         resetFilter, | ||||
|         toggleSchemaFilter, | ||||
|         toggleTableFilter, | ||||
|         addSchemaToFilter, | ||||
|         hasActiveFilter, | ||||
|         schemasDisplayed, | ||||
|         addTablesToFilter, | ||||
|         removeTablesFromFilter, | ||||
|     }; | ||||
|  | ||||
|     return ( | ||||
|         <diagramFilterContext.Provider value={value}> | ||||
|             {children} | ||||
|         </diagramFilterContext.Provider> | ||||
|     ); | ||||
| }; | ||||
							
								
								
									
										4
									
								
								src/context/diagram-filter-context/use-diagram-filter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/context/diagram-filter-context/use-diagram-filter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| import { useContext } from 'react'; | ||||
| import { diagramFilterContext } from './diagram-filter-context'; | ||||
|  | ||||
| export const useDiagramFilter = () => useContext(diagramFilterContext); | ||||
| @@ -32,14 +32,20 @@ export interface DiffContext { | ||||
|     originalDiagram: Diagram | null; | ||||
|     diffMap: DiffMap; | ||||
|     hasDiff: boolean; | ||||
|     isSummaryOnly: boolean; | ||||
|  | ||||
|     calculateDiff: ({ | ||||
|         diagram, | ||||
|         newDiagram, | ||||
|         options, | ||||
|     }: { | ||||
|         diagram: Diagram; | ||||
|         newDiagram: Diagram; | ||||
|         options?: { | ||||
|             summaryOnly?: boolean; | ||||
|         }; | ||||
|     }) => void; | ||||
|     resetDiff: () => void; | ||||
|  | ||||
|     // table diff | ||||
|     checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean; | ||||
| @@ -60,6 +66,15 @@ export interface DiffContext { | ||||
|     checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean; | ||||
|     getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null; | ||||
|     getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null; | ||||
|     getFieldNewPrimaryKey: ({ fieldId }: { fieldId: string }) => boolean | null; | ||||
|     getFieldNewNullable: ({ fieldId }: { fieldId: string }) => boolean | null; | ||||
|     getFieldNewCharacterMaximumLength: ({ | ||||
|         fieldId, | ||||
|     }: { | ||||
|         fieldId: string; | ||||
|     }) => string | null; | ||||
|     getFieldNewScale: ({ fieldId }: { fieldId: string }) => number | null; | ||||
|     getFieldNewPrecision: ({ fieldId }: { fieldId: string }) => number | null; | ||||
|  | ||||
|     // relationship diff | ||||
|     checkIfNewRelationship: ({ | ||||
|   | ||||
| @@ -6,7 +6,10 @@ import type { | ||||
| } from './diff-context'; | ||||
| import { diffContext } from './diff-context'; | ||||
|  | ||||
| import { generateDiff, getDiffMapKey } from './diff-check/diff-check'; | ||||
| import { | ||||
|     generateDiff, | ||||
|     getDiffMapKey, | ||||
| } from '@/lib/domain/diff/diff-check/diff-check'; | ||||
| import type { Diagram } from '@/lib/domain/diagram'; | ||||
| import { useEventEmitter } from 'ahooks'; | ||||
| import type { DBField } from '@/lib/domain/db-field'; | ||||
| @@ -29,6 +32,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     const [fieldsChanged, setFieldsChanged] = React.useState< | ||||
|         Map<string, boolean> | ||||
|     >(new Map<string, boolean>()); | ||||
|     const [isSummaryOnly, setIsSummaryOnly] = React.useState<boolean>(false); | ||||
|  | ||||
|     const events = useEventEmitter<DiffEvent>(); | ||||
|  | ||||
| @@ -124,7 +128,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     ); | ||||
|  | ||||
|     const calculateDiff: DiffContext['calculateDiff'] = useCallback( | ||||
|         ({ diagram, newDiagram: newDiagramArg }) => { | ||||
|         ({ diagram, newDiagram: newDiagramArg, options }) => { | ||||
|             const { | ||||
|                 diffMap: newDiffs, | ||||
|                 changedTables: newChangedTables, | ||||
| @@ -136,6 +140,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|             setFieldsChanged(newChangedFields); | ||||
|             setNewDiagram(newDiagramArg); | ||||
|             setOriginalDiagram(diagram); | ||||
|             setIsSummaryOnly(options?.summaryOnly ?? false); | ||||
|  | ||||
|             events.emit({ | ||||
|                 action: 'diff_calculated', | ||||
| @@ -302,6 +307,117 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         [diffMap] | ||||
|     ); | ||||
|  | ||||
|     const getFieldNewPrimaryKey = useCallback< | ||||
|         DiffContext['getFieldNewPrimaryKey'] | ||||
|     >( | ||||
|         ({ fieldId }) => { | ||||
|             const fieldKey = getDiffMapKey({ | ||||
|                 diffObject: 'field', | ||||
|                 objectId: fieldId, | ||||
|                 attribute: 'primaryKey', | ||||
|             }); | ||||
|  | ||||
|             if (diffMap.has(fieldKey)) { | ||||
|                 const diff = diffMap.get(fieldKey); | ||||
|  | ||||
|                 if (diff?.type === 'changed') { | ||||
|                     return diff.newValue as boolean; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return null; | ||||
|         }, | ||||
|         [diffMap] | ||||
|     ); | ||||
|  | ||||
|     const getFieldNewNullable = useCallback<DiffContext['getFieldNewNullable']>( | ||||
|         ({ fieldId }) => { | ||||
|             const fieldKey = getDiffMapKey({ | ||||
|                 diffObject: 'field', | ||||
|                 objectId: fieldId, | ||||
|                 attribute: 'nullable', | ||||
|             }); | ||||
|  | ||||
|             if (diffMap.has(fieldKey)) { | ||||
|                 const diff = diffMap.get(fieldKey); | ||||
|  | ||||
|                 if (diff?.type === 'changed') { | ||||
|                     return diff.newValue as boolean; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return null; | ||||
|         }, | ||||
|         [diffMap] | ||||
|     ); | ||||
|  | ||||
|     const getFieldNewCharacterMaximumLength = useCallback< | ||||
|         DiffContext['getFieldNewCharacterMaximumLength'] | ||||
|     >( | ||||
|         ({ fieldId }) => { | ||||
|             const fieldKey = getDiffMapKey({ | ||||
|                 diffObject: 'field', | ||||
|                 objectId: fieldId, | ||||
|                 attribute: 'characterMaximumLength', | ||||
|             }); | ||||
|  | ||||
|             if (diffMap.has(fieldKey)) { | ||||
|                 const diff = diffMap.get(fieldKey); | ||||
|  | ||||
|                 if (diff?.type === 'changed') { | ||||
|                     return diff.newValue as string; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return null; | ||||
|         }, | ||||
|         [diffMap] | ||||
|     ); | ||||
|  | ||||
|     const getFieldNewScale = useCallback<DiffContext['getFieldNewScale']>( | ||||
|         ({ fieldId }) => { | ||||
|             const fieldKey = getDiffMapKey({ | ||||
|                 diffObject: 'field', | ||||
|                 objectId: fieldId, | ||||
|                 attribute: 'scale', | ||||
|             }); | ||||
|  | ||||
|             if (diffMap.has(fieldKey)) { | ||||
|                 const diff = diffMap.get(fieldKey); | ||||
|  | ||||
|                 if (diff?.type === 'changed') { | ||||
|                     return diff.newValue as number; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return null; | ||||
|         }, | ||||
|         [diffMap] | ||||
|     ); | ||||
|  | ||||
|     const getFieldNewPrecision = useCallback< | ||||
|         DiffContext['getFieldNewPrecision'] | ||||
|     >( | ||||
|         ({ fieldId }) => { | ||||
|             const fieldKey = getDiffMapKey({ | ||||
|                 diffObject: 'field', | ||||
|                 objectId: fieldId, | ||||
|                 attribute: 'precision', | ||||
|             }); | ||||
|  | ||||
|             if (diffMap.has(fieldKey)) { | ||||
|                 const diff = diffMap.get(fieldKey); | ||||
|  | ||||
|                 if (diff?.type === 'changed') { | ||||
|                     return diff.newValue as number; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return null; | ||||
|         }, | ||||
|         [diffMap] | ||||
|     ); | ||||
|  | ||||
|     const checkIfNewRelationship = useCallback< | ||||
|         DiffContext['checkIfNewRelationship'] | ||||
|     >( | ||||
| @@ -336,6 +452,15 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         [diffMap] | ||||
|     ); | ||||
|  | ||||
|     const resetDiff = useCallback<DiffContext['resetDiff']>(() => { | ||||
|         setDiffMap(new Map<string, ChartDBDiff>()); | ||||
|         setTablesChanged(new Map<string, boolean>()); | ||||
|         setFieldsChanged(new Map<string, boolean>()); | ||||
|         setNewDiagram(null); | ||||
|         setOriginalDiagram(null); | ||||
|         setIsSummaryOnly(false); | ||||
|     }, []); | ||||
|  | ||||
|     return ( | ||||
|         <diffContext.Provider | ||||
|             value={{ | ||||
| @@ -343,8 +468,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 originalDiagram, | ||||
|                 diffMap, | ||||
|                 hasDiff: diffMap.size > 0, | ||||
|                 isSummaryOnly, | ||||
|  | ||||
|                 calculateDiff, | ||||
|                 resetDiff, | ||||
|  | ||||
|                 // table diff | ||||
|                 getTableNewName, | ||||
| @@ -359,6 +486,11 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 checkIfNewField, | ||||
|                 getFieldNewName, | ||||
|                 getFieldNewType, | ||||
|                 getFieldNewPrimaryKey, | ||||
|                 getFieldNewNullable, | ||||
|                 getFieldNewCharacterMaximumLength, | ||||
|                 getFieldNewScale, | ||||
|                 getFieldNewPrecision, | ||||
|  | ||||
|                 // relationship diff | ||||
|                 checkIfNewRelationship, | ||||
|   | ||||
| @@ -8,6 +8,7 @@ export enum KeyboardShortcutAction { | ||||
|     TOGGLE_SIDE_PANEL = 'toggle_side_panel', | ||||
|     SHOW_ALL = 'show_all', | ||||
|     TOGGLE_THEME = 'toggle_theme', | ||||
|     TOGGLE_FILTER = 'toggle_filter', | ||||
| } | ||||
|  | ||||
| export interface KeyboardShortcut { | ||||
| @@ -71,6 +72,13 @@ export const keyboardShortcuts: Record< | ||||
|         keyCombinationMac: 'meta+m', | ||||
|         keyCombinationWin: 'ctrl+m', | ||||
|     }, | ||||
|     [KeyboardShortcutAction.TOGGLE_FILTER]: { | ||||
|         action: KeyboardShortcutAction.TOGGLE_FILTER, | ||||
|         keyCombinationLabelMac: '⌘F', | ||||
|         keyCombinationLabelWin: 'Ctrl+F', | ||||
|         keyCombinationMac: 'meta+f', | ||||
|         keyCombinationWin: 'ctrl+f', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export interface KeyboardShortcutForOS { | ||||
|   | ||||
| @@ -2,9 +2,9 @@ import { emptyFn } from '@/lib/utils'; | ||||
| import { createContext } from 'react'; | ||||
|  | ||||
| export type SidebarSection = | ||||
|     | 'dbml' | ||||
|     | 'tables' | ||||
|     | 'relationships' | ||||
|     | 'dependencies' | ||||
|     | 'refs' | ||||
|     | 'areas' | ||||
|     | 'customTypes'; | ||||
|  | ||||
| @@ -13,14 +13,16 @@ export interface LayoutContext { | ||||
|     openTableFromSidebar: (tableId: string) => void; | ||||
|     closeAllTablesInSidebar: () => void; | ||||
|  | ||||
|     openedRelationshipInSidebar: string | undefined; | ||||
|     openRelationshipFromSidebar: (relationshipId: string) => void; | ||||
|     closeAllRelationshipsInSidebar: () => void; | ||||
|  | ||||
|     openedDependencyInSidebar: string | undefined; | ||||
|     openDependencyFromSidebar: (dependencyId: string) => void; | ||||
|     closeAllDependenciesInSidebar: () => void; | ||||
|  | ||||
|     openedRefInSidebar: string | undefined; | ||||
|     openRefFromSidebar: (refId: string) => void; | ||||
|     closeAllRefsInSidebar: () => void; | ||||
|  | ||||
|     openedAreaInSidebar: string | undefined; | ||||
|     openAreaFromSidebar: (areaId: string) => void; | ||||
|     closeAllAreasInSidebar: () => void; | ||||
| @@ -36,24 +38,22 @@ export interface LayoutContext { | ||||
|     hideSidePanel: () => void; | ||||
|     showSidePanel: () => void; | ||||
|     toggleSidePanel: () => void; | ||||
|  | ||||
|     isSelectSchemaOpen: boolean; | ||||
|     openSelectSchema: () => void; | ||||
|     closeSelectSchema: () => void; | ||||
| } | ||||
|  | ||||
| export const layoutContext = createContext<LayoutContext>({ | ||||
|     openedTableInSidebar: undefined, | ||||
|     selectedSidebarSection: 'tables', | ||||
|  | ||||
|     openedRelationshipInSidebar: undefined, | ||||
|     openRelationshipFromSidebar: emptyFn, | ||||
|     closeAllRelationshipsInSidebar: emptyFn, | ||||
|  | ||||
|     openedDependencyInSidebar: undefined, | ||||
|     openDependencyFromSidebar: emptyFn, | ||||
|     closeAllDependenciesInSidebar: emptyFn, | ||||
|  | ||||
|     openedRefInSidebar: undefined, | ||||
|     openRefFromSidebar: emptyFn, | ||||
|     closeAllRefsInSidebar: emptyFn, | ||||
|  | ||||
|     openedAreaInSidebar: undefined, | ||||
|     openAreaFromSidebar: emptyFn, | ||||
|     closeAllAreasInSidebar: emptyFn, | ||||
| @@ -70,8 +70,4 @@ export const layoutContext = createContext<LayoutContext>({ | ||||
|     hideSidePanel: emptyFn, | ||||
|     showSidePanel: emptyFn, | ||||
|     toggleSidePanel: emptyFn, | ||||
|  | ||||
|     isSelectSchemaOpen: false, | ||||
|     openSelectSchema: emptyFn, | ||||
|     closeSelectSchema: emptyFn, | ||||
| }); | ||||
|   | ||||
| @@ -10,10 +10,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     const [openedTableInSidebar, setOpenedTableInSidebar] = React.useState< | ||||
|         string | undefined | ||||
|     >(); | ||||
|     const [openedRelationshipInSidebar, setOpenedRelationshipInSidebar] = | ||||
|         React.useState<string | undefined>(); | ||||
|     const [openedDependencyInSidebar, setOpenedDependencyInSidebar] = | ||||
|         React.useState<string | undefined>(); | ||||
|     const [openedRefInSidebar, setOpenedRefInSidebar] = React.useState< | ||||
|         string | undefined | ||||
|     >(); | ||||
|     const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState< | ||||
|         string | undefined | ||||
|     >(); | ||||
| @@ -23,17 +22,18 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         React.useState<SidebarSection>('tables'); | ||||
|     const [isSidePanelShowed, setIsSidePanelShowed] = | ||||
|         React.useState<boolean>(isDesktop); | ||||
|     const [isSelectSchemaOpen, setIsSelectSchemaOpen] = | ||||
|         React.useState<boolean>(false); | ||||
|  | ||||
|     const closeAllTablesInSidebar: LayoutContext['closeAllTablesInSidebar'] = | ||||
|         () => setOpenedTableInSidebar(''); | ||||
|  | ||||
|     const closeAllRelationshipsInSidebar: LayoutContext['closeAllRelationshipsInSidebar'] = | ||||
|         () => setOpenedRelationshipInSidebar(''); | ||||
|         () => setOpenedRefInSidebar(''); | ||||
|  | ||||
|     const closeAllDependenciesInSidebar: LayoutContext['closeAllDependenciesInSidebar'] = | ||||
|         () => setOpenedDependencyInSidebar(''); | ||||
|         () => setOpenedRefInSidebar(''); | ||||
|  | ||||
|     const closeAllRefsInSidebar: LayoutContext['closeAllRefsInSidebar'] = () => | ||||
|         setOpenedRefInSidebar(''); | ||||
|  | ||||
|     const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] = | ||||
|         () => setOpenedAreaInSidebar(''); | ||||
| @@ -62,17 +62,23 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     const openRelationshipFromSidebar: LayoutContext['openRelationshipFromSidebar'] = | ||||
|         (relationshipId) => { | ||||
|             showSidePanel(); | ||||
|             setSelectedSidebarSection('relationships'); | ||||
|             setOpenedRelationshipInSidebar(relationshipId); | ||||
|             setSelectedSidebarSection('refs'); | ||||
|             setOpenedRefInSidebar(relationshipId); | ||||
|         }; | ||||
|  | ||||
|     const openDependencyFromSidebar: LayoutContext['openDependencyFromSidebar'] = | ||||
|         (dependencyId) => { | ||||
|             showSidePanel(); | ||||
|             setSelectedSidebarSection('dependencies'); | ||||
|             setOpenedDependencyInSidebar(dependencyId); | ||||
|             setSelectedSidebarSection('refs'); | ||||
|             setOpenedRefInSidebar(dependencyId); | ||||
|         }; | ||||
|  | ||||
|     const openRefFromSidebar: LayoutContext['openRefFromSidebar'] = (refId) => { | ||||
|         showSidePanel(); | ||||
|         setSelectedSidebarSection('refs'); | ||||
|         setOpenedRefInSidebar(refId); | ||||
|     }; | ||||
|  | ||||
|     const openAreaFromSidebar: LayoutContext['openAreaFromSidebar'] = ( | ||||
|         areaId | ||||
|     ) => { | ||||
| @@ -88,11 +94,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|             setOpenedTableInSidebar(customTypeId); | ||||
|         }; | ||||
|  | ||||
|     const openSelectSchema: LayoutContext['openSelectSchema'] = () => | ||||
|         setIsSelectSchemaOpen(true); | ||||
|  | ||||
|     const closeSelectSchema: LayoutContext['closeSelectSchema'] = () => | ||||
|         setIsSelectSchemaOpen(false); | ||||
|     return ( | ||||
|         <layoutContext.Provider | ||||
|             value={{ | ||||
| @@ -100,7 +101,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 selectedSidebarSection, | ||||
|                 openTableFromSidebar, | ||||
|                 selectSidebarSection: setSelectedSidebarSection, | ||||
|                 openedRelationshipInSidebar, | ||||
|                 openRelationshipFromSidebar, | ||||
|                 closeAllTablesInSidebar, | ||||
|                 closeAllRelationshipsInSidebar, | ||||
| @@ -108,12 +108,11 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 hideSidePanel, | ||||
|                 showSidePanel, | ||||
|                 toggleSidePanel, | ||||
|                 isSelectSchemaOpen, | ||||
|                 openSelectSchema, | ||||
|                 closeSelectSchema, | ||||
|                 openedDependencyInSidebar, | ||||
|                 openDependencyFromSidebar, | ||||
|                 closeAllDependenciesInSidebar, | ||||
|                 openedRefInSidebar, | ||||
|                 openRefFromSidebar, | ||||
|                 closeAllRefsInSidebar, | ||||
|                 openedAreaInSidebar, | ||||
|                 openAreaFromSidebar, | ||||
|                 closeAllAreasInSidebar, | ||||
|   | ||||
| @@ -4,8 +4,6 @@ import type { Theme } from '../theme-context/theme-context'; | ||||
|  | ||||
| export type ScrollAction = 'pan' | 'zoom'; | ||||
|  | ||||
| export type SchemasFilter = Record<string, string[]>; | ||||
|  | ||||
| export interface LocalConfigContext { | ||||
|     theme: Theme; | ||||
|     setTheme: (theme: Theme) => void; | ||||
| @@ -13,16 +11,14 @@ export interface LocalConfigContext { | ||||
|     scrollAction: ScrollAction; | ||||
|     setScrollAction: (action: ScrollAction) => void; | ||||
|  | ||||
|     schemasFilter: SchemasFilter; | ||||
|     setSchemasFilter: React.Dispatch<React.SetStateAction<SchemasFilter>>; | ||||
|     showDBViews: boolean; | ||||
|     setShowDBViews: (showViews: boolean) => void; | ||||
|  | ||||
|     showCardinality: boolean; | ||||
|     setShowCardinality: (showCardinality: boolean) => void; | ||||
|  | ||||
|     hideMultiSchemaNotification: boolean; | ||||
|     setHideMultiSchemaNotification: ( | ||||
|         hideMultiSchemaNotification: boolean | ||||
|     ) => void; | ||||
|     showFieldAttributes: boolean; | ||||
|     setShowFieldAttributes: (showFieldAttributes: boolean) => void; | ||||
|  | ||||
|     githubRepoOpened: boolean; | ||||
|     setGithubRepoOpened: (githubRepoOpened: boolean) => void; | ||||
| @@ -30,9 +26,6 @@ export interface LocalConfigContext { | ||||
|     starUsDialogLastOpen: number; | ||||
|     setStarUsDialogLastOpen: (lastOpen: number) => void; | ||||
|  | ||||
|     showDependenciesOnCanvas: boolean; | ||||
|     setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void; | ||||
|  | ||||
|     showMiniMapOnCanvas: boolean; | ||||
|     setShowMiniMapOnCanvas: (showMiniMapOnCanvas: boolean) => void; | ||||
| } | ||||
| @@ -44,14 +37,14 @@ export const LocalConfigContext = createContext<LocalConfigContext>({ | ||||
|     scrollAction: 'pan', | ||||
|     setScrollAction: emptyFn, | ||||
|  | ||||
|     schemasFilter: {}, | ||||
|     setSchemasFilter: emptyFn, | ||||
|     showDBViews: false, | ||||
|     setShowDBViews: emptyFn, | ||||
|  | ||||
|     showCardinality: true, | ||||
|     setShowCardinality: emptyFn, | ||||
|  | ||||
|     hideMultiSchemaNotification: false, | ||||
|     setHideMultiSchemaNotification: emptyFn, | ||||
|     showFieldAttributes: true, | ||||
|     setShowFieldAttributes: emptyFn, | ||||
|  | ||||
|     githubRepoOpened: false, | ||||
|     setGithubRepoOpened: emptyFn, | ||||
| @@ -59,9 +52,6 @@ export const LocalConfigContext = createContext<LocalConfigContext>({ | ||||
|     starUsDialogLastOpen: 0, | ||||
|     setStarUsDialogLastOpen: emptyFn, | ||||
|  | ||||
|     showDependenciesOnCanvas: false, | ||||
|     setShowDependenciesOnCanvas: emptyFn, | ||||
|  | ||||
|     showMiniMapOnCanvas: false, | ||||
|     setShowMiniMapOnCanvas: emptyFn, | ||||
| }); | ||||
|   | ||||
| @@ -1,17 +1,16 @@ | ||||
| import React, { useEffect } from 'react'; | ||||
| import type { SchemasFilter, ScrollAction } from './local-config-context'; | ||||
| import type { ScrollAction } from './local-config-context'; | ||||
| import { LocalConfigContext } from './local-config-context'; | ||||
| import type { Theme } from '../theme-context/theme-context'; | ||||
|  | ||||
| const themeKey = 'theme'; | ||||
| const scrollActionKey = 'scroll_action'; | ||||
| const schemasFilterKey = 'schemas_filter'; | ||||
| const showCardinalityKey = 'show_cardinality'; | ||||
| const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification'; | ||||
| const showFieldAttributesKey = 'show_field_attributes'; | ||||
| const githubRepoOpenedKey = 'github_repo_opened'; | ||||
| const starUsDialogLastOpenKey = 'star_us_dialog_last_open'; | ||||
| const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas'; | ||||
| const showMiniMapOnCanvasKey = 'show_minimap_on_canvas'; | ||||
| const showDBViewsKey = 'show_db_views'; | ||||
|  | ||||
| export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     children, | ||||
| @@ -24,20 +23,17 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         (localStorage.getItem(scrollActionKey) as ScrollAction) || 'pan' | ||||
|     ); | ||||
|  | ||||
|     const [schemasFilter, setSchemasFilter] = React.useState<SchemasFilter>( | ||||
|         JSON.parse( | ||||
|             localStorage.getItem(schemasFilterKey) || '{}' | ||||
|         ) as SchemasFilter | ||||
|     const [showDBViews, setShowDBViews] = React.useState<boolean>( | ||||
|         (localStorage.getItem(showDBViewsKey) || 'false') === 'true' | ||||
|     ); | ||||
|  | ||||
|     const [showCardinality, setShowCardinality] = React.useState<boolean>( | ||||
|         (localStorage.getItem(showCardinalityKey) || 'true') === 'true' | ||||
|     ); | ||||
|  | ||||
|     const [hideMultiSchemaNotification, setHideMultiSchemaNotification] = | ||||
|     const [showFieldAttributes, setShowFieldAttributes] = | ||||
|         React.useState<boolean>( | ||||
|             (localStorage.getItem(hideMultiSchemaNotificationKey) || | ||||
|                 'false') === 'true' | ||||
|             (localStorage.getItem(showFieldAttributesKey) || 'true') === 'true' | ||||
|         ); | ||||
|  | ||||
|     const [githubRepoOpened, setGithubRepoOpened] = React.useState<boolean>( | ||||
| @@ -49,12 +45,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|             parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0') | ||||
|         ); | ||||
|  | ||||
|     const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] = | ||||
|         React.useState<boolean>( | ||||
|             (localStorage.getItem(showDependenciesOnCanvasKey) || 'false') === | ||||
|                 'true' | ||||
|         ); | ||||
|  | ||||
|     const [showMiniMapOnCanvas, setShowMiniMapOnCanvas] = | ||||
|         React.useState<boolean>( | ||||
|             (localStorage.getItem(showMiniMapOnCanvasKey) || 'true') === 'true' | ||||
| @@ -71,13 +61,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString()); | ||||
|     }, [githubRepoOpened]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem( | ||||
|             hideMultiSchemaNotificationKey, | ||||
|             hideMultiSchemaNotification.toString() | ||||
|         ); | ||||
|     }, [hideMultiSchemaNotification]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem(themeKey, theme); | ||||
|     }, [theme]); | ||||
| @@ -87,20 +70,13 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     }, [scrollAction]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem(schemasFilterKey, JSON.stringify(schemasFilter)); | ||||
|     }, [schemasFilter]); | ||||
|         localStorage.setItem(showDBViewsKey, showDBViews.toString()); | ||||
|     }, [showDBViews]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem(showCardinalityKey, showCardinality.toString()); | ||||
|     }, [showCardinality]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem( | ||||
|             showDependenciesOnCanvasKey, | ||||
|             showDependenciesOnCanvas.toString() | ||||
|         ); | ||||
|     }, [showDependenciesOnCanvas]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         localStorage.setItem( | ||||
|             showMiniMapOnCanvasKey, | ||||
| @@ -115,18 +91,16 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 setTheme, | ||||
|                 scrollAction, | ||||
|                 setScrollAction, | ||||
|                 schemasFilter, | ||||
|                 setSchemasFilter, | ||||
|                 showDBViews, | ||||
|                 setShowDBViews, | ||||
|                 showCardinality, | ||||
|                 setShowCardinality, | ||||
|                 hideMultiSchemaNotification, | ||||
|                 setHideMultiSchemaNotification, | ||||
|                 showFieldAttributes, | ||||
|                 setShowFieldAttributes, | ||||
|                 setGithubRepoOpened, | ||||
|                 githubRepoOpened, | ||||
|                 starUsDialogLastOpen, | ||||
|                 setStarUsDialogLastOpen, | ||||
|                 showDependenciesOnCanvas, | ||||
|                 setShowDependenciesOnCanvas, | ||||
|                 showMiniMapOnCanvas, | ||||
|                 setShowMiniMapOnCanvas, | ||||
|             }} | ||||
|   | ||||
| @@ -7,12 +7,21 @@ import type { ChartDBConfig } from '@/lib/domain/config'; | ||||
| import type { DBDependency } from '@/lib/domain/db-dependency'; | ||||
| import type { Area } from '@/lib/domain/area'; | ||||
| import type { DBCustomType } from '@/lib/domain/db-custom-type'; | ||||
| import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter'; | ||||
|  | ||||
| export interface StorageContext { | ||||
|     // Config operations | ||||
|     getConfig: () => Promise<ChartDBConfig | undefined>; | ||||
|     updateConfig: (config: Partial<ChartDBConfig>) => Promise<void>; | ||||
|  | ||||
|     // Diagram filter operations | ||||
|     getDiagramFilter: (diagramId: string) => Promise<DiagramFilter | undefined>; | ||||
|     updateDiagramFilter: ( | ||||
|         diagramId: string, | ||||
|         filter: DiagramFilter | ||||
|     ) => Promise<void>; | ||||
|     deleteDiagramFilter: (diagramId: string) => Promise<void>; | ||||
|  | ||||
|     // Diagram operations | ||||
|     addDiagram: (params: { diagram: Diagram }) => Promise<void>; | ||||
|     listDiagrams: (options?: { | ||||
| @@ -132,6 +141,10 @@ export const storageInitialValue: StorageContext = { | ||||
|     getConfig: emptyFn, | ||||
|     updateConfig: emptyFn, | ||||
|  | ||||
|     getDiagramFilter: emptyFn, | ||||
|     updateDiagramFilter: emptyFn, | ||||
|     deleteDiagramFilter: emptyFn, | ||||
|  | ||||
|     addDiagram: emptyFn, | ||||
|     listDiagrams: emptyFn, | ||||
|     getDiagram: emptyFn, | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,8 +1,7 @@ | ||||
| import { createContext } from 'react'; | ||||
| import { emptyFn } from '@/lib/utils'; | ||||
|  | ||||
| export type Theme = 'light' | 'dark' | 'system'; | ||||
| export type EffectiveTheme = Exclude<Theme, 'system'>; | ||||
| import type { Theme, EffectiveTheme } from '@/lib/types'; | ||||
| export type { Theme, EffectiveTheme }; | ||||
|  | ||||
| export interface ThemeContext { | ||||
|     theme: Theme; | ||||
|   | ||||
| @@ -48,6 +48,7 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         handleThemeToggle, | ||||
|         { | ||||
|             preventDefault: true, | ||||
|             enableOnFormTags: true, | ||||
|         }, | ||||
|         [handleThemeToggle] | ||||
|     ); | ||||
|   | ||||
| @@ -35,7 +35,22 @@ import type { OnChange } from '@monaco-editor/react'; | ||||
| import { useDebounce } from '@/hooks/use-debounce-v2'; | ||||
| import { InstructionsSection } from './instructions-section/instructions-section'; | ||||
| import { parseSQLError } from '@/lib/data/sql-import'; | ||||
| import type { editor } from 'monaco-editor'; | ||||
| import type { editor, IDisposable } from 'monaco-editor'; | ||||
| import { waitFor } from '@/lib/utils'; | ||||
| import { | ||||
|     validateSQL, | ||||
|     type ValidationResult, | ||||
| } from '@/lib/data/sql-import/sql-validator'; | ||||
| import { SQLValidationStatus } from './sql-validation-status'; | ||||
|  | ||||
| const calculateContentSizeMB = (content: string): number => { | ||||
|     return content.length / (1024 * 1024); // Convert to MB | ||||
| }; | ||||
|  | ||||
| const calculateIsLargeFile = (content: string): boolean => { | ||||
|     const contentSizeMB = calculateContentSizeMB(content); | ||||
|     return contentSizeMB > 2; // Consider large if over 2MB | ||||
| }; | ||||
|  | ||||
| const errorScriptOutputMessage = | ||||
|     'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.'; | ||||
| @@ -117,6 +132,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|     const { effectiveTheme } = useTheme(); | ||||
|     const [errorMessage, setErrorMessage] = useState(''); | ||||
|     const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null); | ||||
|     const pasteDisposableRef = useRef<IDisposable | null>(null); | ||||
|  | ||||
|     const { t } = useTranslation(); | ||||
|     const { isSm: isDesktop } = useBreakpoint('sm'); | ||||
| @@ -124,6 +140,11 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|     const [showCheckJsonButton, setShowCheckJsonButton] = useState(false); | ||||
|     const [isCheckingJson, setIsCheckingJson] = useState(false); | ||||
|     const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false); | ||||
|     const [sqlValidation, setSqlValidation] = useState<ValidationResult | null>( | ||||
|         null | ||||
|     ); | ||||
|     const [isAutoFixing, setIsAutoFixing] = useState(false); | ||||
|     const [showAutoFixButton, setShowAutoFixButton] = useState(false); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         setScriptResult(''); | ||||
| @@ -134,11 +155,33 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|     // Check if the ddl is valid | ||||
|     useEffect(() => { | ||||
|         if (importMethod !== 'ddl') { | ||||
|             setSqlValidation(null); | ||||
|             setShowAutoFixButton(false); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!scriptResult.trim()) return; | ||||
|         if (!scriptResult.trim()) { | ||||
|             setSqlValidation(null); | ||||
|             setShowAutoFixButton(false); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // First run our validation based on database type | ||||
|         const validation = validateSQL(scriptResult, databaseType); | ||||
|         setSqlValidation(validation); | ||||
|  | ||||
|         // If we have auto-fixable errors, show the auto-fix button | ||||
|         if (validation.fixedSQL && validation.errors.length > 0) { | ||||
|             setShowAutoFixButton(true); | ||||
|             // Don't try to parse invalid SQL | ||||
|             setErrorMessage('SQL contains syntax errors'); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Hide auto-fix button if no fixes available | ||||
|         setShowAutoFixButton(false); | ||||
|  | ||||
|         // Validate the SQL (either original or already fixed) | ||||
|         parseSQLError({ | ||||
|             sqlContent: scriptResult, | ||||
|             sourceDatabaseType: databaseType, | ||||
| @@ -184,8 +227,44 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|         } | ||||
|     }, [errorMessage.length, onImport, scriptResult]); | ||||
|  | ||||
|     const handleAutoFix = useCallback(() => { | ||||
|         if (sqlValidation?.fixedSQL) { | ||||
|             setIsAutoFixing(true); | ||||
|             setShowAutoFixButton(false); | ||||
|             setErrorMessage(''); | ||||
|  | ||||
|             // Apply the fix with a delay so user sees the fixing message | ||||
|             setTimeout(() => { | ||||
|                 setScriptResult(sqlValidation.fixedSQL!); | ||||
|  | ||||
|                 setTimeout(() => { | ||||
|                     setIsAutoFixing(false); | ||||
|                 }, 100); | ||||
|             }, 1000); | ||||
|         } | ||||
|     }, [sqlValidation, setScriptResult]); | ||||
|  | ||||
|     const handleErrorClick = useCallback((line: number) => { | ||||
|         if (editorRef.current) { | ||||
|             // Set cursor to the error line | ||||
|             editorRef.current.setPosition({ lineNumber: line, column: 1 }); | ||||
|             editorRef.current.revealLineInCenter(line); | ||||
|             editorRef.current.focus(); | ||||
|         } | ||||
|     }, []); | ||||
|  | ||||
|     const formatEditor = useCallback(() => { | ||||
|         if (editorRef.current) { | ||||
|             const model = editorRef.current.getModel(); | ||||
|             if (model) { | ||||
|                 const content = model.getValue(); | ||||
|  | ||||
|                 // Skip formatting for large files (> 2MB) | ||||
|                 if (calculateIsLargeFile(content)) { | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             setTimeout(() => { | ||||
|                 editorRef.current | ||||
|                     ?.getAction('editor.action.formatDocument') | ||||
| @@ -211,7 +290,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|     const handleCheckJson = useCallback(async () => { | ||||
|         setIsCheckingJson(true); | ||||
|  | ||||
|         const fixedJson = await fixMetadataJson(scriptResult); | ||||
|         await waitFor(1000); | ||||
|         const fixedJson = fixMetadataJson(scriptResult); | ||||
|  | ||||
|         if (isStringMetadataJson(fixedJson)) { | ||||
|             setScriptResult(fixedJson); | ||||
| @@ -227,37 +307,69 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|         setIsCheckingJson(false); | ||||
|     }, [scriptResult, setScriptResult, formatEditor]); | ||||
|  | ||||
|     const detectAndSetImportMethod = useCallback(() => { | ||||
|         const content = editorRef.current?.getValue(); | ||||
|         if (content && content.trim()) { | ||||
|             const detectedType = detectContentType(content); | ||||
|             if (detectedType && detectedType !== importMethod) { | ||||
|                 setImportMethod(detectedType); | ||||
|             } | ||||
|         } | ||||
|     }, [setImportMethod, importMethod]); | ||||
|  | ||||
|     const [editorDidMount, setEditorDidMount] = useState(false); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (editorRef.current && editorDidMount) { | ||||
|             editorRef.current.onDidPaste(() => { | ||||
|                 setTimeout(() => { | ||||
|                     editorRef.current | ||||
|                         ?.getAction('editor.action.formatDocument') | ||||
|                         ?.run(); | ||||
|                 }, 0); | ||||
|                 setTimeout(detectAndSetImportMethod, 0); | ||||
|             }); | ||||
|         } | ||||
|     }, [detectAndSetImportMethod, editorDidMount]); | ||||
|         // Cleanup paste handler on unmount | ||||
|         return () => { | ||||
|             if (pasteDisposableRef.current) { | ||||
|                 pasteDisposableRef.current.dispose(); | ||||
|                 pasteDisposableRef.current = null; | ||||
|             } | ||||
|         }; | ||||
|     }, []); | ||||
|  | ||||
|     const handleEditorDidMount = useCallback( | ||||
|         (editor: editor.IStandaloneCodeEditor) => { | ||||
|             editorRef.current = editor; | ||||
|             setEditorDidMount(true); | ||||
|  | ||||
|             // Cleanup previous disposable if it exists | ||||
|             if (pasteDisposableRef.current) { | ||||
|                 pasteDisposableRef.current.dispose(); | ||||
|                 pasteDisposableRef.current = null; | ||||
|             } | ||||
|  | ||||
|             // Add paste handler for all modes | ||||
|             const disposable = editor.onDidPaste(() => { | ||||
|                 const model = editor.getModel(); | ||||
|                 if (!model) return; | ||||
|  | ||||
|                 const content = model.getValue(); | ||||
|  | ||||
|                 // Skip formatting for large files (> 2MB) to prevent browser freezing | ||||
|                 const isLargeFile = calculateIsLargeFile(content); | ||||
|  | ||||
|                 // First, detect content type to determine if we should switch modes | ||||
|                 const detectedType = detectContentType(content); | ||||
|                 if (detectedType && detectedType !== importMethod) { | ||||
|                     // Switch to the detected mode immediately | ||||
|                     setImportMethod(detectedType); | ||||
|  | ||||
|                     // Only format if it's JSON (query mode) AND file is not too large | ||||
|                     if (detectedType === 'query' && !isLargeFile) { | ||||
|                         // For JSON mode, format after a short delay | ||||
|                         setTimeout(() => { | ||||
|                             editor | ||||
|                                 .getAction('editor.action.formatDocument') | ||||
|                                 ?.run(); | ||||
|                         }, 100); | ||||
|                     } | ||||
|                     // For DDL mode, do NOT format as it can break the SQL | ||||
|                 } else { | ||||
|                     // Content type didn't change, apply formatting based on current mode | ||||
|                     if (importMethod === 'query' && !isLargeFile) { | ||||
|                         // Only format JSON content if not too large | ||||
|                         setTimeout(() => { | ||||
|                             editor | ||||
|                                 .getAction('editor.action.formatDocument') | ||||
|                                 ?.run(); | ||||
|                         }, 100); | ||||
|                     } | ||||
|                     // For DDL mode or large files, do NOT format | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             pasteDisposableRef.current = disposable; | ||||
|         }, | ||||
|         [] | ||||
|         [importMethod, setImportMethod] | ||||
|     ); | ||||
|  | ||||
|     const renderHeader = useCallback(() => { | ||||
| @@ -314,7 +426,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|                                     : 'dbml-light' | ||||
|                             } | ||||
|                             options={{ | ||||
|                                 formatOnPaste: true, | ||||
|                                 formatOnPaste: false, // Never format on paste - we handle it manually | ||||
|                                 minimap: { enabled: false }, | ||||
|                                 scrollBeyondLastLine: false, | ||||
|                                 automaticLayout: true, | ||||
| @@ -343,10 +455,13 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|                     </Suspense> | ||||
|                 </div> | ||||
|  | ||||
|                 {errorMessage ? ( | ||||
|                     <div className="mt-2 flex shrink-0 items-center gap-2"> | ||||
|                         <p className="text-xs text-red-700">{errorMessage}</p> | ||||
|                     </div> | ||||
|                 {errorMessage || (importMethod === 'ddl' && sqlValidation) ? ( | ||||
|                     <SQLValidationStatus | ||||
|                         validation={sqlValidation} | ||||
|                         errorMessage={errorMessage} | ||||
|                         isAutoFixing={isAutoFixing} | ||||
|                         onErrorClick={handleErrorClick} | ||||
|                     /> | ||||
|                 ) : null} | ||||
|             </div> | ||||
|         ), | ||||
| @@ -357,6 +472,9 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|             effectiveTheme, | ||||
|             debouncedHandleInputChange, | ||||
|             handleEditorDidMount, | ||||
|             sqlValidation, | ||||
|             isAutoFixing, | ||||
|             handleErrorClick, | ||||
|         ] | ||||
|     ); | ||||
|  | ||||
| @@ -442,13 +560,28 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|                                 ) | ||||
|                             )} | ||||
|                         </Button> | ||||
|                     ) : showAutoFixButton && importMethod === 'ddl' ? ( | ||||
|                         <Button | ||||
|                             type="button" | ||||
|                             variant="secondary" | ||||
|                             onClick={handleAutoFix} | ||||
|                             disabled={isAutoFixing} | ||||
|                             className="bg-sky-600 text-white hover:bg-sky-700" | ||||
|                         > | ||||
|                             {isAutoFixing ? ( | ||||
|                                 <Spinner size="small" /> | ||||
|                             ) : ( | ||||
|                                 'Try auto-fix' | ||||
|                             )} | ||||
|                         </Button> | ||||
|                     ) : keepDialogAfterImport ? ( | ||||
|                         <Button | ||||
|                             type="button" | ||||
|                             variant="default" | ||||
|                             disabled={ | ||||
|                                 scriptResult.trim().length === 0 || | ||||
|                                 errorMessage.length > 0 | ||||
|                                 errorMessage.length > 0 || | ||||
|                                 isAutoFixing | ||||
|                             } | ||||
|                             onClick={handleImport} | ||||
|                         > | ||||
| @@ -461,7 +594,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|                                 variant="default" | ||||
|                                 disabled={ | ||||
|                                     scriptResult.trim().length === 0 || | ||||
|                                     errorMessage.length > 0 | ||||
|                                     errorMessage.length > 0 || | ||||
|                                     isAutoFixing | ||||
|                                 } | ||||
|                                 onClick={handleImport} | ||||
|                             > | ||||
| @@ -494,6 +628,10 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({ | ||||
|         handleCheckJson, | ||||
|         goBack, | ||||
|         t, | ||||
|         importMethod, | ||||
|         isAutoFixing, | ||||
|         showAutoFixButton, | ||||
|         handleAutoFix, | ||||
|     ]); | ||||
|  | ||||
|     return ( | ||||
|   | ||||
							
								
								
									
										179
									
								
								src/dialogs/common/import-database/sql-validation-status.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										179
									
								
								src/dialogs/common/import-database/sql-validation-status.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,179 @@ | ||||
| import React, { useMemo } from 'react'; | ||||
| import { CheckCircle, AlertTriangle, MessageCircleWarning } from 'lucide-react'; | ||||
| import { Alert, AlertDescription } from '@/components/alert/alert'; | ||||
| import type { ValidationResult } from '@/lib/data/sql-import/sql-validator'; | ||||
| import { Separator } from '@/components/separator/separator'; | ||||
| import { ScrollArea } from '@/components/scroll-area/scroll-area'; | ||||
| import { Spinner } from '@/components/spinner/spinner'; | ||||
|  | ||||
| interface SQLValidationStatusProps { | ||||
|     validation?: ValidationResult | null; | ||||
|     errorMessage: string; | ||||
|     isAutoFixing?: boolean; | ||||
|     onErrorClick?: (line: number) => void; | ||||
| } | ||||
|  | ||||
| export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({ | ||||
|     validation, | ||||
|     errorMessage, | ||||
|     isAutoFixing = false, | ||||
|     onErrorClick, | ||||
| }) => { | ||||
|     const hasErrors = useMemo( | ||||
|         () => validation?.errors.length && validation.errors.length > 0, | ||||
|         [validation?.errors] | ||||
|     ); | ||||
|     const hasWarnings = useMemo( | ||||
|         () => validation?.warnings && validation.warnings.length > 0, | ||||
|         [validation?.warnings] | ||||
|     ); | ||||
|     const wasAutoFixed = useMemo( | ||||
|         () => | ||||
|             validation?.warnings?.some((w) => | ||||
|                 w.message.includes('Auto-fixed') | ||||
|             ) || false, | ||||
|         [validation?.warnings] | ||||
|     ); | ||||
|  | ||||
|     if (!validation && !errorMessage && !isAutoFixing) return null; | ||||
|  | ||||
|     if (isAutoFixing) { | ||||
|         return ( | ||||
|             <> | ||||
|                 <Separator className="mb-1 mt-2" /> | ||||
|                 <div className="rounded-md border border-sky-200 bg-sky-50 dark:border-sky-800 dark:bg-sky-950"> | ||||
|                     <div className="space-y-3 p-3 pt-2 text-sky-700 dark:text-sky-300"> | ||||
|                         <div className="flex items-start gap-2"> | ||||
|                             <Spinner className="mt-0.5 size-4 shrink-0 text-sky-700 dark:text-sky-300" /> | ||||
|                             <div className="flex-1 text-sm text-sky-700 dark:text-sky-300"> | ||||
|                                 Auto-fixing SQL syntax errors... | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     // If we have parser errors (errorMessage) after validation | ||||
|     if (errorMessage && !hasErrors) { | ||||
|         return ( | ||||
|             <> | ||||
|                 <Separator className="mb-1 mt-2" /> | ||||
|                 <div className="mb-1 flex shrink-0 items-center gap-2"> | ||||
|                     <p className="text-xs text-red-700">{errorMessage}</p> | ||||
|                 </div> | ||||
|             </> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <> | ||||
|             <Separator className="mb-1 mt-2" /> | ||||
|  | ||||
|             {hasErrors ? ( | ||||
|                 <div className="rounded-md border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950"> | ||||
|                     <ScrollArea className="h-24"> | ||||
|                         <div className="space-y-3 p-3 pt-2 text-red-700 dark:text-red-300"> | ||||
|                             {validation?.errors | ||||
|                                 .slice(0, 3) | ||||
|                                 .map((error, idx) => ( | ||||
|                                     <div | ||||
|                                         key={idx} | ||||
|                                         className="flex items-start gap-2" | ||||
|                                     > | ||||
|                                         <MessageCircleWarning className="mt-0.5 size-4 shrink-0 text-red-700 dark:text-red-300" /> | ||||
|                                         <div className="flex-1 text-sm text-red-700 dark:text-red-300"> | ||||
|                                             <button | ||||
|                                                 onClick={() => | ||||
|                                                     onErrorClick?.(error.line) | ||||
|                                                 } | ||||
|                                                 className="rounded font-medium underline hover:text-red-600 focus:outline-none focus:ring-1 focus:ring-red-500 dark:hover:text-red-200" | ||||
|                                                 type="button" | ||||
|                                             > | ||||
|                                                 Line {error.line} | ||||
|                                             </button> | ||||
|                                             <span className="mx-1">:</span> | ||||
|                                             <span className="text-xs"> | ||||
|                                                 {error.message} | ||||
|                                             </span> | ||||
|                                             {error.suggestion && ( | ||||
|                                                 <div className="mt-1 flex items-start gap-2"> | ||||
|                                                     <span className="text-xs font-medium "> | ||||
|                                                         {error.suggestion} | ||||
|                                                     </span> | ||||
|                                                 </div> | ||||
|                                             )} | ||||
|                                         </div> | ||||
|                                     </div> | ||||
|                                 ))} | ||||
|                             {validation?.errors && | ||||
|                             validation?.errors.length > 3 ? ( | ||||
|                                 <div className="flex items-center gap-2"> | ||||
|                                     <MessageCircleWarning className="mt-0.5 size-4 shrink-0 text-red-700 dark:text-red-300" /> | ||||
|                                     <span className="text-xs font-medium"> | ||||
|                                         {validation.errors.length - 3} more | ||||
|                                         error | ||||
|                                         {validation.errors.length - 3 > 1 | ||||
|                                             ? 's' | ||||
|                                             : ''} | ||||
|                                     </span> | ||||
|                                 </div> | ||||
|                             ) : null} | ||||
|                         </div> | ||||
|                     </ScrollArea> | ||||
|                 </div> | ||||
|             ) : null} | ||||
|  | ||||
|             {wasAutoFixed && !hasErrors ? ( | ||||
|                 <Alert className="border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950"> | ||||
|                     <CheckCircle className="size-4 text-green-600 dark:text-green-400" /> | ||||
|                     <AlertDescription className="text-sm text-green-700 dark:text-green-300"> | ||||
|                         SQL syntax errors were automatically fixed. Your SQL is | ||||
|                         now ready to import. | ||||
|                     </AlertDescription> | ||||
|                 </Alert> | ||||
|             ) : null} | ||||
|  | ||||
|             {hasWarnings && !hasErrors ? ( | ||||
|                 <div className="rounded-md border border-sky-200 bg-sky-50 dark:border-sky-800 dark:bg-sky-950"> | ||||
|                     <ScrollArea className="h-24"> | ||||
|                         <div className="space-y-3 p-3 pt-2 text-sky-700 dark:text-sky-300"> | ||||
|                             <div className="flex items-start gap-2"> | ||||
|                                 <AlertTriangle className="mt-0.5 size-4 shrink-0 text-sky-700 dark:text-sky-300" /> | ||||
|                                 <div className="flex-1 text-sm text-sky-700 dark:text-sky-300"> | ||||
|                                     <div className="mb-1 font-medium"> | ||||
|                                         Import Info: | ||||
|                                     </div> | ||||
|                                     {validation?.warnings.map( | ||||
|                                         (warning, idx) => ( | ||||
|                                             <div | ||||
|                                                 key={idx} | ||||
|                                                 className="ml-2 text-xs" | ||||
|                                             > | ||||
|                                                 • {warning.message} | ||||
|                                             </div> | ||||
|                                         ) | ||||
|                                     )} | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </ScrollArea> | ||||
|                 </div> | ||||
|             ) : null} | ||||
|  | ||||
|             {!hasErrors && !hasWarnings && !errorMessage && validation ? ( | ||||
|                 <div className="rounded-md border border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950"> | ||||
|                     <div className="space-y-3 p-3 pt-2 text-green-700 dark:text-green-300"> | ||||
|                         <div className="flex items-start gap-2"> | ||||
|                             <CheckCircle className="mt-0.5 size-4 shrink-0 text-green-700 dark:text-green-300" /> | ||||
|                             <div className="flex-1 text-sm text-green-700 dark:text-green-300"> | ||||
|                                 SQL syntax validated successfully | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             ) : null} | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
							
								
								
									
										2
									
								
								src/dialogs/common/select-tables/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/dialogs/common/select-tables/constants.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| export const MAX_TABLES_IN_DIAGRAM = 500; | ||||
| export const MAX_TABLES_WITHOUT_SHOWING_FILTER = 50; | ||||
							
								
								
									
										683
									
								
								src/dialogs/common/select-tables/select-tables.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										683
									
								
								src/dialogs/common/select-tables/select-tables.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,683 @@ | ||||
| import React, { useState, useMemo, useEffect, useCallback } from 'react'; | ||||
| import { Button } from '@/components/button/button'; | ||||
| import { Input } from '@/components/input/input'; | ||||
| import { Search, AlertCircle, Check, X, View, Table } from 'lucide-react'; | ||||
| import { Checkbox } from '@/components/checkbox/checkbox'; | ||||
| import type { DatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata'; | ||||
| import { schemaNameToDomainSchemaName } from '@/lib/domain/db-schema'; | ||||
| import { cn } from '@/lib/utils'; | ||||
| import { | ||||
|     DialogDescription, | ||||
|     DialogFooter, | ||||
|     DialogHeader, | ||||
|     DialogInternalContent, | ||||
|     DialogTitle, | ||||
| } from '@/components/dialog/dialog'; | ||||
| import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata'; | ||||
| import { generateTableKey } from '@/lib/domain'; | ||||
| import { Spinner } from '@/components/spinner/spinner'; | ||||
| import { | ||||
|     Pagination, | ||||
|     PaginationContent, | ||||
|     PaginationItem, | ||||
|     PaginationPrevious, | ||||
|     PaginationNext, | ||||
| } from '@/components/pagination/pagination'; | ||||
| import { MAX_TABLES_IN_DIAGRAM } from './constants'; | ||||
| import { useBreakpoint } from '@/hooks/use-breakpoint'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
|  | ||||
| export interface SelectTablesProps { | ||||
|     databaseMetadata?: DatabaseMetadata; | ||||
|     onImport: ({ | ||||
|         selectedTables, | ||||
|         databaseMetadata, | ||||
|     }: { | ||||
|         selectedTables?: SelectedTable[]; | ||||
|         databaseMetadata?: DatabaseMetadata; | ||||
|     }) => Promise<void>; | ||||
|     onBack: () => void; | ||||
|     isLoading?: boolean; | ||||
| } | ||||
|  | ||||
| const TABLES_PER_PAGE = 10; | ||||
|  | ||||
| interface TableInfo { | ||||
|     key: string; | ||||
|     schema?: string; | ||||
|     tableName: string; | ||||
|     fullName: string; | ||||
|     type: 'table' | 'view'; | ||||
| } | ||||
|  | ||||
| export const SelectTables: React.FC<SelectTablesProps> = ({ | ||||
|     databaseMetadata, | ||||
|     onImport, | ||||
|     onBack, | ||||
|     isLoading = false, | ||||
| }) => { | ||||
|     const [searchTerm, setSearchTerm] = useState(''); | ||||
|     const [currentPage, setCurrentPage] = useState(1); | ||||
|     const [showTables, setShowTables] = useState(true); | ||||
|     const [showViews, setShowViews] = useState(false); | ||||
|     const { t } = useTranslation(); | ||||
|     const [isImporting, setIsImporting] = useState(false); | ||||
|  | ||||
|     // Prepare all tables and views with their metadata | ||||
|     const allTables = useMemo(() => { | ||||
|         const tables: TableInfo[] = []; | ||||
|  | ||||
|         // Add regular tables | ||||
|         databaseMetadata?.tables.forEach((table) => { | ||||
|             const schema = schemaNameToDomainSchemaName(table.schema); | ||||
|             const tableName = table.table; | ||||
|  | ||||
|             const key = `table:${generateTableKey({ tableName, schemaName: schema })}`; | ||||
|  | ||||
|             tables.push({ | ||||
|                 key, | ||||
|                 schema, | ||||
|                 tableName, | ||||
|                 fullName: schema ? `${schema}.${tableName}` : tableName, | ||||
|                 type: 'table', | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         // Add views | ||||
|         databaseMetadata?.views?.forEach((view) => { | ||||
|             const schema = schemaNameToDomainSchemaName(view.schema); | ||||
|             const viewName = view.view_name; | ||||
|  | ||||
|             if (!viewName) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const key = `view:${generateTableKey({ | ||||
|                 tableName: viewName, | ||||
|                 schemaName: schema, | ||||
|             })}`; | ||||
|  | ||||
|             tables.push({ | ||||
|                 key, | ||||
|                 schema, | ||||
|                 tableName: viewName, | ||||
|                 fullName: | ||||
|                     schema === 'default' ? viewName : `${schema}.${viewName}`, | ||||
|                 type: 'view', | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         return tables.sort((a, b) => a.fullName.localeCompare(b.fullName)); | ||||
|     }, [databaseMetadata?.tables, databaseMetadata?.views]); | ||||
|  | ||||
|     // Count tables and views separately | ||||
|     const tableCount = useMemo( | ||||
|         () => allTables.filter((t) => t.type === 'table').length, | ||||
|         [allTables] | ||||
|     ); | ||||
|     const viewCount = useMemo( | ||||
|         () => allTables.filter((t) => t.type === 'view').length, | ||||
|         [allTables] | ||||
|     ); | ||||
|  | ||||
|     // Initialize selectedTables with all tables (not views) if less than 100 tables | ||||
|     const [selectedTables, setSelectedTables] = useState<Set<string>>(() => { | ||||
|         const tables = allTables.filter((t) => t.type === 'table'); | ||||
|         if (tables.length < MAX_TABLES_IN_DIAGRAM) { | ||||
|             return new Set(tables.map((t) => t.key)); | ||||
|         } | ||||
|         return new Set(); | ||||
|     }); | ||||
|  | ||||
|     // Filter tables based on search term and type filters | ||||
|     const filteredTables = useMemo(() => { | ||||
|         let filtered = allTables; | ||||
|  | ||||
|         // Filter by type | ||||
|         filtered = filtered.filter((table) => { | ||||
|             if (table.type === 'table' && !showTables) return false; | ||||
|             if (table.type === 'view' && !showViews) return false; | ||||
|             return true; | ||||
|         }); | ||||
|  | ||||
|         // Filter by search term | ||||
|         if (searchTerm.trim()) { | ||||
|             const searchLower = searchTerm.toLowerCase(); | ||||
|             filtered = filtered.filter( | ||||
|                 (table) => | ||||
|                     table.tableName.toLowerCase().includes(searchLower) || | ||||
|                     table.schema?.toLowerCase().includes(searchLower) || | ||||
|                     table.fullName.toLowerCase().includes(searchLower) | ||||
|             ); | ||||
|         } | ||||
|  | ||||
|         return filtered; | ||||
|     }, [allTables, searchTerm, showTables, showViews]); | ||||
|  | ||||
|     // Calculate pagination | ||||
|     const totalPages = useMemo( | ||||
|         () => Math.max(1, Math.ceil(filteredTables.length / TABLES_PER_PAGE)), | ||||
|         [filteredTables.length] | ||||
|     ); | ||||
|  | ||||
|     const paginatedTables = useMemo(() => { | ||||
|         const startIndex = (currentPage - 1) * TABLES_PER_PAGE; | ||||
|         const endIndex = startIndex + TABLES_PER_PAGE; | ||||
|         return filteredTables.slice(startIndex, endIndex); | ||||
|     }, [filteredTables, currentPage]); | ||||
|  | ||||
|     // Get currently visible selected tables | ||||
|     const visibleSelectedTables = useMemo(() => { | ||||
|         return paginatedTables.filter((table) => selectedTables.has(table.key)); | ||||
|     }, [paginatedTables, selectedTables]); | ||||
|  | ||||
|     const canAddMore = useMemo( | ||||
|         () => selectedTables.size < MAX_TABLES_IN_DIAGRAM, | ||||
|         [selectedTables.size] | ||||
|     ); | ||||
|     const hasSearchResults = useMemo( | ||||
|         () => filteredTables.length > 0, | ||||
|         [filteredTables.length] | ||||
|     ); | ||||
|     const allVisibleSelected = useMemo( | ||||
|         () => | ||||
|             visibleSelectedTables.length === paginatedTables.length && | ||||
|             paginatedTables.length > 0, | ||||
|         [visibleSelectedTables.length, paginatedTables.length] | ||||
|     ); | ||||
|     const canSelectAllFiltered = useMemo( | ||||
|         () => | ||||
|             filteredTables.length > 0 && | ||||
|             filteredTables.some((table) => !selectedTables.has(table.key)) && | ||||
|             canAddMore, | ||||
|         [filteredTables, selectedTables, canAddMore] | ||||
|     ); | ||||
|  | ||||
|     // Reset to first page when search changes | ||||
|     useEffect(() => { | ||||
|         setCurrentPage(1); | ||||
|     }, [searchTerm]); | ||||
|  | ||||
|     const handleTableToggle = useCallback( | ||||
|         (tableKey: string) => { | ||||
|             const newSelected = new Set(selectedTables); | ||||
|  | ||||
|             if (newSelected.has(tableKey)) { | ||||
|                 newSelected.delete(tableKey); | ||||
|             } else if (selectedTables.size < MAX_TABLES_IN_DIAGRAM) { | ||||
|                 newSelected.add(tableKey); | ||||
|             } | ||||
|  | ||||
|             setSelectedTables(newSelected); | ||||
|         }, | ||||
|         [selectedTables] | ||||
|     ); | ||||
|  | ||||
|     const handleTogglePageSelection = useCallback(() => { | ||||
|         const newSelected = new Set(selectedTables); | ||||
|  | ||||
|         if (allVisibleSelected) { | ||||
|             // Deselect all on current page | ||||
|             for (const table of paginatedTables) { | ||||
|                 newSelected.delete(table.key); | ||||
|             } | ||||
|         } else { | ||||
|             // Select all on current page | ||||
|             for (const table of paginatedTables) { | ||||
|                 if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break; | ||||
|                 newSelected.add(table.key); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         setSelectedTables(newSelected); | ||||
|     }, [allVisibleSelected, paginatedTables, selectedTables]); | ||||
|  | ||||
|     const handleSelectAllFiltered = useCallback(() => { | ||||
|         const newSelected = new Set(selectedTables); | ||||
|  | ||||
|         for (const table of filteredTables) { | ||||
|             if (newSelected.size >= MAX_TABLES_IN_DIAGRAM) break; | ||||
|             newSelected.add(table.key); | ||||
|         } | ||||
|  | ||||
|         setSelectedTables(newSelected); | ||||
|     }, [filteredTables, selectedTables]); | ||||
|  | ||||
|     const handleNextPage = useCallback(() => { | ||||
|         if (currentPage < totalPages) { | ||||
|             setCurrentPage(currentPage + 1); | ||||
|         } | ||||
|     }, [currentPage, totalPages]); | ||||
|  | ||||
|     const handlePrevPage = useCallback(() => { | ||||
|         if (currentPage > 1) { | ||||
|             setCurrentPage(currentPage - 1); | ||||
|         } | ||||
|     }, [currentPage]); | ||||
|  | ||||
|     const handleClearSelection = useCallback(() => { | ||||
|         setSelectedTables(new Set()); | ||||
|     }, []); | ||||
|  | ||||
|     const handleConfirm = useCallback(async () => { | ||||
|         if (isImporting) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         setIsImporting(true); | ||||
|  | ||||
|         try { | ||||
|             const selectedTableObjects: SelectedTable[] = Array.from( | ||||
|                 selectedTables | ||||
|             ) | ||||
|                 .map((key): SelectedTable | null => { | ||||
|                     const table = allTables.find((t) => t.key === key); | ||||
|                     if (!table) return null; | ||||
|  | ||||
|                     return { | ||||
|                         schema: table.schema, | ||||
|                         table: table.tableName, | ||||
|                         type: table.type, | ||||
|                     } satisfies SelectedTable; | ||||
|                 }) | ||||
|                 .filter((t): t is SelectedTable => t !== null); | ||||
|  | ||||
|             await onImport({ | ||||
|                 selectedTables: selectedTableObjects, | ||||
|                 databaseMetadata, | ||||
|             }); | ||||
|         } finally { | ||||
|             setIsImporting(false); | ||||
|         } | ||||
|     }, [selectedTables, allTables, onImport, databaseMetadata, isImporting]); | ||||
|  | ||||
|     const { isMd: isDesktop } = useBreakpoint('md'); | ||||
|  | ||||
|     const renderPagination = useCallback( | ||||
|         () => ( | ||||
|             <Pagination> | ||||
|                 <PaginationContent> | ||||
|                     <PaginationItem> | ||||
|                         <PaginationPrevious | ||||
|                             onClick={handlePrevPage} | ||||
|                             className={cn( | ||||
|                                 'cursor-pointer', | ||||
|                                 currentPage === 1 && | ||||
|                                     'pointer-events-none opacity-50' | ||||
|                             )} | ||||
|                         /> | ||||
|                     </PaginationItem> | ||||
|                     <PaginationItem> | ||||
|                         <span className="px-3 text-sm text-muted-foreground"> | ||||
|                             Page {currentPage} of {totalPages} | ||||
|                         </span> | ||||
|                     </PaginationItem> | ||||
|                     <PaginationItem> | ||||
|                         <PaginationNext | ||||
|                             onClick={handleNextPage} | ||||
|                             className={cn( | ||||
|                                 'cursor-pointer', | ||||
|                                 (currentPage >= totalPages || | ||||
|                                     filteredTables.length === 0) && | ||||
|                                     'pointer-events-none opacity-50' | ||||
|                             )} | ||||
|                         /> | ||||
|                     </PaginationItem> | ||||
|                 </PaginationContent> | ||||
|             </Pagination> | ||||
|         ), | ||||
|         [ | ||||
|             currentPage, | ||||
|             totalPages, | ||||
|             handlePrevPage, | ||||
|             handleNextPage, | ||||
|             filteredTables.length, | ||||
|         ] | ||||
|     ); | ||||
|  | ||||
|     if (isLoading) { | ||||
|         return ( | ||||
|             <div className="flex h-[400px] items-center justify-center"> | ||||
|                 <div className="text-center"> | ||||
|                     <Spinner className="mb-4" /> | ||||
|                     <p className="text-sm text-muted-foreground"> | ||||
|                         Parsing database metadata... | ||||
|                     </p> | ||||
|                 </div> | ||||
|             </div> | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     return ( | ||||
|         <> | ||||
|             <DialogHeader> | ||||
|                 <DialogTitle>Select Tables to Import</DialogTitle> | ||||
|                 <DialogDescription> | ||||
|                     {tableCount} {tableCount === 1 ? 'table' : 'tables'} | ||||
|                     {viewCount > 0 && ( | ||||
|                         <> | ||||
|                             {' and '} | ||||
|                             {viewCount} {viewCount === 1 ? 'view' : 'views'} | ||||
|                         </> | ||||
|                     )} | ||||
|                     {' found. '} | ||||
|                     {allTables.length > MAX_TABLES_IN_DIAGRAM | ||||
|                         ? `Select up to ${MAX_TABLES_IN_DIAGRAM} to import.` | ||||
|                         : 'Choose which ones to import.'} | ||||
|                 </DialogDescription> | ||||
|             </DialogHeader> | ||||
|             <DialogInternalContent> | ||||
|                 <div className="flex h-full flex-col space-y-4"> | ||||
|                     {/* Warning/Info Banner */} | ||||
|                     {allTables.length > MAX_TABLES_IN_DIAGRAM ? ( | ||||
|                         <div | ||||
|                             className={cn( | ||||
|                                 'flex items-center gap-2 rounded-lg p-3 text-sm', | ||||
|                                 'bg-amber-50 text-amber-800 dark:bg-amber-950 dark:text-amber-200' | ||||
|                             )} | ||||
|                         > | ||||
|                             <AlertCircle className="size-4 shrink-0" /> | ||||
|                             <span> | ||||
|                                 Due to performance limitations, you can import a | ||||
|                                 maximum of {MAX_TABLES_IN_DIAGRAM} tables. | ||||
|                             </span> | ||||
|                         </div> | ||||
|                     ) : null} | ||||
|                     {/* Search Input */} | ||||
|                     <div className="relative"> | ||||
|                         <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" /> | ||||
|                         <Input | ||||
|                             placeholder="Search tables..." | ||||
|                             value={searchTerm} | ||||
|                             onChange={(e) => setSearchTerm(e.target.value)} | ||||
|                             className="px-9" | ||||
|                         /> | ||||
|                         {searchTerm && ( | ||||
|                             <button | ||||
|                                 onClick={() => setSearchTerm('')} | ||||
|                                 className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" | ||||
|                             > | ||||
|                                 <X className="size-4" /> | ||||
|                             </button> | ||||
|                         )} | ||||
|                     </div> | ||||
|  | ||||
|                     {/* Selection Status and Actions - Responsive layout */} | ||||
|                     <div className="flex flex-col items-center gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4"> | ||||
|                         {/* Left side: selection count -> checkboxes -> results found */} | ||||
|                         <div className="flex flex-col items-center gap-3 text-sm sm:flex-row sm:items-center sm:gap-4"> | ||||
|                             <div className="flex flex-col items-center gap-1 sm:flex-row sm:items-center sm:gap-4"> | ||||
|                                 <span className="text-center font-medium"> | ||||
|                                     {selectedTables.size} /{' '} | ||||
|                                     {Math.min( | ||||
|                                         MAX_TABLES_IN_DIAGRAM, | ||||
|                                         allTables.length | ||||
|                                     )}{' '} | ||||
|                                     items selected | ||||
|                                 </span> | ||||
|                             </div> | ||||
|  | ||||
|                             <div className="flex items-center gap-3 sm:border-x sm:px-4"> | ||||
|                                 <div className="flex items-center gap-2"> | ||||
|                                     <Checkbox | ||||
|                                         checked={showTables} | ||||
|                                         onCheckedChange={(checked) => { | ||||
|                                             // Prevent unchecking if it's the only one checked | ||||
|                                             if (!checked && !showViews) return; | ||||
|                                             setShowTables(!!checked); | ||||
|                                         }} | ||||
|                                     /> | ||||
|                                     <Table | ||||
|                                         className="size-4" | ||||
|                                         strokeWidth={1.5} | ||||
|                                     /> | ||||
|                                     <span>tables</span> | ||||
|                                 </div> | ||||
|                                 <div className="flex items-center gap-2"> | ||||
|                                     <Checkbox | ||||
|                                         checked={showViews} | ||||
|                                         onCheckedChange={(checked) => { | ||||
|                                             // Prevent unchecking if it's the only one checked | ||||
|                                             if (!checked && !showTables) return; | ||||
|                                             setShowViews(!!checked); | ||||
|                                         }} | ||||
|                                     /> | ||||
|                                     <View | ||||
|                                         className="size-4" | ||||
|                                         strokeWidth={1.5} | ||||
|                                     /> | ||||
|                                     <span>views</span> | ||||
|                                 </div> | ||||
|                             </div> | ||||
|  | ||||
|                             <span className="hidden text-muted-foreground sm:inline"> | ||||
|                                 {filteredTables.length}{' '} | ||||
|                                 {filteredTables.length === 1 | ||||
|                                     ? 'result' | ||||
|                                     : 'results'}{' '} | ||||
|                                 found | ||||
|                             </span> | ||||
|                         </div> | ||||
|  | ||||
|                         {/* Right side: action buttons */} | ||||
|                         <div className="flex flex-wrap items-center justify-center gap-2"> | ||||
|                             {hasSearchResults && ( | ||||
|                                 <> | ||||
|                                     {/* Show page selection button when not searching and no selection */} | ||||
|                                     {!searchTerm && | ||||
|                                         selectedTables.size === 0 && ( | ||||
|                                             <Button | ||||
|                                                 variant="outline" | ||||
|                                                 size="sm" | ||||
|                                                 onClick={ | ||||
|                                                     handleTogglePageSelection | ||||
|                                                 } | ||||
|                                                 disabled={ | ||||
|                                                     paginatedTables.length === 0 | ||||
|                                                 } | ||||
|                                             > | ||||
|                                                 {allVisibleSelected | ||||
|                                                     ? 'Deselect' | ||||
|                                                     : 'Select'}{' '} | ||||
|                                                 page | ||||
|                                             </Button> | ||||
|                                         )} | ||||
|                                     {/* Show Select all button when there are unselected tables */} | ||||
|                                     {canSelectAllFiltered && | ||||
|                                         selectedTables.size === 0 && ( | ||||
|                                             <Button | ||||
|                                                 variant="outline" | ||||
|                                                 size="sm" | ||||
|                                                 onClick={ | ||||
|                                                     handleSelectAllFiltered | ||||
|                                                 } | ||||
|                                                 disabled={!canSelectAllFiltered} | ||||
|                                                 title={(() => { | ||||
|                                                     const unselectedCount = | ||||
|                                                         filteredTables.filter( | ||||
|                                                             (table) => | ||||
|                                                                 !selectedTables.has( | ||||
|                                                                     table.key | ||||
|                                                                 ) | ||||
|                                                         ).length; | ||||
|                                                     const remainingCapacity = | ||||
|                                                         MAX_TABLES_IN_DIAGRAM - | ||||
|                                                         selectedTables.size; | ||||
|                                                     if ( | ||||
|                                                         unselectedCount > | ||||
|                                                         remainingCapacity | ||||
|                                                     ) { | ||||
|                                                         return `Can only select ${remainingCapacity} more tables (${MAX_TABLES_IN_DIAGRAM} max limit)`; | ||||
|                                                     } | ||||
|                                                     return undefined; | ||||
|                                                 })()} | ||||
|                                             > | ||||
|                                                 {(() => { | ||||
|                                                     const unselectedCount = | ||||
|                                                         filteredTables.filter( | ||||
|                                                             (table) => | ||||
|                                                                 !selectedTables.has( | ||||
|                                                                     table.key | ||||
|                                                                 ) | ||||
|                                                         ).length; | ||||
|                                                     const remainingCapacity = | ||||
|                                                         MAX_TABLES_IN_DIAGRAM - | ||||
|                                                         selectedTables.size; | ||||
|                                                     if ( | ||||
|                                                         unselectedCount > | ||||
|                                                         remainingCapacity | ||||
|                                                     ) { | ||||
|                                                         return `Select ${remainingCapacity} of ${unselectedCount}`; | ||||
|                                                     } | ||||
|                                                     return `Select all ${unselectedCount}`; | ||||
|                                                 })()} | ||||
|                                             </Button> | ||||
|                                         )} | ||||
|                                 </> | ||||
|                             )} | ||||
|                             {selectedTables.size > 0 && ( | ||||
|                                 <> | ||||
|                                     {/* Show page selection/deselection button when user has selections */} | ||||
|                                     {paginatedTables.length > 0 && ( | ||||
|                                         <Button | ||||
|                                             variant="outline" | ||||
|                                             size="sm" | ||||
|                                             onClick={handleTogglePageSelection} | ||||
|                                         > | ||||
|                                             {allVisibleSelected | ||||
|                                                 ? 'Deselect' | ||||
|                                                 : 'Select'}{' '} | ||||
|                                             page | ||||
|                                         </Button> | ||||
|                                     )} | ||||
|                                     <Button | ||||
|                                         variant="outline" | ||||
|                                         size="sm" | ||||
|                                         onClick={handleClearSelection} | ||||
|                                     > | ||||
|                                         Clear selection | ||||
|                                     </Button> | ||||
|                                 </> | ||||
|                             )} | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|  | ||||
|                 {/* Table List */} | ||||
|                 <div className="flex min-h-[428px] flex-1 flex-col"> | ||||
|                     {hasSearchResults ? ( | ||||
|                         <> | ||||
|                             <div className="flex-1 py-4"> | ||||
|                                 <div className="space-y-1"> | ||||
|                                     {paginatedTables.map((table) => { | ||||
|                                         const isSelected = selectedTables.has( | ||||
|                                             table.key | ||||
|                                         ); | ||||
|                                         const isDisabled = | ||||
|                                             !isSelected && | ||||
|                                             selectedTables.size >= | ||||
|                                                 MAX_TABLES_IN_DIAGRAM; | ||||
|  | ||||
|                                         return ( | ||||
|                                             <div | ||||
|                                                 key={table.key} | ||||
|                                                 className={cn( | ||||
|                                                     'flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors', | ||||
|                                                     { | ||||
|                                                         'cursor-not-allowed': | ||||
|                                                             isDisabled, | ||||
|  | ||||
|                                                         'bg-muted hover:bg-muted/80': | ||||
|                                                             isSelected, | ||||
|                                                         'hover:bg-accent': | ||||
|                                                             !isSelected && | ||||
|                                                             !isDisabled, | ||||
|                                                     } | ||||
|                                                 )} | ||||
|                                             > | ||||
|                                                 <Checkbox | ||||
|                                                     checked={isSelected} | ||||
|                                                     disabled={isDisabled} | ||||
|                                                     onCheckedChange={() => | ||||
|                                                         handleTableToggle( | ||||
|                                                             table.key | ||||
|                                                         ) | ||||
|                                                     } | ||||
|                                                 /> | ||||
|                                                 {table.type === 'view' ? ( | ||||
|                                                     <View | ||||
|                                                         className="size-4" | ||||
|                                                         strokeWidth={1.5} | ||||
|                                                     /> | ||||
|                                                 ) : ( | ||||
|                                                     <Table | ||||
|                                                         className="size-4" | ||||
|                                                         strokeWidth={1.5} | ||||
|                                                     /> | ||||
|                                                 )} | ||||
|                                                 <span className="flex-1"> | ||||
|                                                     {table.schema ? ( | ||||
|                                                         <span className="text-muted-foreground"> | ||||
|                                                             {table.schema}. | ||||
|                                                         </span> | ||||
|                                                     ) : null} | ||||
|                                                     <span className="font-medium"> | ||||
|                                                         {table.tableName} | ||||
|                                                     </span> | ||||
|                                                     {table.type === 'view' && ( | ||||
|                                                         <span className="ml-2 text-xs text-muted-foreground"> | ||||
|                                                             (view) | ||||
|                                                         </span> | ||||
|                                                     )} | ||||
|                                                 </span> | ||||
|                                                 {isSelected && ( | ||||
|                                                     <Check className="size-4 text-pink-600" /> | ||||
|                                                 )} | ||||
|                                             </div> | ||||
|                                         ); | ||||
|                                     })} | ||||
|                                 </div> | ||||
|                             </div> | ||||
|                         </> | ||||
|                     ) : ( | ||||
|                         <div className="flex h-full items-center justify-center py-4"> | ||||
|                             <p className="text-sm text-muted-foreground"> | ||||
|                                 {searchTerm | ||||
|                                     ? 'No tables found matching your search.' | ||||
|                                     : 'Start typing to search for tables...'} | ||||
|                             </p> | ||||
|                         </div> | ||||
|                     )} | ||||
|                 </div> | ||||
|                 {isDesktop ? renderPagination() : null} | ||||
|             </DialogInternalContent> | ||||
|             <DialogFooter className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2 md:justify-between md:gap-0"> | ||||
|                 <Button | ||||
|                     type="button" | ||||
|                     variant="secondary" | ||||
|                     onClick={onBack} | ||||
|                     disabled={isImporting} | ||||
|                 > | ||||
|                     {t('new_diagram_dialog.back')} | ||||
|                 </Button> | ||||
|  | ||||
|                 <Button | ||||
|                     onClick={handleConfirm} | ||||
|                     disabled={selectedTables.size === 0 || isImporting} | ||||
|                     className="bg-pink-500 text-white hover:bg-pink-600" | ||||
|                 > | ||||
|                     {isImporting ? ( | ||||
|                         <> | ||||
|                             <Spinner className="mr-2 size-4 text-white" /> | ||||
|                             Importing... | ||||
|                         </> | ||||
|                     ) : ( | ||||
|                         `Import ${selectedTables.size} Tables` | ||||
|                     )} | ||||
|                 </Button> | ||||
|  | ||||
|                 {!isDesktop ? renderPagination() : null} | ||||
|             </DialogFooter> | ||||
|         </> | ||||
|     ); | ||||
| }; | ||||
| @@ -1,4 +1,5 @@ | ||||
| export enum CreateDiagramDialogStep { | ||||
|     SELECT_DATABASE = 'SELECT_DATABASE', | ||||
|     IMPORT_DATABASE = 'IMPORT_DATABASE', | ||||
|     SELECT_TABLES = 'SELECT_TABLES', | ||||
| } | ||||
|   | ||||
| @@ -15,9 +15,13 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition'; | ||||
| import { SelectDatabase } from './select-database/select-database'; | ||||
| import { CreateDiagramDialogStep } from './create-diagram-dialog-step'; | ||||
| import { ImportDatabase } from '../common/import-database/import-database'; | ||||
| import { SelectTables } from '../common/select-tables/select-tables'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||
| import { sqlImportToDiagram } from '@/lib/data/sql-import'; | ||||
| import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata'; | ||||
| import { filterMetadataByTables } from '@/lib/data/import-metadata/filter-metadata'; | ||||
| import { MAX_TABLES_WITHOUT_SHOWING_FILTER } from '../common/select-tables/constants'; | ||||
|  | ||||
| export interface CreateDiagramDialogProps extends BaseDialogProps {} | ||||
|  | ||||
| @@ -42,6 +46,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({ | ||||
|     const { listDiagrams, addDiagram } = useStorage(); | ||||
|     const [diagramNumber, setDiagramNumber] = useState<number>(1); | ||||
|     const navigate = useNavigate(); | ||||
|     const [parsedMetadata, setParsedMetadata] = useState<DatabaseMetadata>(); | ||||
|     const [isParsingMetadata, setIsParsingMetadata] = useState(false); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         setDatabaseEdition(undefined); | ||||
| @@ -62,49 +68,72 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({ | ||||
|         setDatabaseEdition(undefined); | ||||
|         setScriptResult(''); | ||||
|         setImportMethod('query'); | ||||
|         setParsedMetadata(undefined); | ||||
|     }, [dialog.open]); | ||||
|  | ||||
|     const hasExistingDiagram = (diagramId ?? '').trim().length !== 0; | ||||
|  | ||||
|     const importNewDiagram = useCallback(async () => { | ||||
|         let diagram: Diagram | undefined; | ||||
|     const importNewDiagram = useCallback( | ||||
|         async ({ | ||||
|             selectedTables, | ||||
|             databaseMetadata, | ||||
|         }: { | ||||
|             selectedTables?: SelectedTable[]; | ||||
|             databaseMetadata?: DatabaseMetadata; | ||||
|         } = {}) => { | ||||
|             let diagram: Diagram | undefined; | ||||
|  | ||||
|         if (importMethod === 'ddl') { | ||||
|             diagram = await sqlImportToDiagram({ | ||||
|                 sqlContent: scriptResult, | ||||
|                 sourceDatabaseType: databaseType, | ||||
|                 targetDatabaseType: databaseType, | ||||
|             if (importMethod === 'ddl') { | ||||
|                 diagram = await sqlImportToDiagram({ | ||||
|                     sqlContent: scriptResult, | ||||
|                     sourceDatabaseType: databaseType, | ||||
|                     targetDatabaseType: databaseType, | ||||
|                 }); | ||||
|             } else { | ||||
|                 let metadata: DatabaseMetadata | undefined = databaseMetadata; | ||||
|  | ||||
|                 if (!metadata) { | ||||
|                     metadata = loadDatabaseMetadata(scriptResult); | ||||
|                 } | ||||
|  | ||||
|                 if (selectedTables && selectedTables.length > 0) { | ||||
|                     metadata = filterMetadataByTables({ | ||||
|                         metadata, | ||||
|                         selectedTables, | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 diagram = await loadFromDatabaseMetadata({ | ||||
|                     databaseType, | ||||
|                     databaseMetadata: metadata, | ||||
|                     diagramNumber, | ||||
|                     databaseEdition: | ||||
|                         databaseEdition?.trim().length === 0 | ||||
|                             ? undefined | ||||
|                             : databaseEdition, | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             await addDiagram({ diagram }); | ||||
|             await updateConfig({ | ||||
|                 config: { defaultDiagramId: diagram.id }, | ||||
|             }); | ||||
|         } else { | ||||
|             const databaseMetadata: DatabaseMetadata = | ||||
|                 loadDatabaseMetadata(scriptResult); | ||||
|  | ||||
|             diagram = await loadFromDatabaseMetadata({ | ||||
|                 databaseType, | ||||
|                 databaseMetadata, | ||||
|                 diagramNumber, | ||||
|                 databaseEdition: | ||||
|                     databaseEdition?.trim().length === 0 | ||||
|                         ? undefined | ||||
|                         : databaseEdition, | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         await addDiagram({ diagram }); | ||||
|         await updateConfig({ config: { defaultDiagramId: diagram.id } }); | ||||
|         closeCreateDiagramDialog(); | ||||
|         navigate(`/diagrams/${diagram.id}`); | ||||
|     }, [ | ||||
|         importMethod, | ||||
|         databaseType, | ||||
|         addDiagram, | ||||
|         databaseEdition, | ||||
|         closeCreateDiagramDialog, | ||||
|         navigate, | ||||
|         updateConfig, | ||||
|         scriptResult, | ||||
|         diagramNumber, | ||||
|     ]); | ||||
|             closeCreateDiagramDialog(); | ||||
|             navigate(`/diagrams/${diagram.id}`); | ||||
|         }, | ||||
|         [ | ||||
|             importMethod, | ||||
|             databaseType, | ||||
|             addDiagram, | ||||
|             databaseEdition, | ||||
|             closeCreateDiagramDialog, | ||||
|             navigate, | ||||
|             updateConfig, | ||||
|             scriptResult, | ||||
|             diagramNumber, | ||||
|         ] | ||||
|     ); | ||||
|  | ||||
|     const createEmptyDiagram = useCallback(async () => { | ||||
|         const diagram: Diagram = { | ||||
| @@ -138,10 +167,56 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({ | ||||
|         openImportDBMLDialog, | ||||
|     ]); | ||||
|  | ||||
|     const importNewDiagramOrFilterTables = useCallback(async () => { | ||||
|         try { | ||||
|             setIsParsingMetadata(true); | ||||
|  | ||||
|             if (importMethod === 'ddl') { | ||||
|                 await importNewDiagram(); | ||||
|             } else { | ||||
|                 // Parse metadata asynchronously to avoid blocking the UI | ||||
|                 const metadata = await new Promise<DatabaseMetadata>( | ||||
|                     (resolve, reject) => { | ||||
|                         setTimeout(() => { | ||||
|                             try { | ||||
|                                 const result = | ||||
|                                     loadDatabaseMetadata(scriptResult); | ||||
|                                 resolve(result); | ||||
|                             } catch (err) { | ||||
|                                 reject(err); | ||||
|                             } | ||||
|                         }, 0); | ||||
|                     } | ||||
|                 ); | ||||
|  | ||||
|                 const totalTablesAndViews = | ||||
|                     metadata.tables.length + (metadata.views?.length || 0); | ||||
|  | ||||
|                 setParsedMetadata(metadata); | ||||
|  | ||||
|                 // Check if it's a large database that needs table selection | ||||
|                 if (totalTablesAndViews > MAX_TABLES_WITHOUT_SHOWING_FILTER) { | ||||
|                     setStep(CreateDiagramDialogStep.SELECT_TABLES); | ||||
|                 } else { | ||||
|                     await importNewDiagram({ | ||||
|                         databaseMetadata: metadata, | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         } finally { | ||||
|             setIsParsingMetadata(false); | ||||
|         } | ||||
|     }, [importMethod, scriptResult, importNewDiagram]); | ||||
|  | ||||
|     return ( | ||||
|         <Dialog | ||||
|             {...dialog} | ||||
|             onOpenChange={(open) => { | ||||
|                 // Don't allow closing while parsing metadata | ||||
|                 if (isParsingMetadata) { | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 if (!hasExistingDiagram) { | ||||
|                     return; | ||||
|                 } | ||||
| @@ -154,6 +229,8 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({ | ||||
|             <DialogContent | ||||
|                 className="flex max-h-dvh w-full flex-col md:max-w-[900px]" | ||||
|                 showClose={hasExistingDiagram} | ||||
|                 onInteractOutside={(e) => e.preventDefault()} | ||||
|                 onEscapeKeyDown={(e) => e.preventDefault()} | ||||
|             > | ||||
|                 {step === CreateDiagramDialogStep.SELECT_DATABASE ? ( | ||||
|                     <SelectDatabase | ||||
| @@ -165,9 +242,9 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({ | ||||
|                             setStep(CreateDiagramDialogStep.IMPORT_DATABASE) | ||||
|                         } | ||||
|                     /> | ||||
|                 ) : ( | ||||
|                 ) : step === CreateDiagramDialogStep.IMPORT_DATABASE ? ( | ||||
|                     <ImportDatabase | ||||
|                         onImport={importNewDiagram} | ||||
|                         onImport={importNewDiagramOrFilterTables} | ||||
|                         onCreateEmptyDiagram={createEmptyDiagram} | ||||
|                         databaseEdition={databaseEdition} | ||||
|                         databaseType={databaseType} | ||||
| @@ -180,8 +257,18 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({ | ||||
|                         title={t('new_diagram_dialog.import_database.title')} | ||||
|                         importMethod={importMethod} | ||||
|                         setImportMethod={setImportMethod} | ||||
|                         keepDialogAfterImport={true} | ||||
|                     /> | ||||
|                 )} | ||||
|                 ) : step === CreateDiagramDialogStep.SELECT_TABLES ? ( | ||||
|                     <SelectTables | ||||
|                         isLoading={isParsingMetadata || !parsedMetadata} | ||||
|                         databaseMetadata={parsedMetadata} | ||||
|                         onImport={importNewDiagram} | ||||
|                         onBack={() => | ||||
|                             setStep(CreateDiagramDialogStep.IMPORT_DATABASE) | ||||
|                         } | ||||
|                     /> | ||||
|                 ) : null} | ||||
|             </DialogContent> | ||||
|         </Dialog> | ||||
|     ); | ||||
|   | ||||
| @@ -218,8 +218,14 @@ export const CreateRelationshipDialog: React.FC< | ||||
|                     closeCreateRelationshipDialog(); | ||||
|                 } | ||||
|             }} | ||||
|             modal={false} | ||||
|         > | ||||
|             <DialogContent className="flex flex-col overflow-y-auto" showClose> | ||||
|             <DialogContent | ||||
|                 className="flex flex-col overflow-y-auto" | ||||
|                 showClose | ||||
|                 forceOverlay | ||||
|                 onInteractOutside={(e) => e.preventDefault()} | ||||
|             > | ||||
|                 <DialogHeader> | ||||
|                     <DialogTitle> | ||||
|                         {t('create_relationship_dialog.title')} | ||||
|   | ||||
| @@ -20,12 +20,18 @@ 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'; | ||||
| import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter'; | ||||
| import { | ||||
|     filterDependency, | ||||
|     filterRelationship, | ||||
|     filterTable, | ||||
| } from '@/lib/domain/diagram-filter/filter'; | ||||
| import { defaultSchemas } from '@/lib/data/default-schemas'; | ||||
|  | ||||
| export interface ExportSQLDialogProps extends BaseDialogProps { | ||||
|     targetDatabaseType: DatabaseType; | ||||
| @@ -36,7 +42,8 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | ||||
|     targetDatabaseType, | ||||
| }) => { | ||||
|     const { closeExportSQLDialog } = useDialog(); | ||||
|     const { currentDiagram, filteredSchemas } = useChartDB(); | ||||
|     const { currentDiagram } = useChartDB(); | ||||
|     const { filter } = useDiagramFilter(); | ||||
|     const { t } = useTranslation(); | ||||
|     const [script, setScript] = React.useState<string>(); | ||||
|     const [error, setError] = React.useState<boolean>(false); | ||||
| @@ -48,7 +55,16 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | ||||
|         const filteredDiagram: Diagram = { | ||||
|             ...currentDiagram, | ||||
|             tables: currentDiagram.tables?.filter((table) => | ||||
|                 shouldShowTablesBySchemaFilter(table, filteredSchemas) | ||||
|                 filterTable({ | ||||
|                     table: { | ||||
|                         id: table.id, | ||||
|                         schema: table.schema, | ||||
|                     }, | ||||
|                     filter, | ||||
|                     options: { | ||||
|                         defaultSchema: defaultSchemas[targetDatabaseType], | ||||
|                     }, | ||||
|                 }) | ||||
|             ), | ||||
|             relationships: currentDiagram.relationships?.filter((rel) => { | ||||
|                 const sourceTable = currentDiagram.tables?.find( | ||||
| @@ -60,11 +76,20 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | ||||
|                 return ( | ||||
|                     sourceTable && | ||||
|                     targetTable && | ||||
|                     shouldShowTablesBySchemaFilter( | ||||
|                         sourceTable, | ||||
|                         filteredSchemas | ||||
|                     ) && | ||||
|                     shouldShowTablesBySchemaFilter(targetTable, filteredSchemas) | ||||
|                     filterRelationship({ | ||||
|                         tableA: { | ||||
|                             id: sourceTable.id, | ||||
|                             schema: sourceTable.schema, | ||||
|                         }, | ||||
|                         tableB: { | ||||
|                             id: targetTable.id, | ||||
|                             schema: targetTable.schema, | ||||
|                         }, | ||||
|                         filter, | ||||
|                         options: { | ||||
|                             defaultSchema: defaultSchemas[targetDatabaseType], | ||||
|                         }, | ||||
|                     }) | ||||
|                 ); | ||||
|             }), | ||||
|             dependencies: currentDiagram.dependencies?.filter((dep) => { | ||||
| @@ -77,11 +102,20 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | ||||
|                 return ( | ||||
|                     table && | ||||
|                     dependentTable && | ||||
|                     shouldShowTablesBySchemaFilter(table, filteredSchemas) && | ||||
|                     shouldShowTablesBySchemaFilter( | ||||
|                         dependentTable, | ||||
|                         filteredSchemas | ||||
|                     ) | ||||
|                     filterDependency({ | ||||
|                         tableA: { | ||||
|                             id: table.id, | ||||
|                             schema: table.schema, | ||||
|                         }, | ||||
|                         tableB: { | ||||
|                             id: dependentTable.id, | ||||
|                             schema: dependentTable.schema, | ||||
|                         }, | ||||
|                         filter, | ||||
|                         options: { | ||||
|                             defaultSchema: defaultSchemas[targetDatabaseType], | ||||
|                         }, | ||||
|                     }) | ||||
|                 ); | ||||
|             }), | ||||
|         }; | ||||
| @@ -101,7 +135,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({ | ||||
|                 signal: abortControllerRef.current?.signal, | ||||
|             }); | ||||
|         } | ||||
|     }, [targetDatabaseType, currentDiagram, filteredSchemas]); | ||||
|     }, [targetDatabaseType, currentDiagram, filter]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (!dialog.open) { | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import React, { | ||||
|     Suspense, | ||||
|     useRef, | ||||
| } from 'react'; | ||||
| import * as monaco from 'monaco-editor'; | ||||
| import type * as monaco from 'monaco-editor'; | ||||
| import { useDialog } from '@/hooks/use-dialog'; | ||||
| import { | ||||
|     Dialog, | ||||
| @@ -23,53 +23,24 @@ 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, sanitizeDBML } from '@/lib/dbml-import'; | ||||
| import { | ||||
|     importDBMLToDiagram, | ||||
|     sanitizeDBML, | ||||
|     preprocessDBML, | ||||
| } from '@/lib/dbml/dbml-import/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 type { DBTable } from '@/lib/domain/db-table'; | ||||
| 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; | ||||
| } | ||||
| import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error'; | ||||
| import { | ||||
|     clearErrorHighlight, | ||||
|     highlightErrorLine, | ||||
| } from '@/components/code-snippet/dbml/utils'; | ||||
|  | ||||
| export interface ImportDBMLDialogProps extends BaseDialogProps { | ||||
|     withCreateEmptyDiagram?: boolean; | ||||
| @@ -145,39 +116,8 @@ Ref: comments.user_id > users.id // Each comment is written by one user`; | ||||
|         } | ||||
|     }, [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(); | ||||
|         clearErrorHighlight(decorationsCollection.current); | ||||
|     }, []); | ||||
|  | ||||
|     const validateDBML = useCallback( | ||||
| @@ -189,7 +129,8 @@ Ref: comments.user_id > users.id // Each comment is written by one user`; | ||||
|             if (!content.trim()) return; | ||||
|  | ||||
|             try { | ||||
|                 const sanitizedContent = sanitizeDBML(content); | ||||
|                 const preprocessedContent = preprocessDBML(content); | ||||
|                 const sanitizedContent = sanitizeDBML(preprocessedContent); | ||||
|                 const parser = new Parser(); | ||||
|                 parser.parse(sanitizedContent, 'dbml'); | ||||
|             } catch (e) { | ||||
| @@ -199,7 +140,12 @@ Ref: comments.user_id > users.id // Each comment is written by one user`; | ||||
|                         t('import_dbml_dialog.error.description') + | ||||
|                             ` (1 error found - in line ${parsedError.line})` | ||||
|                     ); | ||||
|                     highlightErrorLine(parsedError); | ||||
|                     highlightErrorLine({ | ||||
|                         error: parsedError, | ||||
|                         model: editorRef.current?.getModel(), | ||||
|                         editorDecorationsCollection: | ||||
|                             decorationsCollection.current, | ||||
|                     }); | ||||
|                 } else { | ||||
|                     setErrorMessage( | ||||
|                         e instanceof Error ? e.message : JSON.stringify(e) | ||||
| @@ -207,7 +153,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`; | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         [clearDecorations, highlightErrorLine, t] | ||||
|         [clearDecorations, t] | ||||
|     ); | ||||
|  | ||||
|     const debouncedValidateRef = useRef<((value: string) => void) | null>(null); | ||||
| @@ -242,13 +188,11 @@ Ref: comments.user_id > users.id // Each comment is written by one user`; | ||||
|         if (!dbmlContent.trim() || errorMessage) return; | ||||
|  | ||||
|         try { | ||||
|             // Sanitize DBML content before importing | ||||
|             const sanitizedContent = sanitizeDBML(dbmlContent); | ||||
|             const importedDiagram = await importDBMLToDiagram(sanitizedContent); | ||||
|             const importedDiagram = await importDBMLToDiagram(dbmlContent); | ||||
|             const tableIdsToRemove = tables | ||||
|                 .filter((table) => | ||||
|                     importedDiagram.tables?.some( | ||||
|                         (t) => | ||||
|                         (t: DBTable) => | ||||
|                             t.name === table.name && t.schema === table.schema | ||||
|                     ) | ||||
|                 ) | ||||
| @@ -257,19 +201,21 @@ Ref: comments.user_id > users.id // Each comment is written by one user`; | ||||
|             const relationshipIdsToRemove = relationships | ||||
|                 .filter((relationship) => { | ||||
|                     const sourceTable = tables.find( | ||||
|                         (table) => table.id === relationship.sourceTableId | ||||
|                         (table: DBTable) => | ||||
|                             table.id === relationship.sourceTableId | ||||
|                     ); | ||||
|                     const targetTable = tables.find( | ||||
|                         (table) => table.id === relationship.targetTableId | ||||
|                         (table: DBTable) => | ||||
|                             table.id === relationship.targetTableId | ||||
|                     ); | ||||
|                     if (!sourceTable || !targetTable) return true; | ||||
|                     const replacementSourceTable = importedDiagram.tables?.find( | ||||
|                         (table) => | ||||
|                         (table: DBTable) => | ||||
|                             table.name === sourceTable.name && | ||||
|                             table.schema === sourceTable.schema | ||||
|                     ); | ||||
|                     const replacementTargetTable = importedDiagram.tables?.find( | ||||
|                         (table) => | ||||
|                         (table: DBTable) => | ||||
|                             table.name === targetTable.name && | ||||
|                             table.schema === targetTable.schema | ||||
|                     ); | ||||
|   | ||||
| @@ -0,0 +1,98 @@ | ||||
| import React, { useCallback } from 'react'; | ||||
| import { | ||||
|     DropdownMenu, | ||||
|     DropdownMenuContent, | ||||
|     DropdownMenuItem, | ||||
|     DropdownMenuSeparator, | ||||
|     DropdownMenuTrigger, | ||||
| } from '@/components/dropdown-menu/dropdown-menu'; | ||||
| import { Button } from '@/components/button/button'; | ||||
| import { Ellipsis, Layers2, SquareArrowOutUpRight, Trash2 } from 'lucide-react'; | ||||
| import { useChartDB } from '@/hooks/use-chartdb'; | ||||
| import type { Diagram } from '@/lib/domain'; | ||||
| import { useStorage } from '@/hooks/use-storage'; | ||||
| import { cloneDiagram } from '@/lib/clone'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
|  | ||||
| interface DiagramRowActionsMenuProps { | ||||
|     diagram: Diagram; | ||||
|     onOpen: () => void; | ||||
|     refetch: () => void; | ||||
|     numberOfDiagrams: number; | ||||
| } | ||||
|  | ||||
| export const DiagramRowActionsMenu: React.FC<DiagramRowActionsMenuProps> = ({ | ||||
|     diagram, | ||||
|     onOpen, | ||||
|     refetch, | ||||
|     numberOfDiagrams, | ||||
| }) => { | ||||
|     const { diagramId } = useChartDB(); | ||||
|     const { deleteDiagram, addDiagram } = useStorage(); | ||||
|     const { t } = useTranslation(); | ||||
|  | ||||
|     const onDelete = useCallback(async () => { | ||||
|         deleteDiagram(diagram.id); | ||||
|         refetch(); | ||||
|  | ||||
|         if (diagram.id === diagramId || numberOfDiagrams <= 1) { | ||||
|             window.location.href = '/'; | ||||
|         } | ||||
|     }, [deleteDiagram, diagram.id, diagramId, refetch, numberOfDiagrams]); | ||||
|  | ||||
|     const onDuplicate = useCallback(async () => { | ||||
|         const duplicatedDiagram = cloneDiagram(diagram); | ||||
|  | ||||
|         const diagramToAdd = duplicatedDiagram.diagram; | ||||
|  | ||||
|         if (!diagramToAdd) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         diagramToAdd.name = `${diagram.name} (Copy)`; | ||||
|  | ||||
|         addDiagram({ diagram: diagramToAdd }); | ||||
|         refetch(); | ||||
|     }, [addDiagram, refetch, diagram]); | ||||
|  | ||||
|     return ( | ||||
|         <DropdownMenu> | ||||
|             <DropdownMenuTrigger asChild> | ||||
|                 <Button | ||||
|                     variant="ghost" | ||||
|                     size="icon" | ||||
|                     className="size-8 p-0" | ||||
|                     onClick={(e) => e.stopPropagation()} | ||||
|                 > | ||||
|                     <Ellipsis className="size-4" /> | ||||
|                 </Button> | ||||
|             </DropdownMenuTrigger> | ||||
|             <DropdownMenuContent align="end"> | ||||
|                 <DropdownMenuItem | ||||
|                     onClick={onOpen} | ||||
|                     className="flex justify-between gap-4" | ||||
|                 > | ||||
|                     {t('open_diagram_dialog.diagram_actions.open')} | ||||
|                     <SquareArrowOutUpRight className="size-3.5" /> | ||||
|                 </DropdownMenuItem> | ||||
|  | ||||
|                 <DropdownMenuItem | ||||
|                     onClick={onDuplicate} | ||||
|                     className="flex justify-between gap-4" | ||||
|                 > | ||||
|                     {t('open_diagram_dialog.diagram_actions.duplicate')} | ||||
|                     <Layers2 className="size-3.5" /> | ||||
|                 </DropdownMenuItem> | ||||
|  | ||||
|                 <DropdownMenuSeparator /> | ||||
|                 <DropdownMenuItem | ||||
|                     onClick={onDelete} | ||||
|                     className="flex justify-between gap-4 text-red-700" | ||||
|                 > | ||||
|                     {t('open_diagram_dialog.diagram_actions.delete')} | ||||
|                     <Trash2 className="size-3.5 text-red-700" /> | ||||
|                 </DropdownMenuItem> | ||||
|             </DropdownMenuContent> | ||||
|         </DropdownMenu> | ||||
|     ); | ||||
| }; | ||||
| @@ -27,6 +27,7 @@ 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'; | ||||
| import { DiagramRowActionsMenu } from './diagram-row-actions-menu/diagram-row-actions-menu'; | ||||
|  | ||||
| export interface OpenDiagramDialogProps extends BaseDialogProps { | ||||
|     canClose?: boolean; | ||||
| @@ -46,21 +47,22 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | ||||
|         string | undefined | ||||
|     >(); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         setSelectedDiagramId(undefined); | ||||
|     }, [dialog.open]); | ||||
|     const fetchDiagrams = useCallback(async () => { | ||||
|         const diagrams = await listDiagrams({ includeTables: true }); | ||||
|         setDiagrams( | ||||
|             diagrams.sort( | ||||
|                 (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime() | ||||
|             ) | ||||
|         ); | ||||
|     }, [listDiagrams]); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         const fetchDiagrams = async () => { | ||||
|             const diagrams = await listDiagrams({ includeTables: true }); | ||||
|             setDiagrams( | ||||
|                 diagrams.sort( | ||||
|                     (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime() | ||||
|                 ) | ||||
|             ); | ||||
|         }; | ||||
|         if (!dialog.open) { | ||||
|             return; | ||||
|         } | ||||
|         setSelectedDiagramId(undefined); | ||||
|         fetchDiagrams(); | ||||
|     }, [listDiagrams, setDiagrams, dialog.open]); | ||||
|     }, [dialog.open, fetchDiagrams]); | ||||
|  | ||||
|     const openDiagram = useCallback( | ||||
|         (diagramId: string) => { | ||||
| @@ -166,6 +168,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | ||||
|                                             'open_diagram_dialog.table_columns.tables_count' | ||||
|                                         )} | ||||
|                                     </TableHead> | ||||
|                                     <TableHead /> | ||||
|                                 </TableRow> | ||||
|                             </TableHeader> | ||||
|                             <TableBody> | ||||
| @@ -221,6 +224,19 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | ||||
|                                         <TableCell className="text-center"> | ||||
|                                             {diagram.tables?.length} | ||||
|                                         </TableCell> | ||||
|                                         <TableCell className="items-center p-0 pr-1 text-right"> | ||||
|                                             <DiagramRowActionsMenu | ||||
|                                                 diagram={diagram} | ||||
|                                                 onOpen={() => { | ||||
|                                                     openDiagram(diagram.id); | ||||
|                                                     closeOpenDiagramDialog(); | ||||
|                                                 }} | ||||
|                                                 numberOfDiagrams={ | ||||
|                                                     diagrams.length | ||||
|                                                 } | ||||
|                                                 refetch={fetchDiagrams} | ||||
|                                             /> | ||||
|                                         </TableCell> | ||||
|                                     </TableRow> | ||||
|                                 ))} | ||||
|                             </TableBody> | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import React, { useCallback, useEffect, useMemo } from 'react'; | ||||
| import React, { useCallback, useEffect, useMemo, useState } from 'react'; | ||||
| import { useDialog } from '@/hooks/use-dialog'; | ||||
| import { | ||||
|     Dialog, | ||||
| @@ -17,11 +17,23 @@ import type { DBSchema } from '@/lib/domain/db-schema'; | ||||
| import { schemaNameToSchemaId } from '@/lib/domain/db-schema'; | ||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { Input } from '@/components/input/input'; | ||||
| import { Separator } from '@/components/separator/separator'; | ||||
| import { Group, SquarePlus } from 'lucide-react'; | ||||
| import { | ||||
|     Tooltip, | ||||
|     TooltipContent, | ||||
|     TooltipTrigger, | ||||
| } from '@/components/tooltip/tooltip'; | ||||
| import { useChartDB } from '@/hooks/use-chartdb'; | ||||
| import { defaultSchemas } from '@/lib/data/default-schemas'; | ||||
| import { Label } from '@/components/label/label'; | ||||
|  | ||||
| export interface TableSchemaDialogProps extends BaseDialogProps { | ||||
|     table?: DBTable; | ||||
|     schemas: DBSchema[]; | ||||
|     onConfirm: (schema: string) => void; | ||||
|     onConfirm: ({ schema }: { schema: DBSchema }) => void; | ||||
|     allowSchemaCreation?: boolean; | ||||
| } | ||||
|  | ||||
| export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({ | ||||
| @@ -29,27 +41,73 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({ | ||||
|     table, | ||||
|     schemas, | ||||
|     onConfirm, | ||||
|     allowSchemaCreation = false, | ||||
| }) => { | ||||
|     const { t } = useTranslation(); | ||||
|     const [selectedSchema, setSelectedSchema] = React.useState<string>( | ||||
|     const { databaseType } = useChartDB(); | ||||
|     const [selectedSchemaId, setSelectedSchemaId] = useState<string>( | ||||
|         table?.schema | ||||
|             ? schemaNameToSchemaId(table.schema) | ||||
|             : (schemas?.[0]?.id ?? '') | ||||
|     ); | ||||
|     const allowSchemaSelection = useMemo( | ||||
|         () => schemas && schemas.length > 0, | ||||
|         [schemas] | ||||
|     ); | ||||
|  | ||||
|     const defaultSchemaName = useMemo( | ||||
|         () => defaultSchemas?.[databaseType], | ||||
|         [databaseType] | ||||
|     ); | ||||
|  | ||||
|     const [isCreatingNew, setIsCreatingNew] = | ||||
|         useState<boolean>(!allowSchemaSelection); | ||||
|     const [newSchemaName, setNewSchemaName] = useState<string>( | ||||
|         allowSchemaCreation && !allowSchemaSelection | ||||
|             ? (defaultSchemaName ?? '') | ||||
|             : '' | ||||
|     ); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (!dialog.open) return; | ||||
|         setSelectedSchema( | ||||
|         setSelectedSchemaId( | ||||
|             table?.schema | ||||
|                 ? schemaNameToSchemaId(table.schema) | ||||
|                 : (schemas?.[0]?.id ?? '') | ||||
|         ); | ||||
|     }, [dialog.open, schemas, table?.schema]); | ||||
|         setIsCreatingNew(!allowSchemaSelection); | ||||
|         setNewSchemaName( | ||||
|             allowSchemaCreation && !allowSchemaSelection | ||||
|                 ? (defaultSchemaName ?? '') | ||||
|                 : '' | ||||
|         ); | ||||
|     }, [ | ||||
|         defaultSchemaName, | ||||
|         dialog.open, | ||||
|         schemas, | ||||
|         table?.schema, | ||||
|         allowSchemaSelection, | ||||
|         allowSchemaCreation, | ||||
|     ]); | ||||
|  | ||||
|     const { closeTableSchemaDialog } = useDialog(); | ||||
|  | ||||
|     const handleConfirm = useCallback(() => { | ||||
|         onConfirm(selectedSchema); | ||||
|     }, [onConfirm, selectedSchema]); | ||||
|         if (isCreatingNew && newSchemaName.trim()) { | ||||
|             const newSchema: DBSchema = { | ||||
|                 id: schemaNameToSchemaId(newSchemaName.trim()), | ||||
|                 name: newSchemaName.trim(), | ||||
|                 tableCount: 0, | ||||
|             }; | ||||
|  | ||||
|             onConfirm({ schema: newSchema }); | ||||
|         } else { | ||||
|             const schema = schemas.find((s) => s.id === selectedSchemaId); | ||||
|             if (!schema) return; | ||||
|  | ||||
|             onConfirm({ schema }); | ||||
|         } | ||||
|     }, [onConfirm, selectedSchemaId, schemas, isCreatingNew, newSchemaName]); | ||||
|  | ||||
|     const schemaOptions: SelectBoxOption[] = useMemo( | ||||
|         () => | ||||
| @@ -60,6 +118,25 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({ | ||||
|         [schemas] | ||||
|     ); | ||||
|  | ||||
|     const renderSwitchCreateOrSelectButton = useCallback( | ||||
|         () => ( | ||||
|             <Button | ||||
|                 variant="outline" | ||||
|                 className="w-full justify-start" | ||||
|                 onClick={() => setIsCreatingNew(!isCreatingNew)} | ||||
|                 disabled={!allowSchemaSelection || !allowSchemaCreation} | ||||
|             > | ||||
|                 {!isCreatingNew ? ( | ||||
|                     <SquarePlus className="mr-2 size-4 " /> | ||||
|                 ) : ( | ||||
|                     <Group className="mr-2 size-4 " /> | ||||
|                 )} | ||||
|                 {isCreatingNew ? 'Select existing schema' : 'Create new schema'} | ||||
|             </Button> | ||||
|         ), | ||||
|         [isCreatingNew, allowSchemaSelection, allowSchemaCreation] | ||||
|     ); | ||||
|  | ||||
|     return ( | ||||
|         <Dialog | ||||
|             {...dialog} | ||||
| @@ -67,48 +144,106 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({ | ||||
|                 if (!open) { | ||||
|                     closeTableSchemaDialog(); | ||||
|                 } | ||||
|  | ||||
|                 setTimeout(() => (document.body.style.pointerEvents = ''), 500); | ||||
|             }} | ||||
|         > | ||||
|             <DialogContent className="flex flex-col" showClose> | ||||
|                 <DialogHeader> | ||||
|                     <DialogTitle> | ||||
|                         {table | ||||
|                             ? t('update_table_schema_dialog.title') | ||||
|                             : t('new_table_schema_dialog.title')} | ||||
|                         {!allowSchemaSelection && allowSchemaCreation | ||||
|                             ? t('create_table_schema_dialog.title') | ||||
|                             : table | ||||
|                               ? t('update_table_schema_dialog.title') | ||||
|                               : t('new_table_schema_dialog.title')} | ||||
|                     </DialogTitle> | ||||
|                     <DialogDescription> | ||||
|                         {table | ||||
|                             ? t('update_table_schema_dialog.description', { | ||||
|                                   tableName: table.name, | ||||
|                               }) | ||||
|                             : t('new_table_schema_dialog.description')} | ||||
|                         {!allowSchemaSelection && allowSchemaCreation | ||||
|                             ? t('create_table_schema_dialog.description') | ||||
|                             : table | ||||
|                               ? t('update_table_schema_dialog.description', { | ||||
|                                     tableName: table.name, | ||||
|                                 }) | ||||
|                               : t('new_table_schema_dialog.description')} | ||||
|                     </DialogDescription> | ||||
|                 </DialogHeader> | ||||
|                 <div className="grid gap-4 py-1"> | ||||
|                     <div className="grid w-full items-center gap-4"> | ||||
|                         <SelectBox | ||||
|                             options={schemaOptions} | ||||
|                             multiple={false} | ||||
|                             value={selectedSchema} | ||||
|                             onChange={(value) => | ||||
|                                 setSelectedSchema(value as string) | ||||
|                             } | ||||
|                         /> | ||||
|                         {!isCreatingNew ? ( | ||||
|                             <SelectBox | ||||
|                                 options={schemaOptions} | ||||
|                                 multiple={false} | ||||
|                                 value={selectedSchemaId} | ||||
|                                 onChange={(value) => | ||||
|                                     setSelectedSchemaId(value as string) | ||||
|                                 } | ||||
|                             /> | ||||
|                         ) : ( | ||||
|                             <div className="flex flex-col gap-2"> | ||||
|                                 {allowSchemaCreation && | ||||
|                                 !allowSchemaSelection ? ( | ||||
|                                     <Label htmlFor="new-schema-name"> | ||||
|                                         Schema Name | ||||
|                                     </Label> | ||||
|                                 ) : null} | ||||
|                                 <Input | ||||
|                                     id="new-schema-name" | ||||
|                                     value={newSchemaName} | ||||
|                                     onChange={(e) => | ||||
|                                         setNewSchemaName(e.target.value) | ||||
|                                     } | ||||
|                                     placeholder={`Enter schema name.${defaultSchemaName ? ` e.g. ${defaultSchemaName}.` : ''}`} | ||||
|                                     autoFocus | ||||
|                                 /> | ||||
|                             </div> | ||||
|                         )} | ||||
|  | ||||
|                         {allowSchemaCreation && allowSchemaSelection ? ( | ||||
|                             <> | ||||
|                                 <div className="relative"> | ||||
|                                     <Separator className="my-2" /> | ||||
|                                     <span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-background px-2 text-xs text-muted-foreground"> | ||||
|                                         or | ||||
|                                     </span> | ||||
|                                 </div> | ||||
|                                 {allowSchemaSelection ? ( | ||||
|                                     renderSwitchCreateOrSelectButton() | ||||
|                                 ) : ( | ||||
|                                     <Tooltip> | ||||
|                                         <TooltipTrigger asChild> | ||||
|                                             <span> | ||||
|                                                 {renderSwitchCreateOrSelectButton()} | ||||
|                                             </span> | ||||
|                                         </TooltipTrigger> | ||||
|                                         <TooltipContent> | ||||
|                                             <p>No existing schemas available</p> | ||||
|                                         </TooltipContent> | ||||
|                                     </Tooltip> | ||||
|                                 )} | ||||
|                             </> | ||||
|                         ) : null} | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <DialogFooter className="flex gap-1 md:justify-between"> | ||||
|                     <DialogClose asChild> | ||||
|                         <Button variant="secondary"> | ||||
|                             {table | ||||
|                                 ? t('update_table_schema_dialog.cancel') | ||||
|                                 : t('new_table_schema_dialog.cancel')} | ||||
|                             {isCreatingNew | ||||
|                                 ? t('create_table_schema_dialog.cancel') | ||||
|                                 : table | ||||
|                                   ? t('update_table_schema_dialog.cancel') | ||||
|                                   : t('new_table_schema_dialog.cancel')} | ||||
|                         </Button> | ||||
|                     </DialogClose> | ||||
|                     <DialogClose asChild> | ||||
|                         <Button onClick={handleConfirm}> | ||||
|                             {table | ||||
|                                 ? t('update_table_schema_dialog.confirm') | ||||
|                                 : t('new_table_schema_dialog.confirm')} | ||||
|                         <Button | ||||
|                             onClick={handleConfirm} | ||||
|                             disabled={isCreatingNew && !newSchemaName.trim()} | ||||
|                         > | ||||
|                             {isCreatingNew | ||||
|                                 ? t('create_table_schema_dialog.create') | ||||
|                                 : table | ||||
|                                   ? t('update_table_schema_dialog.confirm') | ||||
|                                   : t('new_table_schema_dialog.confirm')} | ||||
|                         </Button> | ||||
|                     </DialogClose> | ||||
|                 </DialogFooter> | ||||
|   | ||||
| @@ -83,6 +83,7 @@ | ||||
|     } | ||||
|     body { | ||||
|         @apply bg-background text-foreground; | ||||
|         overscroll-behavior-x: none; | ||||
|     } | ||||
|  | ||||
|     .text-editable { | ||||
| @@ -154,3 +155,29 @@ | ||||
|         background-size: 650%; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* Edit button emphasis animation */ | ||||
| @keyframes dbml_edit-button-emphasis { | ||||
|     0% { | ||||
|         transform: scale(1); | ||||
|         box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); | ||||
|         background-color: rgba(59, 130, 246, 0); | ||||
|     } | ||||
|     50% { | ||||
|         transform: scale(1.1); | ||||
|         box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); | ||||
|         background-color: rgba(59, 130, 246, 0.1); | ||||
|     } | ||||
|     100% { | ||||
|         transform: scale(1); | ||||
|         box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); | ||||
|         background-color: rgba(59, 130, 246, 0); | ||||
|     } | ||||
| } | ||||
|  | ||||
| .dbml-edit-button-emphasis { | ||||
|     animation: dbml_edit-button-emphasis 0.6s ease-in-out; | ||||
|     animation-iteration-count: 1; | ||||
|     position: relative; | ||||
|     z-index: 10; | ||||
| } | ||||
|   | ||||
| @@ -23,23 +23,25 @@ import { bn, bnMetadata } from './locales/bn'; | ||||
| import { gu, guMetadata } from './locales/gu'; | ||||
| import { vi, viMetadata } from './locales/vi'; | ||||
| import { ar, arMetadata } from './locales/ar'; | ||||
| import { hr, hrMetadata } from './locales/hr'; | ||||
|  | ||||
| export const languages: LanguageMetadata[] = [ | ||||
|     enMetadata, | ||||
|     esMetadata, | ||||
|     frMetadata, | ||||
|     deMetadata, | ||||
|     esMetadata, | ||||
|     ukMetadata, | ||||
|     ruMetadata, | ||||
|     trMetadata, | ||||
|     hrMetadata, | ||||
|     pt_BRMetadata, | ||||
|     hiMetadata, | ||||
|     jaMetadata, | ||||
|     ko_KRMetadata, | ||||
|     pt_BRMetadata, | ||||
|     ukMetadata, | ||||
|     ruMetadata, | ||||
|     zh_CNMetadata, | ||||
|     zh_TWMetadata, | ||||
|     neMetadata, | ||||
|     mrMetadata, | ||||
|     trMetadata, | ||||
|     id_IDMetadata, | ||||
|     teMetadata, | ||||
|     bnMetadata, | ||||
| @@ -70,6 +72,7 @@ const resources = { | ||||
|     gu, | ||||
|     vi, | ||||
|     ar, | ||||
|     hr, | ||||
| }; | ||||
|  | ||||
| i18n.use(LanguageDetector) | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const ar: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'جديد', | ||||
|             browse: 'تصفح', | ||||
|             tables: 'الجداول', | ||||
|             refs: 'المراجع', | ||||
|             areas: 'المناطق', | ||||
|             dependencies: 'التبعيات', | ||||
|             custom_types: 'الأنواع المخصصة', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'ملف', | ||||
|                 new: 'جديد', | ||||
|                 open: 'فتح', | ||||
|             actions: { | ||||
|                 actions: 'الإجراءات', | ||||
|                 new: 'مخطط جديد', | ||||
|                 browse: 'تصفح...', | ||||
|                 save: 'حفظ', | ||||
|                 import: 'استيراد قاعدة بيانات', | ||||
|                 export_sql: 'SQL تصدير', | ||||
|                 export_as: 'تصدير كـ', | ||||
|                 delete_diagram: 'حذف الرسم البياني', | ||||
|                 exit: 'خروج', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'تحرير', | ||||
| @@ -26,7 +34,10 @@ export const ar: LanguageTranslation = { | ||||
|                 hide_sidebar: 'إخفاء الشريط الجانبي', | ||||
|                 hide_cardinality: 'إخفاء الكاردينالية', | ||||
|                 show_cardinality: 'إظهار الكاردينالية', | ||||
|                 hide_field_attributes: 'إخفاء خصائص الحقل', | ||||
|                 show_field_attributes: 'إظهار خصائص الحقل', | ||||
|                 zoom_on_scroll: 'تكبير/تصغير عند التمرير', | ||||
|                 show_views: 'عروض قاعدة البيانات', | ||||
|                 theme: 'المظهر', | ||||
|                 show_dependencies: 'إظهار الاعتمادات', | ||||
|                 hide_dependencies: 'إخفاء الاعتمادات', | ||||
| @@ -70,15 +81,6 @@ export const ar: LanguageTranslation = { | ||||
|             cancel: 'إلغاء', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'مخططات متعددة', | ||||
|             description: | ||||
|                 '{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك', | ||||
|             dont_show_again: 'لا تظهره مجدداً', | ||||
|             change_schema: 'تغيير', | ||||
|             none: 'لا شيء', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'فشل النسخ', | ||||
| @@ -113,14 +115,11 @@ export const ar: LanguageTranslation = { | ||||
|         copied: '!تم النسخ', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: ':المخطط', | ||||
|             filter_by_schema: 'تصفية حسب المخطط', | ||||
|             search_schema: '...بحث في المخطط', | ||||
|             no_schemas_found: '.لم يتم العثور على مخططات', | ||||
|             view_all_options: '...عرض جميع الخيارات', | ||||
|             tables_section: { | ||||
|                 tables: 'الجداول', | ||||
|                 add_table: 'إضافة جدول', | ||||
|                 add_view: 'إضافة عرض', | ||||
|                 filter: 'تصفية', | ||||
|                 collapse: 'طي الكل', | ||||
|                 // TODO: Translate | ||||
| @@ -146,16 +145,22 @@ export const ar: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'خصائص الحقل', | ||||
|                         unique: 'فريد', | ||||
|                         auto_increment: 'زيادة تلقائية', | ||||
|                         comments: 'تعليقات', | ||||
|                         no_comments: 'لا يوجد تعليقات', | ||||
|                         delete_field: 'حذف الحقل', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'الدقة', | ||||
|                         scale: 'النطاق', | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'خصائص الفهرس', | ||||
|                         name: 'الإسم', | ||||
|                         unique: 'فريد', | ||||
|                         index_type: 'نوع الفهرس', | ||||
|                         delete_index: 'حذف الفهرس', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -172,12 +177,15 @@ export const ar: LanguageTranslation = { | ||||
|                     description: 'أنشئ جدولاً للبدء', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'العلاقات', | ||||
|             refs_section: { | ||||
|                 refs: 'المراجع', | ||||
|                 filter: 'تصفية', | ||||
|                 add_relationship: 'إضافة علاقة', | ||||
|                 collapse: 'طي الكل', | ||||
|                 add_relationship: 'إضافة علاقة', | ||||
|                 relationships: 'العلاقات', | ||||
|                 dependencies: 'الاعتمادات', | ||||
|                 relationship: { | ||||
|                     relationship: 'العلاقة', | ||||
|                     primary: 'الجدول الأساسي', | ||||
|                     foreign: 'الجدول المرتبط', | ||||
|                     cardinality: 'الكاردينالية', | ||||
| @@ -187,16 +195,8 @@ export const ar: LanguageTranslation = { | ||||
|                         delete_relationship: 'حذف', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'لا توجد علاقات', | ||||
|                     description: 'إنشئ علاقة لربط الجداول', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'الاعتمادات', | ||||
|                 filter: 'تصفية', | ||||
|                 collapse: 'طي الكل', | ||||
|                 dependency: { | ||||
|                     dependency: 'الاعتماد', | ||||
|                     table: 'الجدول', | ||||
|                     dependent_table: 'عرض الاعتمادات', | ||||
|                     delete_dependency: 'حذف', | ||||
| @@ -206,8 +206,8 @@ export const ar: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'لا توجد اعتمادات', | ||||
|                     description: 'إنشاء اعتماد للبدء', | ||||
|                     title: 'لا توجد علاقات', | ||||
|                     description: 'إنشاء علاقة للبدء', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -251,9 +251,12 @@ export const ar: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -269,6 +272,11 @@ export const ar: LanguageTranslation = { | ||||
|             redo: 'إعادة', | ||||
|             reorder_diagram: 'إعادة ترتيب الرسم البياني', | ||||
|             highlight_overlapping_tables: 'تمييز الجداول المتداخلة', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -315,6 +323,12 @@ export const ar: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'إلغاء', | ||||
|             open: 'فتح', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'فتح', | ||||
|                 duplicate: 'تكرار', | ||||
|                 delete: 'حذف الرسم التخطيطي', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -400,6 +414,13 @@ export const ar: LanguageTranslation = { | ||||
|             cancel: 'إلغاء', | ||||
|             confirm: 'تغيير', | ||||
|         }, | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'إنشاء مخطط جديد', | ||||
|             description: | ||||
|                 'لا توجد مخططات حتى الآن. قم بإنشاء أول مخطط لتنظيم جداولك.', | ||||
|             create: 'إنشاء', | ||||
|             cancel: 'إلغاء', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: '!ساعدنا على التحسن', | ||||
| @@ -453,6 +474,7 @@ export const ar: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'جدول جديد', | ||||
|             new_view: 'عرض جديد', | ||||
|             new_relationship: 'علاقة جديدة', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -474,6 +496,8 @@ export const ar: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'اللغة', | ||||
|         }, | ||||
|         on: 'تشغيل', | ||||
|         off: 'إيقاف', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const bn: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'নতুন', | ||||
|             browse: 'ব্রাউজ', | ||||
|             tables: 'টেবিল', | ||||
|             refs: 'রেফস', | ||||
|             areas: 'এলাকা', | ||||
|             dependencies: 'নির্ভরতা', | ||||
|             custom_types: 'কাস্টম টাইপ', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'ফাইল', | ||||
|                 new: 'নতুন', | ||||
|                 open: 'খুলুন', | ||||
|             actions: { | ||||
|                 actions: 'কার্য', | ||||
|                 new: 'নতুন ডায়াগ্রাম', | ||||
|                 browse: 'ব্রাউজ করুন...', | ||||
|                 save: 'সংরক্ষণ করুন', | ||||
|                 import: 'ডাটাবেস আমদানি করুন', | ||||
|                 export_sql: 'SQL রপ্তানি করুন', | ||||
|                 export_as: 'রূপে রপ্তানি করুন', | ||||
|                 delete_diagram: 'ডায়াগ্রাম মুছুন', | ||||
|                 exit: 'প্রস্থান করুন', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'সম্পাদনা', | ||||
| @@ -26,7 +34,10 @@ export const bn: LanguageTranslation = { | ||||
|                 hide_sidebar: 'সাইডবার লুকান', | ||||
|                 hide_cardinality: 'কার্ডিনালিটি লুকান', | ||||
|                 show_cardinality: 'কার্ডিনালিটি দেখান', | ||||
|                 hide_field_attributes: 'ফিল্ড অ্যাট্রিবিউট লুকান', | ||||
|                 show_field_attributes: 'ফিল্ড অ্যাট্রিবিউট দেখান', | ||||
|                 zoom_on_scroll: 'স্ক্রলে জুম করুন', | ||||
|                 show_views: 'ডাটাবেস ভিউ', | ||||
|                 theme: 'থিম', | ||||
|                 show_dependencies: 'নির্ভরতাগুলি দেখান', | ||||
|                 hide_dependencies: 'নির্ভরতাগুলি লুকান', | ||||
| @@ -71,15 +82,6 @@ export const bn: LanguageTranslation = { | ||||
|             cancel: 'বাতিল করুন', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'বহু স্কিমা', | ||||
|             description: | ||||
|                 '{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।', | ||||
|             dont_show_again: 'পুনরায় দেখাবেন না', | ||||
|             change_schema: 'পরিবর্তন করুন', | ||||
|             none: 'কিছুই না', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'কপি ব্যর্থ হয়েছে', | ||||
| @@ -114,14 +116,11 @@ export const bn: LanguageTranslation = { | ||||
|         copied: 'অনুলিপি সম্পন্ন!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'স্কিমা:', | ||||
|             filter_by_schema: 'স্কিমা দ্বারা ফিল্টার করুন', | ||||
|             search_schema: 'স্কিমা খুঁজুন...', | ||||
|             no_schemas_found: 'কোনো স্কিমা পাওয়া যায়নি।', | ||||
|             view_all_options: 'সমস্ত বিকল্প দেখুন...', | ||||
|             tables_section: { | ||||
|                 tables: 'টেবিল', | ||||
|                 add_table: 'টেবিল যোগ করুন', | ||||
|                 add_view: 'ভিউ যোগ করুন', | ||||
|                 filter: 'ফিল্টার', | ||||
|                 collapse: 'সব ভাঁজ করুন', | ||||
|                 // TODO: Translate | ||||
| @@ -147,16 +146,23 @@ export const bn: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'ফিল্ড কর্ম', | ||||
|                         unique: 'অদ্বিতীয়', | ||||
|                         auto_increment: 'স্বয়ংক্রিয় বৃদ্ধি', | ||||
|                         comments: 'মন্তব্য', | ||||
|                         no_comments: 'কোনো মন্তব্য নেই', | ||||
|                         delete_field: 'ফিল্ড মুছুন', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'নির্ভুলতা', | ||||
|                         scale: 'স্কেল', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'ইনডেক্স কর্ম', | ||||
|                         name: 'নাম', | ||||
|                         unique: 'অদ্বিতীয়', | ||||
|                         index_type: 'ইনডেক্স ধরন', | ||||
|                         delete_index: 'ইনডেক্স মুছুন', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -173,14 +179,17 @@ export const bn: LanguageTranslation = { | ||||
|                     description: 'শুরু করতে একটি টেবিল তৈরি করুন', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'সম্পর্ক', | ||||
|             refs_section: { | ||||
|                 refs: 'রেফস', | ||||
|                 filter: 'ফিল্টার', | ||||
|                 add_relationship: 'সম্পর্ক যোগ করুন', | ||||
|                 collapse: 'সব ভাঁজ করুন', | ||||
|                 add_relationship: 'সম্পর্ক যোগ করুন', | ||||
|                 relationships: 'সম্পর্ক', | ||||
|                 dependencies: 'নির্ভরতাগুলি', | ||||
|                 relationship: { | ||||
|                     relationship: 'সম্পর্ক', | ||||
|                     primary: 'প্রাথমিক টেবিল', | ||||
|                     foreign: 'বিদেশি টেবিল', | ||||
|                     foreign: 'রেফারেন্স করা টেবিল', | ||||
|                     cardinality: 'কার্ডিনালিটি', | ||||
|                     delete_relationship: 'মুছুন', | ||||
|                     relationship_actions: { | ||||
| @@ -188,27 +197,19 @@ export const bn: LanguageTranslation = { | ||||
|                         delete_relationship: 'মুছুন', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'কোনো সম্পর্ক নেই', | ||||
|                     description: 'টেবিল সংযোগ করতে একটি সম্পর্ক তৈরি করুন', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'নির্ভরতাগুলি', | ||||
|                 filter: 'ফিল্টার', | ||||
|                 collapse: 'ভাঁজ করুন', | ||||
|                 dependency: { | ||||
|                     dependency: 'নির্ভরতা', | ||||
|                     table: 'টেবিল', | ||||
|                     dependent_table: 'নির্ভরশীল টেবিল', | ||||
|                     delete_dependency: 'নির্ভরতা মুছুন', | ||||
|                     dependent_table: 'নির্ভরশীল ভিউ', | ||||
|                     delete_dependency: 'মুছুন', | ||||
|                     dependency_actions: { | ||||
|                         title: 'কর্ম', | ||||
|                         delete_dependency: 'নির্ভরতা মুছুন', | ||||
|                         delete_dependency: 'মুছুন', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'কোনো নির্ভরতাগুলি নেই', | ||||
|                     description: 'এই অংশে কোনো নির্ভরতা উপলব্ধ নেই।', | ||||
|                     title: 'কোনো সম্পর্ক নেই', | ||||
|                     description: 'শুরু করতে একটি সম্পর্ক তৈরি করুন', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -251,9 +252,12 @@ export const bn: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -269,6 +273,12 @@ export const bn: LanguageTranslation = { | ||||
|             redo: 'পুনরায় করুন', | ||||
|             reorder_diagram: 'ডায়াগ্রাম পুনর্বিন্যাস করুন', | ||||
|             highlight_overlapping_tables: 'ওভারল্যাপিং টেবিল হাইলাইট করুন', | ||||
|  | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -315,6 +325,12 @@ export const bn: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'বাতিল করুন', | ||||
|             open: 'খুলুন', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'খুলুন', | ||||
|                 duplicate: 'ডুপ্লিকেট', | ||||
|                 delete: 'ডায়াগ্রাম মুছুন', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -400,6 +416,13 @@ export const bn: LanguageTranslation = { | ||||
|             cancel: 'বাতিল করুন', | ||||
|             confirm: 'পরিবর্তন করুন', | ||||
|         }, | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'নতুন স্কিমা তৈরি করুন', | ||||
|             description: | ||||
|                 'এখনও কোনো স্কিমা নেই। আপনার টেবিলগুলি সংগঠিত করতে আপনার প্রথম স্কিমা তৈরি করুন।', | ||||
|             create: 'তৈরি করুন', | ||||
|             cancel: 'বাতিল করুন', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: 'আমাদের উন্নত করতে সাহায্য করুন!', | ||||
| @@ -456,6 +479,7 @@ export const bn: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'নতুন টেবিল', | ||||
|             new_view: 'নতুন ভিউ', | ||||
|             new_relationship: 'নতুন সম্পর্ক', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -477,6 +501,9 @@ export const bn: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'ভাষা পরিবর্তন করুন', | ||||
|         }, | ||||
|  | ||||
|         on: 'চালু', | ||||
|         off: 'বন্ধ', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const de: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Neu', | ||||
|             browse: 'Durchsuchen', | ||||
|             tables: 'Tabellen', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Bereiche', | ||||
|             dependencies: 'Abhängigkeiten', | ||||
|             custom_types: 'Benutzerdefinierte Typen', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Datei', | ||||
|                 new: 'Neu', | ||||
|                 open: 'Öffnen', | ||||
|             actions: { | ||||
|                 actions: 'Aktionen', | ||||
|                 new: 'Neues Diagramm', | ||||
|                 browse: 'Durchsuchen...', | ||||
|                 save: 'Speichern', | ||||
|                 import: 'Datenbank importieren', | ||||
|                 export_sql: 'SQL exportieren', | ||||
|                 export_as: 'Exportieren als', | ||||
|                 delete_diagram: 'Diagramm löschen', | ||||
|                 exit: 'Beenden', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Bearbeiten', | ||||
| @@ -26,7 +34,10 @@ export const de: LanguageTranslation = { | ||||
|                 hide_sidebar: 'Seitenleiste ausblenden', | ||||
|                 hide_cardinality: 'Kardinalität ausblenden', | ||||
|                 show_cardinality: 'Kardinalität anzeigen', | ||||
|                 hide_field_attributes: 'Feldattribute ausblenden', | ||||
|                 show_field_attributes: 'Feldattribute anzeigen', | ||||
|                 zoom_on_scroll: 'Zoom beim Scrollen', | ||||
|                 show_views: 'Datenbankansichten', | ||||
|                 theme: 'Stil', | ||||
|                 show_dependencies: 'Abhängigkeiten anzeigen', | ||||
|                 hide_dependencies: 'Abhängigkeiten ausblenden', | ||||
| @@ -71,15 +82,6 @@ export const de: LanguageTranslation = { | ||||
|             cancel: 'Abbrechen', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Mehrere Schemas', | ||||
|             description: | ||||
|                 '{{schemasCount}} Schemas in diesem Diagramm. Derzeit angezeigt: {{formattedSchemas}}.', | ||||
|             dont_show_again: 'Nicht erneut anzeigen', | ||||
|             change_schema: 'Schema ändern', | ||||
|             none: 'Keine', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Kopieren fehlgeschlagen', | ||||
| @@ -115,14 +117,11 @@ export const de: LanguageTranslation = { | ||||
|         copied: 'Kopiert!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Schema:', | ||||
|             filter_by_schema: 'Nach Schema filtern', | ||||
|             search_schema: 'Schema suchen...', | ||||
|             no_schemas_found: 'Keine Schemas gefunden.', | ||||
|             view_all_options: 'Alle Optionen anzeigen...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tabellen', | ||||
|                 add_table: 'Tabelle hinzufügen', | ||||
|                 add_view: 'Ansicht hinzufügen', | ||||
|                 filter: 'Filter', | ||||
|                 collapse: 'Alle einklappen', | ||||
|                 // TODO: Translate | ||||
| @@ -148,16 +147,23 @@ export const de: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Feldattribute', | ||||
|                         unique: 'Eindeutig', | ||||
|                         auto_increment: 'Automatisch hochzählen', | ||||
|                         comments: 'Kommentare', | ||||
|                         no_comments: 'Keine Kommentare', | ||||
|                         delete_field: 'Feld löschen', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'Präzision', | ||||
|                         scale: 'Skalierung', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'Indexattribute', | ||||
|                         name: 'Name', | ||||
|                         unique: 'Eindeutig', | ||||
|                         index_type: 'Indextyp', | ||||
|                         delete_index: 'Index löschen', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -174,32 +180,26 @@ export const de: LanguageTranslation = { | ||||
|                     description: 'Erstellen Sie eine Tabelle, um zu beginnen', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Beziehungen', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Filter', | ||||
|                 add_relationship: 'Beziehung hinzufügen', | ||||
|                 collapse: 'Alle einklappen', | ||||
|                 add_relationship: 'Beziehung hinzufügen', | ||||
|                 relationships: 'Beziehungen', | ||||
|                 dependencies: 'Abhängigkeiten', | ||||
|                 relationship: { | ||||
|                     relationship: 'Beziehung', | ||||
|                     primary: 'Primäre Tabelle', | ||||
|                     foreign: 'Referenzierte Tabelle', | ||||
|                     cardinality: 'Kardinalität', | ||||
|                     delete_relationship: 'Beziehung löschen', | ||||
|                     delete_relationship: 'Löschen', | ||||
|                     relationship_actions: { | ||||
|                         title: 'Aktionen', | ||||
|                         delete_relationship: 'Beziehung löschen', | ||||
|                         delete_relationship: 'Löschen', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Keine Beziehungen', | ||||
|                     description: | ||||
|                         'Erstellen Sie eine Beziehung, um Tabellen zu verbinden', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Abhängigkeiten', | ||||
|                 filter: 'Filter', | ||||
|                 collapse: 'Alle einklappen', | ||||
|                 dependency: { | ||||
|                     dependency: 'Abhängigkeit', | ||||
|                     table: 'Tabelle', | ||||
|                     dependent_table: 'Abhängige Ansicht', | ||||
|                     delete_dependency: 'Löschen', | ||||
| @@ -209,8 +209,8 @@ export const de: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Keine Abhängigkeiten', | ||||
|                     description: 'Erstellen Sie eine Ansicht, um zu beginnen', | ||||
|                     title: 'Keine Beziehungen', | ||||
|                     description: 'Erstellen Sie eine Beziehung, um zu beginnen', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -253,9 +253,12 @@ export const de: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -270,7 +273,14 @@ export const de: LanguageTranslation = { | ||||
|             undo: 'Rückgängig', | ||||
|             redo: 'Wiederholen', | ||||
|             reorder_diagram: 'Diagramm neu anordnen', | ||||
|  | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: 'Überlappende Tabellen hervorheben', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -318,6 +328,12 @@ export const de: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Abbrechen', | ||||
|             open: 'Öffnen', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Öffnen', | ||||
|                 duplicate: 'Duplizieren', | ||||
|                 delete: 'Diagramm löschen', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -403,6 +419,13 @@ export const de: LanguageTranslation = { | ||||
|             cancel: 'Abbrechen', | ||||
|             confirm: 'Ändern', | ||||
|         }, | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'Neues Schema erstellen', | ||||
|             description: | ||||
|                 'Es existieren noch keine Schemas. Erstellen Sie Ihr erstes Schema, um Ihre Tabellen zu organisieren.', | ||||
|             create: 'Erstellen', | ||||
|             cancel: 'Abbrechen', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: 'Hilf uns, uns zu verbessern!', | ||||
| @@ -459,6 +482,7 @@ export const de: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Neue Tabelle', | ||||
|             new_view: 'Neue Ansicht', | ||||
|             new_relationship: 'Neue Beziehung', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -481,6 +505,9 @@ export const de: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Sprache', | ||||
|         }, | ||||
|  | ||||
|         on: 'Ein', | ||||
|         off: 'Aus', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata } from '../types'; | ||||
|  | ||||
| export const en = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'New', | ||||
|             browse: 'Browse', | ||||
|             tables: 'Tables', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Areas', | ||||
|             dependencies: 'Dependencies', | ||||
|             custom_types: 'Custom Types', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'File', | ||||
|                 new: 'New', | ||||
|                 open: 'Open', | ||||
|             actions: { | ||||
|                 actions: 'Actions', | ||||
|                 new: 'New Diagram', | ||||
|                 browse: 'Browse...', | ||||
|                 save: 'Save', | ||||
|                 import: 'Import', | ||||
|                 export_sql: 'Export SQL', | ||||
|                 export_as: 'Export as', | ||||
|                 delete_diagram: 'Delete Diagram', | ||||
|                 exit: 'Exit', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Edit', | ||||
| @@ -26,7 +34,10 @@ export const en = { | ||||
|                 hide_sidebar: 'Hide Sidebar', | ||||
|                 hide_cardinality: 'Hide Cardinality', | ||||
|                 show_cardinality: 'Show Cardinality', | ||||
|                 hide_field_attributes: 'Hide Field Attributes', | ||||
|                 show_field_attributes: 'Show Field Attributes', | ||||
|                 zoom_on_scroll: 'Zoom on Scroll', | ||||
|                 show_views: 'Database Views', | ||||
|                 theme: 'Theme', | ||||
|                 show_dependencies: 'Show Dependencies', | ||||
|                 hide_dependencies: 'Hide Dependencies', | ||||
| @@ -69,15 +80,6 @@ export const en = { | ||||
|             cancel: 'Cancel', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Multiple Schemas', | ||||
|             description: | ||||
|                 '{{schemasCount}} schemas in this diagram. Currently displaying: {{formattedSchemas}}.', | ||||
|             dont_show_again: "Don't show again", | ||||
|             change_schema: 'Change', | ||||
|             none: 'none', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Copy failed', | ||||
| @@ -112,14 +114,11 @@ export const en = { | ||||
|         copied: 'Copied!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Schema:', | ||||
|             filter_by_schema: 'Filter by schema', | ||||
|             search_schema: 'Search schema...', | ||||
|             no_schemas_found: 'No schemas found.', | ||||
|             view_all_options: 'View all Options...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tables', | ||||
|                 add_table: 'Add Table', | ||||
|                 add_view: 'Add View', | ||||
|                 filter: 'Filter', | ||||
|                 collapse: 'Collapse All', | ||||
|                 clear: 'Clear Filter', | ||||
| @@ -143,15 +142,21 @@ export const en = { | ||||
|                     field_actions: { | ||||
|                         title: 'Field Attributes', | ||||
|                         unique: 'Unique', | ||||
|                         auto_increment: 'Auto Increment', | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'Precision', | ||||
|                         scale: 'Scale', | ||||
|                         comments: 'Comments', | ||||
|                         no_comments: 'No comments', | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         delete_field: 'Delete Field', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'Index Attributes', | ||||
|                         name: 'Name', | ||||
|                         unique: 'Unique', | ||||
|                         index_type: 'Index Type', | ||||
|                         delete_index: 'Delete Index', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -168,12 +173,15 @@ export const en = { | ||||
|                     description: 'Create a table to get started', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Relationships', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Filter', | ||||
|                 add_relationship: 'Add Relationship', | ||||
|                 collapse: 'Collapse All', | ||||
|                 add_relationship: 'Add Relationship', | ||||
|                 relationships: 'Relationships', | ||||
|                 dependencies: 'Dependencies', | ||||
|                 relationship: { | ||||
|                     relationship: 'Relationship', | ||||
|                     primary: 'Primary Table', | ||||
|                     foreign: 'Referenced Table', | ||||
|                     cardinality: 'Cardinality', | ||||
| @@ -183,16 +191,8 @@ export const en = { | ||||
|                         delete_relationship: 'Delete', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'No relationships', | ||||
|                     description: 'Create a relationship to connect tables', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Dependencies', | ||||
|                 filter: 'Filter', | ||||
|                 collapse: 'Collapse All', | ||||
|                 dependency: { | ||||
|                     dependency: 'Dependency', | ||||
|                     table: 'Table', | ||||
|                     dependent_table: 'Dependent View', | ||||
|                     delete_dependency: 'Delete', | ||||
| @@ -202,8 +202,8 @@ export const en = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'No dependencies', | ||||
|                     description: 'Create a view to get started', | ||||
|                     title: 'No relationships', | ||||
|                     description: 'Create a relationship to get started', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -245,8 +245,11 @@ export const en = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
| @@ -263,6 +266,10 @@ export const en = { | ||||
|             redo: 'Redo', | ||||
|             reorder_diagram: 'Reorder Diagram', | ||||
|             highlight_overlapping_tables: 'Highlight Overlapping Tables', | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -309,6 +316,12 @@ export const en = { | ||||
|             }, | ||||
|             cancel: 'Cancel', | ||||
|             open: 'Open', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Open', | ||||
|                 duplicate: 'Duplicate', | ||||
|                 delete: 'Delete Diagram', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -394,6 +407,14 @@ export const en = { | ||||
|             confirm: 'Change', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'Create New Schema', | ||||
|             description: | ||||
|                 'No schemas exist yet. Create your first schema to organize your tables.', | ||||
|             create: 'Create', | ||||
|             cancel: 'Cancel', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: 'Help us improve!', | ||||
|             description: | ||||
| @@ -448,6 +469,7 @@ export const en = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'New Table', | ||||
|             new_view: 'New View', | ||||
|             new_relationship: 'New Relationship', | ||||
|             new_area: 'New Area', | ||||
|         }, | ||||
| @@ -468,6 +490,9 @@ export const en = { | ||||
|         language_select: { | ||||
|             change_language: 'Language', | ||||
|         }, | ||||
|  | ||||
|         on: 'On', | ||||
|         off: 'Off', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const es: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Nuevo', | ||||
|             browse: 'Examinar', | ||||
|             tables: 'Tablas', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Áreas', | ||||
|             dependencies: 'Dependencias', | ||||
|             custom_types: 'Tipos Personalizados', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Archivo', | ||||
|                 new: 'Nuevo', | ||||
|                 open: 'Abrir', | ||||
|             actions: { | ||||
|                 actions: 'Acciones', | ||||
|                 new: 'Nuevo Diagrama', | ||||
|                 browse: 'Examinar...', | ||||
|                 save: 'Guardar', | ||||
|                 import: 'Importar Base de Datos', | ||||
|                 export_sql: 'Exportar SQL', | ||||
|                 export_as: 'Exportar como', | ||||
|                 delete_diagram: 'Eliminar Diagrama', | ||||
|                 exit: 'Salir', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Editar', | ||||
| @@ -24,9 +32,12 @@ export const es: LanguageTranslation = { | ||||
|                 view: 'Ver', | ||||
|                 hide_cardinality: 'Ocultar Cardinalidad', | ||||
|                 show_cardinality: 'Mostrar Cardinalidad', | ||||
|                 show_field_attributes: 'Mostrar Atributos de Campo', | ||||
|                 hide_field_attributes: 'Ocultar Atributos de Campo', | ||||
|                 show_sidebar: 'Mostrar Barra Lateral', | ||||
|                 hide_sidebar: 'Ocultar Barra Lateral', | ||||
|                 zoom_on_scroll: 'Zoom al Desplazarse', | ||||
|                 show_views: 'Vistas de Base de Datos', | ||||
|                 theme: 'Tema', | ||||
|                 show_dependencies: 'Mostrar dependencias', | ||||
|                 hide_dependencies: 'Ocultar dependencias', | ||||
| @@ -104,14 +115,11 @@ export const es: LanguageTranslation = { | ||||
|         copied: 'Copied!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Esquema:', | ||||
|             filter_by_schema: 'Filtrar por esquema', | ||||
|             search_schema: 'Buscar esquema...', | ||||
|             no_schemas_found: 'No se encontraron esquemas.', | ||||
|             view_all_options: 'Ver todas las opciones...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tablas', | ||||
|                 add_table: 'Agregar Tabla', | ||||
|                 add_view: 'Agregar Vista', | ||||
|                 filter: 'Filtrar', | ||||
|                 collapse: 'Colapsar Todo', | ||||
|                 // TODO: Translate | ||||
| @@ -137,16 +145,23 @@ export const es: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Atributos del Campo', | ||||
|                         unique: 'Único', | ||||
|                         auto_increment: 'Autoincremento', | ||||
|                         comments: 'Comentarios', | ||||
|                         no_comments: 'Sin comentarios', | ||||
|                         delete_field: 'Eliminar Campo', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'Precisión', | ||||
|                         scale: 'Escala', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'Atributos del Índice', | ||||
|                         name: 'Nombre', | ||||
|                         unique: 'Único', | ||||
|                         index_type: 'Tipo de Índice', | ||||
|                         delete_index: 'Eliminar Índice', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -163,14 +178,17 @@ export const es: LanguageTranslation = { | ||||
|                     description: 'Crea una tabla para comenzar', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Relaciones', | ||||
|                 add_relationship: 'Agregar Relación', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Filtrar', | ||||
|                 collapse: 'Colapsar Todo', | ||||
|                 add_relationship: 'Agregar Relación', | ||||
|                 relationships: 'Relaciones', | ||||
|                 dependencies: 'Dependencias', | ||||
|                 relationship: { | ||||
|                     primary: 'Primaria', | ||||
|                     foreign: 'Foránea', | ||||
|                     relationship: 'Relación', | ||||
|                     primary: 'Tabla Primaria', | ||||
|                     foreign: 'Tabla Referenciada', | ||||
|                     cardinality: 'Cardinalidad', | ||||
|                     delete_relationship: 'Eliminar', | ||||
|                     relationship_actions: { | ||||
| @@ -178,18 +196,10 @@ export const es: LanguageTranslation = { | ||||
|                         delete_relationship: 'Eliminar', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'No hay relaciones', | ||||
|                     description: 'Crea una relación para conectar tablas', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Dependencias', | ||||
|                 filter: 'Filtro', | ||||
|                 collapse: 'Colapsar todo', | ||||
|                 dependency: { | ||||
|                     dependency: 'Dependencia', | ||||
|                     table: 'Tabla', | ||||
|                     dependent_table: 'Vista dependiente', | ||||
|                     dependent_table: 'Vista Dependiente', | ||||
|                     delete_dependency: 'Eliminar', | ||||
|                     dependency_actions: { | ||||
|                         title: 'Acciones', | ||||
| @@ -197,8 +207,8 @@ export const es: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Sin dependencias', | ||||
|                     description: 'Crea una vista para comenzar', | ||||
|                     title: 'Sin relaciones', | ||||
|                     description: 'Crea una relación para comenzar', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -241,9 +251,12 @@ export const es: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -258,7 +271,13 @@ export const es: LanguageTranslation = { | ||||
|             undo: 'Deshacer', | ||||
|             redo: 'Rehacer', | ||||
|             reorder_diagram: 'Reordenar Diagrama', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: 'Resaltar tablas superpuestas', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -307,6 +326,12 @@ export const es: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Cancelar', | ||||
|             open: 'Abrir', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Abrir', | ||||
|                 duplicate: 'Duplicar', | ||||
|                 delete: 'Eliminar Diagrama', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -392,6 +417,13 @@ export const es: LanguageTranslation = { | ||||
|             cancel: 'Cancelar', | ||||
|             confirm: 'Cambiar', | ||||
|         }, | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'Crear Nuevo Esquema', | ||||
|             description: | ||||
|                 'Aún no existen esquemas. Crea tu primer esquema para organizar tus tablas.', | ||||
|             create: 'Crear', | ||||
|             cancel: 'Cancelar', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: '¡Ayúdanos a mejorar!', | ||||
| @@ -401,14 +433,6 @@ export const es: LanguageTranslation = { | ||||
|             confirm: '¡Claro!', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Múltiples Esquemas', | ||||
|             description: | ||||
|                 '{{schemasCount}} esquemas en este diagrama. Actualmente mostrando: {{formattedSchemas}}.', | ||||
|             dont_show_again: 'No mostrar de nuevo', | ||||
|             change_schema: 'Cambiar', | ||||
|             none: 'nada', | ||||
|         }, | ||||
|         // TODO: Translate | ||||
|         export_diagram_dialog: { | ||||
|             title: 'Export Diagram', | ||||
| @@ -457,6 +481,7 @@ export const es: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Nueva Tabla', | ||||
|             new_view: 'Nueva Vista', | ||||
|             new_relationship: 'Nueva Relación', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -479,6 +504,9 @@ export const es: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Idioma', | ||||
|         }, | ||||
|  | ||||
|         on: 'Encendido', | ||||
|         off: 'Apagado', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const fr: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Nouveau', | ||||
|             browse: 'Parcourir', | ||||
|             tables: 'Tables', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Zones', | ||||
|             dependencies: 'Dépendances', | ||||
|             custom_types: 'Types Personnalisés', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Fichier', | ||||
|                 new: 'Nouveau', | ||||
|                 open: 'Ouvrir', | ||||
|             actions: { | ||||
|                 actions: 'Actions', | ||||
|                 new: 'Nouveau Diagramme', | ||||
|                 browse: 'Parcourir...', | ||||
|                 save: 'Enregistrer', | ||||
|                 import: 'Importer Base de Données', | ||||
|                 export_sql: 'Exporter SQL', | ||||
|                 export_as: 'Exporter en tant que', | ||||
|                 delete_diagram: 'Supprimer le Diagramme', | ||||
|                 exit: 'Quitter', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Édition', | ||||
| @@ -26,7 +34,10 @@ export const fr: LanguageTranslation = { | ||||
|                 hide_sidebar: 'Cacher la Barre Latérale', | ||||
|                 hide_cardinality: 'Cacher la Cardinalité', | ||||
|                 show_cardinality: 'Afficher la Cardinalité', | ||||
|                 hide_field_attributes: 'Masquer les Attributs de Champ', | ||||
|                 show_field_attributes: 'Afficher les Attributs de Champ', | ||||
|                 zoom_on_scroll: 'Zoom sur le Défilement', | ||||
|                 show_views: 'Vues de Base de Données', | ||||
|                 theme: 'Thème', | ||||
|                 show_dependencies: 'Afficher les Dépendances', | ||||
|                 hide_dependencies: 'Masquer les Dépendances', | ||||
| @@ -103,14 +114,11 @@ export const fr: LanguageTranslation = { | ||||
|         copied: 'Copié !', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Schéma:', | ||||
|             filter_by_schema: 'Filtrer par schéma', | ||||
|             search_schema: 'Rechercher un schéma...', | ||||
|             no_schemas_found: 'Aucun schéma trouvé.', | ||||
|             view_all_options: 'Voir toutes les Options...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tables', | ||||
|                 add_table: 'Ajouter une Table', | ||||
|                 add_view: 'Ajouter une Vue', | ||||
|                 filter: 'Filtrer', | ||||
|                 collapse: 'Réduire Tout', | ||||
|                 clear: 'Effacer le Filtre', | ||||
| @@ -135,16 +143,23 @@ export const fr: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Attributs du Champ', | ||||
|                         unique: 'Unique', | ||||
|                         auto_increment: 'Auto-incrément', | ||||
|                         comments: 'Commentaires', | ||||
|                         no_comments: 'Pas de commentaires', | ||||
|                         delete_field: 'Supprimer le Champ', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'Précision', | ||||
|                         scale: 'Échelle', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: "Attributs de l'Index", | ||||
|                         name: 'Nom', | ||||
|                         unique: 'Unique', | ||||
|                         index_type: "Type d'index", | ||||
|                         delete_index: "Supprimer l'Index", | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -161,12 +176,15 @@ export const fr: LanguageTranslation = { | ||||
|                     description: 'Créez une table pour commencer', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Relations', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Filtrer', | ||||
|                 add_relationship: 'Ajouter une Relation', | ||||
|                 collapse: 'Réduire Tout', | ||||
|                 add_relationship: 'Ajouter une Relation', | ||||
|                 relationships: 'Relations', | ||||
|                 dependencies: 'Dépendances', | ||||
|                 relationship: { | ||||
|                     relationship: 'Relation', | ||||
|                     primary: 'Table Principale', | ||||
|                     foreign: 'Table Référencée', | ||||
|                     cardinality: 'Cardinalité', | ||||
| @@ -176,16 +194,8 @@ export const fr: LanguageTranslation = { | ||||
|                         delete_relationship: 'Supprimer', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Aucune relation', | ||||
|                     description: 'Créez une relation pour connecter les tables', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Dépendances', | ||||
|                 filter: 'Filtrer', | ||||
|                 collapse: 'Réduire Tout', | ||||
|                 dependency: { | ||||
|                     dependency: 'Dépendance', | ||||
|                     table: 'Table', | ||||
|                     dependent_table: 'Vue Dépendante', | ||||
|                     delete_dependency: 'Supprimer', | ||||
| @@ -195,8 +205,8 @@ export const fr: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Aucune dépendance', | ||||
|                     description: 'Créez une vue pour commencer', | ||||
|                     title: 'Aucune relation', | ||||
|                     description: 'Créez une relation pour commencer', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -239,9 +249,12 @@ export const fr: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -256,7 +269,13 @@ export const fr: LanguageTranslation = { | ||||
|             undo: 'Annuler', | ||||
|             redo: 'Rétablir', | ||||
|             reorder_diagram: 'Réorganiser le Diagramme', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: 'Surligner les tables chevauchées', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -304,6 +323,12 @@ export const fr: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Annuler', | ||||
|             open: 'Ouvrir', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Ouvrir', | ||||
|                 duplicate: 'Dupliquer', | ||||
|                 delete: 'Supprimer le diagramme', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -341,15 +366,6 @@ export const fr: LanguageTranslation = { | ||||
|             transparent_description: 'Remove background color from image.', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Schémas Multiples', | ||||
|             description: | ||||
|                 '{{schemasCount}} schémas dans ce diagramme. Actuellement affiché(s) : {{formattedSchemas}}.', | ||||
|             dont_show_again: 'Ne plus afficher', | ||||
|             change_schema: 'Changer', | ||||
|             none: 'Aucun', | ||||
|         }, | ||||
|  | ||||
|         new_table_schema_dialog: { | ||||
|             title: 'Sélectionner un Schéma', | ||||
|             description: | ||||
| @@ -372,6 +388,13 @@ export const fr: LanguageTranslation = { | ||||
|             cancel: 'Annuler', | ||||
|             confirm: 'Modifier', | ||||
|         }, | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'Créer un Nouveau Schéma', | ||||
|             description: | ||||
|                 "Aucun schéma n'existe encore. Créez votre premier schéma pour organiser vos tables.", | ||||
|             create: 'Créer', | ||||
|             cancel: 'Annuler', | ||||
|         }, | ||||
|  | ||||
|         create_relationship_dialog: { | ||||
|             title: 'Créer une Relation', | ||||
| @@ -454,6 +477,7 @@ export const fr: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Nouvelle Table', | ||||
|             new_view: 'Nouvelle Vue', | ||||
|             new_relationship: 'Nouvelle Relation', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -476,6 +500,9 @@ export const fr: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Langue', | ||||
|         }, | ||||
|  | ||||
|         on: 'Activé', | ||||
|         off: 'Désactivé', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const gu: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'નવું', | ||||
|             browse: 'બ્રાઉજ', | ||||
|             tables: 'ટેબલો', | ||||
|             refs: 'રેફ્સ', | ||||
|             areas: 'ક્ષેત્રો', | ||||
|             dependencies: 'નિર્ભરતાઓ', | ||||
|             custom_types: 'કસ્ટમ ટાઇપ', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'ફાઇલ', | ||||
|                 new: 'નવું', | ||||
|                 open: 'ખોલો', | ||||
|             actions: { | ||||
|                 actions: 'ક્રિયાઓ', | ||||
|                 new: 'નવું ડાયાગ્રામ', | ||||
|                 browse: 'બ્રાઉજ કરો...', | ||||
|                 save: 'સાચવો', | ||||
|                 import: 'ડેટાબેસ આયાત કરો', | ||||
|                 export_sql: 'SQL નિકાસ કરો', | ||||
|                 export_as: 'રૂપે નિકાસ કરો', | ||||
|                 delete_diagram: 'ડાયાગ્રામ કાઢી નાખો', | ||||
|                 exit: 'બહાર જાઓ', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'ફેરફાર', | ||||
| @@ -26,7 +34,10 @@ export const gu: LanguageTranslation = { | ||||
|                 hide_sidebar: 'સાઇડબાર છુપાવો', | ||||
|                 hide_cardinality: 'કાર્ડિનાલિટી છુપાવો', | ||||
|                 show_cardinality: 'કાર્ડિનાલિટી બતાવો', | ||||
|                 hide_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ છુપાવો', | ||||
|                 show_field_attributes: 'ફીલ્ડ અટ્રિબ્યુટ્સ બતાવો', | ||||
|                 zoom_on_scroll: 'સ્ક્રોલ પર ઝૂમ કરો', | ||||
|                 show_views: 'ડેટાબેઝ વ્યૂઝ', | ||||
|                 theme: 'થિમ', | ||||
|                 show_dependencies: 'નિર્ભરતાઓ બતાવો', | ||||
|                 hide_dependencies: 'નિર્ભરતાઓ છુપાવો', | ||||
| @@ -71,15 +82,6 @@ export const gu: LanguageTranslation = { | ||||
|             cancel: 'રદ કરો', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'કઈંક વધારે સ્કીમા', | ||||
|             description: | ||||
|                 '{{schemasCount}} સ્કીમા આ ડાયાગ્રામમાં છે. હાલમાં દર્શાવેલ છે: {{formattedSchemas}}.', | ||||
|             dont_show_again: 'ફરીથી ન બતાવો', | ||||
|             change_schema: 'બદલો', | ||||
|             none: 'કઈ નહીં', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'નકલ નિષ્ફળ', | ||||
| @@ -114,14 +116,11 @@ export const gu: LanguageTranslation = { | ||||
|         copied: 'નકલ થયું!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'સ્કીમા:', | ||||
|             filter_by_schema: 'સ્કીમા દ્વારા ફિલ્ટર કરો', | ||||
|             search_schema: 'સ્કીમા શોધો...', | ||||
|             no_schemas_found: 'કોઈ સ્કીમા મળ્યા નથી.', | ||||
|             view_all_options: 'બધા વિકલ્પો જુઓ...', | ||||
|             tables_section: { | ||||
|                 tables: 'ટેબલ્સ', | ||||
|                 add_table: 'ટેબલ ઉમેરો', | ||||
|                 add_view: 'વ્યૂ ઉમેરો', | ||||
|                 filter: 'ફિલ્ટર', | ||||
|                 collapse: 'બધાને સકુચિત કરો', | ||||
|                 // TODO: Translate | ||||
| @@ -148,16 +147,23 @@ export const gu: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'ફીલ્ડ લક્ષણો', | ||||
|                         unique: 'અદ્વિતીય', | ||||
|                         auto_increment: 'ઑટો ઇન્ક્રિમેન્ટ', | ||||
|                         comments: 'ટિપ્પણીઓ', | ||||
|                         no_comments: 'કોઈ ટિપ્પણીઓ નથી', | ||||
|                         delete_field: 'ફીલ્ડ કાઢી નાખો', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'ચોકસાઈ', | ||||
|                         scale: 'માપ', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'ઇન્ડેક્સ લક્ષણો', | ||||
|                         name: 'નામ', | ||||
|                         unique: 'અદ્વિતીય', | ||||
|                         index_type: 'ઇન્ડેક્સ પ્રકાર', | ||||
|                         delete_index: 'ઇન્ડેક્સ કાઢી નાખો', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -174,14 +180,17 @@ export const gu: LanguageTranslation = { | ||||
|                     description: 'શરૂ કરવા માટે એક ટેબલ બનાવો', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'સંબંધો', | ||||
|             refs_section: { | ||||
|                 refs: 'રેફ્સ', | ||||
|                 filter: 'ફિલ્ટર', | ||||
|                 add_relationship: 'સંબંધ ઉમેરો', | ||||
|                 collapse: 'બધાને સકુચિત કરો', | ||||
|                 add_relationship: 'સંબંધ ઉમેરો', | ||||
|                 relationships: 'સંબંધો', | ||||
|                 dependencies: 'નિર્ભરતાઓ', | ||||
|                 relationship: { | ||||
|                     relationship: 'સંબંધ', | ||||
|                     primary: 'પ્રાથમિક ટેબલ', | ||||
|                     foreign: 'સંદર્ભ ટેબલ', | ||||
|                     foreign: 'સંદર્ભિત ટેબલ', | ||||
|                     cardinality: 'કાર્ડિનાલિટી', | ||||
|                     delete_relationship: 'કાઢી નાખો', | ||||
|                     relationship_actions: { | ||||
| @@ -189,27 +198,19 @@ export const gu: LanguageTranslation = { | ||||
|                         delete_relationship: 'કાઢી નાખો', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'કોઈ સંબંધો નથી', | ||||
|                     description: 'ટેબલ્સ કનેક્ટ કરવા માટે એક સંબંધ બનાવો', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'નિર્ભરતાઓ', | ||||
|                 filter: 'ફિલ્ટર', | ||||
|                 collapse: 'સિકોડો', | ||||
|                 dependency: { | ||||
|                     dependency: 'નિર્ભરતા', | ||||
|                     table: 'ટેબલ', | ||||
|                     dependent_table: 'આધાર રાખેલું ટેબલ', | ||||
|                     delete_dependency: 'નિર્ભરતા કાઢી નાખો', | ||||
|                     dependent_table: 'નિર્ભરશીલ વ્યૂ', | ||||
|                     delete_dependency: 'કાઢી નાખો', | ||||
|                     dependency_actions: { | ||||
|                         title: 'ક્રિયાઓ', | ||||
|                         delete_dependency: 'નિર્ભરતા કાઢી નાખો', | ||||
|                         delete_dependency: 'કાઢી નાખો', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'કોઈ નિર્ભરતાઓ નથી', | ||||
|                     description: 'આ વિભાગમાં કોઈ નિર્ભરતા ઉપલબ્ધ નથી.', | ||||
|                     title: 'કોઈ સંબંધો નથી', | ||||
|                     description: 'શરૂ કરવા માટે એક સંબંધ બનાવો', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -252,9 +253,12 @@ export const gu: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -269,7 +273,13 @@ export const gu: LanguageTranslation = { | ||||
|             undo: 'અનડુ', | ||||
|             redo: 'રીડુ', | ||||
|             reorder_diagram: 'ડાયાગ્રામ ફરીથી વ્યવસ્થિત કરો', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: 'ઓવરલેપ કરતો ટેબલ હાઇલાઇટ કરો', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -315,6 +325,12 @@ export const gu: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'રદ કરો', | ||||
|             open: 'ખોલો', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'ખોલો', | ||||
|                 duplicate: 'ડુપ્લિકેટ', | ||||
|                 delete: 'ડાયાગ્રામ કાઢી નાખો', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -401,6 +417,14 @@ export const gu: LanguageTranslation = { | ||||
|             confirm: 'બદલો', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'નવું સ્કીમા બનાવો', | ||||
|             description: | ||||
|                 'હજી સુધી કોઈ સ્કીમા અસ્તિત્વમાં નથી. તમારા ટેબલ્સ ને વ્યવસ્થિત કરવા માટે તમારું પહેલું સ્કીમા બનાવો.', | ||||
|             create: 'બનાવો', | ||||
|             cancel: 'રદ કરો', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: 'અમને સુધારવામાં મદદ કરો!', | ||||
|             description: | ||||
| @@ -456,6 +480,7 @@ export const gu: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'નવું ટેબલ', | ||||
|             new_view: 'નવું વ્યૂ', | ||||
|             new_relationship: 'નવો સંબંધ', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -477,6 +502,9 @@ export const gu: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'ભાષા બદલો', | ||||
|         }, | ||||
|  | ||||
|         on: 'ચાલુ', | ||||
|         off: 'બંધ', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const hi: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'नया', | ||||
|             browse: 'ब्राउज़', | ||||
|             tables: 'टेबल', | ||||
|             refs: 'रेफ्स', | ||||
|             areas: 'क्षेत्र', | ||||
|             dependencies: 'निर्भरताएं', | ||||
|             custom_types: 'कस्टम टाइप', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'फ़ाइल', | ||||
|                 new: 'नया', | ||||
|                 open: 'खोलें', | ||||
|             actions: { | ||||
|                 actions: 'कार्य', | ||||
|                 new: 'नया आरेख', | ||||
|                 browse: 'ब्राउज़ करें...', | ||||
|                 save: 'सहेजें', | ||||
|                 import: 'डेटाबेस आयात करें', | ||||
|                 export_sql: 'SQL निर्यात करें', | ||||
|                 export_as: 'के रूप में निर्यात करें', | ||||
|                 delete_diagram: 'आरेख हटाएँ', | ||||
|                 exit: 'बाहर जाएँ', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'संपादित करें', | ||||
| @@ -26,7 +34,10 @@ export const hi: LanguageTranslation = { | ||||
|                 hide_sidebar: 'साइडबार छिपाएँ', | ||||
|                 hide_cardinality: 'कार्डिनैलिटी छिपाएँ', | ||||
|                 show_cardinality: 'कार्डिनैलिटी दिखाएँ', | ||||
|                 hide_field_attributes: 'फ़ील्ड विशेषताएँ छिपाएँ', | ||||
|                 show_field_attributes: 'फ़ील्ड विशेषताएँ दिखाएँ', | ||||
|                 zoom_on_scroll: 'स्क्रॉल पर ज़ूम', | ||||
|                 show_views: 'डेटाबेस व्यू', | ||||
|                 theme: 'थीम', | ||||
|                 show_dependencies: 'निर्भरता दिखाएँ', | ||||
|                 hide_dependencies: 'निर्भरता छिपाएँ', | ||||
| @@ -70,15 +81,6 @@ export const hi: LanguageTranslation = { | ||||
|             cancel: 'रद्द करें', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'एकाधिक स्कीमा', | ||||
|             description: | ||||
|                 '{{schemasCount}} स्कीमा इस आरेख में हैं। वर्तमान में प्रदर्शित: {{formattedSchemas}}।', | ||||
|             dont_show_again: 'फिर से न दिखाएँ', | ||||
|             change_schema: 'बदलें', | ||||
|             none: 'कोई नहीं', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'कॉपी असफल', | ||||
| @@ -114,14 +116,11 @@ export const hi: LanguageTranslation = { | ||||
|         copied: 'Copied!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'स्कीमा:', | ||||
|             filter_by_schema: 'स्कीमा द्वारा फ़िल्टर करें', | ||||
|             search_schema: 'स्कीमा खोजें...', | ||||
|             no_schemas_found: 'कोई स्कीमा नहीं मिला।', | ||||
|             view_all_options: 'सभी विकल्प देखें...', | ||||
|             tables_section: { | ||||
|                 tables: 'तालिकाएँ', | ||||
|                 add_table: 'तालिका जोड़ें', | ||||
|                 add_view: 'व्यू जोड़ें', | ||||
|                 filter: 'फ़िल्टर', | ||||
|                 collapse: 'सभी को संक्षिप्त करें', | ||||
|                 // TODO: Translate | ||||
| @@ -147,16 +146,23 @@ export const hi: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'फ़ील्ड विशेषताएँ', | ||||
|                         unique: 'अद्वितीय', | ||||
|                         auto_increment: 'ऑटो इंक्रीमेंट', | ||||
|                         comments: 'टिप्पणियाँ', | ||||
|                         no_comments: 'कोई टिप्पणी नहीं', | ||||
|                         delete_field: 'फ़ील्ड हटाएँ', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'Precision', | ||||
|                         scale: 'Scale', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'सूचकांक विशेषताएँ', | ||||
|                         name: 'नाम', | ||||
|                         unique: 'अद्वितीय', | ||||
|                         index_type: 'इंडेक्स प्रकार', | ||||
|                         delete_index: 'सूचकांक हटाएँ', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -173,12 +179,15 @@ export const hi: LanguageTranslation = { | ||||
|                     description: 'शुरू करने के लिए एक तालिका बनाएँ', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'संबंध', | ||||
|             refs_section: { | ||||
|                 refs: 'रेफ्स', | ||||
|                 filter: 'फ़िल्टर', | ||||
|                 add_relationship: 'संबंध जोड़ें', | ||||
|                 collapse: 'सभी को संक्षिप्त करें', | ||||
|                 add_relationship: 'संबंध जोड़ें', | ||||
|                 relationships: 'संबंध', | ||||
|                 dependencies: 'निर्भरताएँ', | ||||
|                 relationship: { | ||||
|                     relationship: 'संबंध', | ||||
|                     primary: 'प्राथमिक तालिका', | ||||
|                     foreign: 'संदर्भित तालिका', | ||||
|                     cardinality: 'कार्डिनैलिटी', | ||||
| @@ -188,28 +197,19 @@ export const hi: LanguageTranslation = { | ||||
|                         delete_relationship: 'हटाएँ', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'कोई संबंध नहीं', | ||||
|                     description: | ||||
|                         'तालिकाओं को कनेक्ट करने के लिए एक संबंध बनाएँ', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'निर्भरताएँ', | ||||
|                 filter: 'फ़िल्टर', | ||||
|                 collapse: 'सिकोड़ें', | ||||
|                 dependency: { | ||||
|                     dependency: 'निर्भरता', | ||||
|                     table: 'तालिका', | ||||
|                     dependent_table: 'आश्रित तालिका', | ||||
|                     delete_dependency: 'निर्भरता हटाएँ', | ||||
|                     dependent_table: 'आश्रित दृश्य', | ||||
|                     delete_dependency: 'हटाएँ', | ||||
|                     dependency_actions: { | ||||
|                         title: 'कार्रवाइयाँ', | ||||
|                         delete_dependency: 'निर्भरता हटाएँ', | ||||
|                         title: 'क्रियाएँ', | ||||
|                         delete_dependency: 'हटाएँ', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'कोई निर्भरता नहीं', | ||||
|                     description: 'इस अनुभाग में कोई निर्भरता उपलब्ध नहीं है।', | ||||
|                     title: 'कोई संबंध नहीं', | ||||
|                     description: 'शुरू करने के लिए एक संबंध बनाएँ', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -252,9 +252,12 @@ export const hi: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -269,7 +272,13 @@ export const hi: LanguageTranslation = { | ||||
|             undo: 'पूर्ववत करें', | ||||
|             redo: 'पुनः करें', | ||||
|             reorder_diagram: 'आरेख पुनः व्यवस्थित करें', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: 'ओवरलैपिंग तालिकाओं को हाइलाइट करें', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -318,6 +327,12 @@ export const hi: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'रद्द करें', | ||||
|             open: 'खोलें', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'खोलें', | ||||
|                 duplicate: 'डुप्लिकेट', | ||||
|                 delete: 'डायग्राम हटाएं', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -404,6 +419,14 @@ export const hi: LanguageTranslation = { | ||||
|             confirm: 'बदलें', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'नया स्कीमा बनाएं', | ||||
|             description: | ||||
|                 'अभी तक कोई स्कीमा मौजूद नहीं है। अपनी तालिकाओं को व्यवस्थित करने के लिए अपना पहला स्कीमा बनाएं।', | ||||
|             create: 'बनाएं', | ||||
|             cancel: 'रद्द करें', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: 'हमें सुधारने में मदद करें!', | ||||
|             description: | ||||
| @@ -459,6 +482,7 @@ export const hi: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'नई तालिका', | ||||
|             new_view: 'नया व्यू', | ||||
|             new_relationship: 'नया संबंध', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -481,6 +505,9 @@ export const hi: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'भाषा बदलें', | ||||
|         }, | ||||
|  | ||||
|         on: 'चालू', | ||||
|         off: 'बंद', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
							
								
								
									
										508
									
								
								src/i18n/locales/hr.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										508
									
								
								src/i18n/locales/hr.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,508 @@ | ||||
| import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const hr: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Novi', | ||||
|             browse: 'Pregledaj', | ||||
|             tables: 'Tablice', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Područja', | ||||
|             dependencies: 'Ovisnosti', | ||||
|             custom_types: 'Prilagođeni Tipovi', | ||||
|         }, | ||||
|         menu: { | ||||
|             actions: { | ||||
|                 actions: 'Akcije', | ||||
|                 new: 'Novi Dijagram', | ||||
|                 browse: 'Pregledaj...', | ||||
|                 save: 'Spremi', | ||||
|                 import: 'Uvezi', | ||||
|                 export_sql: 'Izvezi SQL', | ||||
|                 export_as: 'Izvezi kao', | ||||
|                 delete_diagram: 'Izbriši dijagram', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Uredi', | ||||
|                 undo: 'Poništi', | ||||
|                 redo: 'Ponovi', | ||||
|                 clear: 'Očisti', | ||||
|             }, | ||||
|             view: { | ||||
|                 view: 'Prikaz', | ||||
|                 show_sidebar: 'Prikaži bočnu traku', | ||||
|                 hide_sidebar: 'Sakrij bočnu traku', | ||||
|                 hide_cardinality: 'Sakrij kardinalnost', | ||||
|                 show_cardinality: 'Prikaži kardinalnost', | ||||
|                 hide_field_attributes: 'Sakrij atribute polja', | ||||
|                 show_field_attributes: 'Prikaži atribute polja', | ||||
|                 zoom_on_scroll: 'Zumiranje pri skrolanju', | ||||
|                 show_views: 'Pogledi Baze Podataka', | ||||
|                 theme: 'Tema', | ||||
|                 show_dependencies: 'Prikaži ovisnosti', | ||||
|                 hide_dependencies: 'Sakrij ovisnosti', | ||||
|                 show_minimap: 'Prikaži mini kartu', | ||||
|                 hide_minimap: 'Sakrij mini kartu', | ||||
|             }, | ||||
|             backup: { | ||||
|                 backup: 'Sigurnosna kopija', | ||||
|                 export_diagram: 'Izvezi dijagram', | ||||
|                 restore_diagram: 'Vrati dijagram', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'Pomoć', | ||||
|                 docs_website: 'Dokumentacija', | ||||
|                 join_discord: 'Pridružite nam se na Discordu', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         delete_diagram_alert: { | ||||
|             title: 'Izbriši dijagram', | ||||
|             description: | ||||
|                 'Ova radnja se ne može poništiti. Ovo će trajno izbrisati dijagram.', | ||||
|             cancel: 'Odustani', | ||||
|             delete: 'Izbriši', | ||||
|         }, | ||||
|  | ||||
|         clear_diagram_alert: { | ||||
|             title: 'Očisti dijagram', | ||||
|             description: | ||||
|                 'Ova radnja se ne može poništiti. Ovo će trajno izbrisati sve podatke u dijagramu.', | ||||
|             cancel: 'Odustani', | ||||
|             clear: 'Očisti', | ||||
|         }, | ||||
|  | ||||
|         reorder_diagram_alert: { | ||||
|             title: 'Preuredi dijagram', | ||||
|             description: | ||||
|                 'Ova radnja će preurediti sve tablice u dijagramu. Želite li nastaviti?', | ||||
|             reorder: 'Preuredi', | ||||
|             cancel: 'Odustani', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Kopiranje neuspješno', | ||||
|                 description: 'Međuspremnik nije podržan.', | ||||
|             }, | ||||
|             failed: { | ||||
|                 title: 'Kopiranje neuspješno', | ||||
|                 description: 'Nešto je pošlo po zlu. Molimo pokušajte ponovno.', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         theme: { | ||||
|             system: 'Sustav', | ||||
|             light: 'Svijetla', | ||||
|             dark: 'Tamna', | ||||
|         }, | ||||
|  | ||||
|         zoom: { | ||||
|             on: 'Uključeno', | ||||
|             off: 'Isključeno', | ||||
|         }, | ||||
|  | ||||
|         last_saved: 'Zadnje spremljeno', | ||||
|         saved: 'Spremljeno', | ||||
|         loading_diagram: 'Učitavanje dijagrama...', | ||||
|         deselect_all: 'Odznači sve', | ||||
|         select_all: 'Označi sve', | ||||
|         clear: 'Očisti', | ||||
|         show_more: 'Prikaži više', | ||||
|         show_less: 'Prikaži manje', | ||||
|         copy_to_clipboard: 'Kopiraj u međuspremnik', | ||||
|         copied: 'Kopirano!', | ||||
|  | ||||
|         side_panel: { | ||||
|             view_all_options: 'Prikaži sve opcije...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tablice', | ||||
|                 add_table: 'Dodaj tablicu', | ||||
|                 add_view: 'Dodaj Pogled', | ||||
|                 filter: 'Filtriraj', | ||||
|                 collapse: 'Sažmi sve', | ||||
|                 clear: 'Očisti filter', | ||||
|                 no_results: | ||||
|                     'Nema pronađenih tablica koje odgovaraju vašem filteru.', | ||||
|                 show_list: 'Prikaži popis tablica', | ||||
|                 show_dbml: 'Prikaži DBML uređivač', | ||||
|  | ||||
|                 table: { | ||||
|                     fields: 'Polja', | ||||
|                     nullable: 'Može biti null?', | ||||
|                     primary_key: 'Primarni ključ', | ||||
|                     indexes: 'Indeksi', | ||||
|                     comments: 'Komentari', | ||||
|                     no_comments: 'Nema komentara', | ||||
|                     add_field: 'Dodaj polje', | ||||
|                     add_index: 'Dodaj indeks', | ||||
|                     index_select_fields: 'Odaberi polja', | ||||
|                     no_types_found: 'Nema pronađenih tipova', | ||||
|                     field_name: 'Naziv', | ||||
|                     field_type: 'Tip', | ||||
|                     field_actions: { | ||||
|                         title: 'Atributi polja', | ||||
|                         unique: 'Jedinstven', | ||||
|                         auto_increment: 'Automatsko povećavanje', | ||||
|                         character_length: 'Maksimalna dužina', | ||||
|                         precision: 'Preciznost', | ||||
|                         scale: 'Skala', | ||||
|                         comments: 'Komentari', | ||||
|                         no_comments: 'Nema komentara', | ||||
|                         default_value: 'Zadana vrijednost', | ||||
|                         no_default: 'Nema zadane vrijednosti', | ||||
|                         delete_field: 'Izbriši polje', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'Atributi indeksa', | ||||
|                         name: 'Naziv', | ||||
|                         unique: 'Jedinstven', | ||||
|                         index_type: 'Vrsta indeksa', | ||||
|                         delete_index: 'Izbriši indeks', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
|                         title: 'Radnje nad tablicom', | ||||
|                         change_schema: 'Promijeni shemu', | ||||
|                         add_field: 'Dodaj polje', | ||||
|                         add_index: 'Dodaj indeks', | ||||
|                         duplicate_table: 'Dupliciraj tablicu', | ||||
|                         delete_table: 'Izbriši tablicu', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Nema tablica', | ||||
|                     description: 'Stvorite tablicu za početak', | ||||
|                 }, | ||||
|             }, | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Filtriraj', | ||||
|                 collapse: 'Sažmi sve', | ||||
|                 add_relationship: 'Dodaj vezu', | ||||
|                 relationships: 'Veze', | ||||
|                 dependencies: 'Ovisnosti', | ||||
|                 relationship: { | ||||
|                     relationship: 'Veza', | ||||
|                     primary: 'Primarna tablica', | ||||
|                     foreign: 'Referentna tablica', | ||||
|                     cardinality: 'Kardinalnost', | ||||
|                     delete_relationship: 'Izbriši', | ||||
|                     relationship_actions: { | ||||
|                         title: 'Radnje', | ||||
|                         delete_relationship: 'Izbriši', | ||||
|                     }, | ||||
|                 }, | ||||
|                 dependency: { | ||||
|                     dependency: 'Ovisnost', | ||||
|                     table: 'Tablica', | ||||
|                     dependent_table: 'Ovisni pogled', | ||||
|                     delete_dependency: 'Izbriši', | ||||
|                     dependency_actions: { | ||||
|                         title: 'Radnje', | ||||
|                         delete_dependency: 'Izbriši', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Nema veze', | ||||
|                     description: 'Stvorite vezu za početak', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
|             areas_section: { | ||||
|                 areas: 'Područja', | ||||
|                 add_area: 'Dodaj područje', | ||||
|                 filter: 'Filtriraj', | ||||
|                 clear: 'Očisti filter', | ||||
|                 no_results: | ||||
|                     'Nema pronađenih područja koja odgovaraju vašem filteru.', | ||||
|  | ||||
|                 area: { | ||||
|                     area_actions: { | ||||
|                         title: 'Radnje nad područjem', | ||||
|                         edit_name: 'Uredi naziv', | ||||
|                         delete_area: 'Izbriši područje', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Nema područja', | ||||
|                     description: 'Stvorite područje za početak', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
|             custom_types_section: { | ||||
|                 custom_types: 'Prilagođeni tipovi', | ||||
|                 filter: 'Filtriraj', | ||||
|                 clear: 'Očisti filter', | ||||
|                 no_results: | ||||
|                     'Nema pronađenih prilagođenih tipova koji odgovaraju vašem filteru.', | ||||
|                 empty_state: { | ||||
|                     title: 'Nema prilagođenih tipova', | ||||
|                     description: | ||||
|                         'Prilagođeni tipovi će se pojaviti ovdje kada budu dostupni u vašoj bazi podataka', | ||||
|                 }, | ||||
|                 custom_type: { | ||||
|                     kind: 'Vrsta', | ||||
|                     enum_values: 'Enum vrijednosti', | ||||
|                     composite_fields: 'Polja', | ||||
|                     no_fields: 'Nema definiranih polja', | ||||
|                     field_name_placeholder: 'Naziv polja', | ||||
|                     field_type_placeholder: 'Odaberi tip', | ||||
|                     add_field: 'Dodaj polje', | ||||
|                     no_fields_tooltip: | ||||
|                         'Nema definiranih polja za ovaj prilagođeni tip', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Radnje', | ||||
|                         highlight_fields: 'Istakni polja', | ||||
|                         clear_field_highlight: 'Ukloni isticanje', | ||||
|                         delete_custom_type: 'Izbriši', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Izbriši tip', | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         toolbar: { | ||||
|             zoom_in: 'Uvećaj', | ||||
|             zoom_out: 'Smanji', | ||||
|             save: 'Spremi', | ||||
|             show_all: 'Prikaži sve', | ||||
|             undo: 'Poništi', | ||||
|             redo: 'Ponovi', | ||||
|             reorder_diagram: 'Preuredi dijagram', | ||||
|             highlight_overlapping_tables: 'Istakni preklapajuće tablice', | ||||
|             clear_custom_type_highlight: 'Ukloni isticanje za "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Isticanje "{{typeName}}" - Kliknite za uklanjanje', | ||||
|             filter: 'Filtriraj tablice', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
|             database_selection: { | ||||
|                 title: 'Koja je vaša baza podataka?', | ||||
|                 description: | ||||
|                     'Svaka baza podataka ima svoje jedinstvene značajke i mogućnosti.', | ||||
|                 check_examples_long: 'Pogledaj primjere', | ||||
|                 check_examples_short: 'Primjeri', | ||||
|             }, | ||||
|  | ||||
|             import_database: { | ||||
|                 title: 'Uvezite svoju bazu podataka', | ||||
|                 database_edition: 'Verzija baze podataka:', | ||||
|                 step_1: 'Pokrenite ovu skriptu u svojoj bazi podataka:', | ||||
|                 step_2: 'Zalijepite rezultat skripte u ovaj dio →', | ||||
|                 script_results_placeholder: 'Rezultati skripte ovdje...', | ||||
|                 ssms_instructions: { | ||||
|                     button_text: 'SSMS upute', | ||||
|                     title: 'Upute', | ||||
|                     step_1: 'Idite na Tools > Options > Query Results > SQL Server.', | ||||
|                     step_2: 'Ako koristite "Results to Grid," promijenite Maximum Characters Retrieved za Non-XML podatke (postavite na 9999999).', | ||||
|                 }, | ||||
|                 instructions_link: 'Trebate pomoć? Pogledajte kako', | ||||
|                 check_script_result: 'Provjeri rezultat skripte', | ||||
|             }, | ||||
|  | ||||
|             cancel: 'Odustani', | ||||
|             import_from_file: 'Uvezi iz datoteke', | ||||
|             back: 'Natrag', | ||||
|             empty_diagram: 'Prazan dijagram', | ||||
|             continue: 'Nastavi', | ||||
|             import: 'Uvezi', | ||||
|         }, | ||||
|  | ||||
|         open_diagram_dialog: { | ||||
|             title: 'Otvori dijagram', | ||||
|             description: 'Odaberite dijagram za otvaranje iz popisa ispod.', | ||||
|             table_columns: { | ||||
|                 name: 'Naziv', | ||||
|                 created_at: 'Stvoreno', | ||||
|                 last_modified: 'Zadnje izmijenjeno', | ||||
|                 tables_count: 'Tablice', | ||||
|             }, | ||||
|             cancel: 'Odustani', | ||||
|             open: 'Otvori', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Otvori', | ||||
|                 duplicate: 'Dupliciraj', | ||||
|                 delete: 'Obriši dijagram', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
|             title: 'Izvezi SQL', | ||||
|             description: | ||||
|                 'Izvezite shemu vašeg dijagrama u {{databaseType}} skriptu', | ||||
|             close: 'Zatvori', | ||||
|             loading: { | ||||
|                 text: 'AI generira SQL za {{databaseType}}...', | ||||
|                 description: 'Ovo bi trebalo potrajati do 30 sekundi.', | ||||
|             }, | ||||
|             error: { | ||||
|                 message: | ||||
|                     'Greška pri generiranju SQL skripte. Molimo pokušajte ponovno kasnije ili <0>kontaktirajte nas</0>.', | ||||
|                 description: | ||||
|                     'Slobodno koristite svoj OPENAI_TOKEN, pogledajte priručnik <0>ovdje</0>.', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         create_relationship_dialog: { | ||||
|             title: 'Kreiraj vezu', | ||||
|             primary_table: 'Primarna tablica', | ||||
|             primary_field: 'Primarno polje', | ||||
|             referenced_table: 'Referentna tablica', | ||||
|             referenced_field: 'Referentno polje', | ||||
|             primary_table_placeholder: 'Odaberi tablicu', | ||||
|             primary_field_placeholder: 'Odaberi polje', | ||||
|             referenced_table_placeholder: 'Odaberi tablicu', | ||||
|             referenced_field_placeholder: 'Odaberi polje', | ||||
|             no_tables_found: 'Nema pronađenih tablica', | ||||
|             no_fields_found: 'Nema pronađenih polja', | ||||
|             create: 'Kreiraj', | ||||
|             cancel: 'Odustani', | ||||
|         }, | ||||
|  | ||||
|         import_database_dialog: { | ||||
|             title: 'Uvezi u trenutni dijagram', | ||||
|             override_alert: { | ||||
|                 title: 'Uvezi bazu podataka', | ||||
|                 content: { | ||||
|                     alert: 'Uvoz ovog dijagrama će utjecati na postojeće tablice i veze.', | ||||
|                     new_tables: | ||||
|                         '<bold>{{newTablesNumber}}</bold> novih tablica će biti dodano.', | ||||
|                     new_relationships: | ||||
|                         '<bold>{{newRelationshipsNumber}}</bold> novih veza će biti stvoreno.', | ||||
|                     tables_override: | ||||
|                         '<bold>{{tablesOverrideNumber}}</bold> tablica će biti prepisano.', | ||||
|                     proceed: 'Želite li nastaviti?', | ||||
|                 }, | ||||
|                 import: 'Uvezi', | ||||
|                 cancel: 'Odustani', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_image_dialog: { | ||||
|             title: 'Izvezi sliku', | ||||
|             description: 'Odaberite faktor veličine za izvoz:', | ||||
|             scale_1x: '1x Obično', | ||||
|             scale_2x: '2x (Preporučeno)', | ||||
|             scale_3x: '3x', | ||||
|             scale_4x: '4x', | ||||
|             cancel: 'Odustani', | ||||
|             export: 'Izvezi', | ||||
|             advanced_options: 'Napredne opcije', | ||||
|             pattern: 'Uključi pozadinski uzorak', | ||||
|             pattern_description: 'Dodaj suptilni mrežni uzorak u pozadinu.', | ||||
|             transparent: 'Prozirna pozadina', | ||||
|             transparent_description: 'Ukloni boju pozadine iz slike.', | ||||
|         }, | ||||
|  | ||||
|         new_table_schema_dialog: { | ||||
|             title: 'Odaberi shemu', | ||||
|             description: | ||||
|                 'Trenutno je prikazano više shema. Odaberite jednu za novu tablicu.', | ||||
|             cancel: 'Odustani', | ||||
|             confirm: 'Potvrdi', | ||||
|         }, | ||||
|  | ||||
|         update_table_schema_dialog: { | ||||
|             title: 'Promijeni shemu', | ||||
|             description: 'Ažuriraj shemu tablice "{{tableName}}"', | ||||
|             cancel: 'Odustani', | ||||
|             confirm: 'Promijeni', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'Stvori novu shemu', | ||||
|             description: | ||||
|                 'Još ne postoje sheme. Stvorite svoju prvu shemu za organiziranje tablica.', | ||||
|             create: 'Stvori', | ||||
|             cancel: 'Odustani', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: 'Pomozite nam da se poboljšamo!', | ||||
|             description: | ||||
|                 'Želite li nam dati zvjezdicu na GitHubu? Samo je jedan klik!', | ||||
|             close: 'Ne sada', | ||||
|             confirm: 'Naravno!', | ||||
|         }, | ||||
|         export_diagram_dialog: { | ||||
|             title: 'Izvezi dijagram', | ||||
|             description: 'Odaberite format za izvoz:', | ||||
|             format_json: 'JSON', | ||||
|             cancel: 'Odustani', | ||||
|             export: 'Izvezi', | ||||
|             error: { | ||||
|                 title: 'Greška pri izvozu dijagrama', | ||||
|                 description: | ||||
|                     'Nešto je pošlo po zlu. Trebate pomoć? support@chartdb.io', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         import_diagram_dialog: { | ||||
|             title: 'Uvezi dijagram', | ||||
|             description: 'Uvezite dijagram iz JSON datoteke.', | ||||
|             cancel: 'Odustani', | ||||
|             import: 'Uvezi', | ||||
|             error: { | ||||
|                 title: 'Greška pri uvozu dijagrama', | ||||
|                 description: | ||||
|                     'JSON dijagrama je nevažeći. Molimo provjerite JSON i pokušajte ponovno. Trebate pomoć? support@chartdb.io', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         import_dbml_dialog: { | ||||
|             example_title: 'Uvezi primjer DBML-a', | ||||
|             title: 'Uvezi DBML', | ||||
|             description: 'Uvezite shemu baze podataka iz DBML formata.', | ||||
|             import: 'Uvezi', | ||||
|             cancel: 'Odustani', | ||||
|             skip_and_empty: 'Preskoči i isprazni', | ||||
|             show_example: 'Prikaži primjer', | ||||
|             error: { | ||||
|                 title: 'Greška pri uvozu DBML-a', | ||||
|                 description: | ||||
|                     'Neuspješno parsiranje DBML-a. Molimo provjerite sintaksu.', | ||||
|             }, | ||||
|         }, | ||||
|         relationship_type: { | ||||
|             one_to_one: 'Jedan na jedan', | ||||
|             one_to_many: 'Jedan na više', | ||||
|             many_to_one: 'Više na jedan', | ||||
|             many_to_many: 'Više na više', | ||||
|         }, | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Nova tablica', | ||||
|             new_view: 'Novi Pogled', | ||||
|             new_relationship: 'Nova veza', | ||||
|             new_area: 'Novo područje', | ||||
|         }, | ||||
|  | ||||
|         table_node_context_menu: { | ||||
|             edit_table: 'Uredi tablicu', | ||||
|             duplicate_table: 'Dupliciraj tablicu', | ||||
|             delete_table: 'Izbriši tablicu', | ||||
|             add_relationship: 'Dodaj vezu', | ||||
|         }, | ||||
|  | ||||
|         snap_to_grid_tooltip: 'Priljepljivanje na mrežu (Drži {{key}})', | ||||
|  | ||||
|         tool_tips: { | ||||
|             double_click_to_edit: 'Dvostruki klik za uređivanje', | ||||
|         }, | ||||
|  | ||||
|         language_select: { | ||||
|             change_language: 'Jezik', | ||||
|         }, | ||||
|  | ||||
|         on: 'Uključeno', | ||||
|         off: 'Isključeno', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| export const hrMetadata: LanguageMetadata = { | ||||
|     name: 'Croatian', | ||||
|     nativeName: 'Hrvatski', | ||||
|     code: 'hr', | ||||
| }; | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const id_ID: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Baru', | ||||
|             browse: 'Jelajahi', | ||||
|             tables: 'Tabel', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Area', | ||||
|             dependencies: 'Ketergantungan', | ||||
|             custom_types: 'Tipe Kustom', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Berkas', | ||||
|                 new: 'Buat Baru', | ||||
|                 open: 'Buka', | ||||
|             actions: { | ||||
|                 actions: 'Aksi', | ||||
|                 new: 'Diagram Baru', | ||||
|                 browse: 'Jelajahi...', | ||||
|                 save: 'Simpan', | ||||
|                 import: 'Impor Database', | ||||
|                 export_sql: 'Ekspor SQL', | ||||
|                 export_as: 'Ekspor Sebagai', | ||||
|                 delete_diagram: 'Hapus Diagram', | ||||
|                 exit: 'Keluar', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Ubah', | ||||
| @@ -26,7 +34,10 @@ export const id_ID: LanguageTranslation = { | ||||
|                 hide_sidebar: 'Sembunyikan Sidebar', | ||||
|                 hide_cardinality: 'Sembunyikan Kardinalitas', | ||||
|                 show_cardinality: 'Tampilkan Kardinalitas', | ||||
|                 hide_field_attributes: 'Sembunyikan Atribut Kolom', | ||||
|                 show_field_attributes: 'Tampilkan Atribut Kolom', | ||||
|                 zoom_on_scroll: 'Perbesar saat Scroll', | ||||
|                 show_views: 'Tampilan Database', | ||||
|                 theme: 'Tema', | ||||
|                 show_dependencies: 'Tampilkan Dependensi', | ||||
|                 hide_dependencies: 'Sembunyikan Dependensi', | ||||
| @@ -70,15 +81,6 @@ export const id_ID: LanguageTranslation = { | ||||
|             cancel: 'Batal', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Schema Lebih dari satu', | ||||
|             description: | ||||
|                 '{{schemasCount}} schema di diagram ini. Sedang ditampilkan: {{formattedSchemas}}.', | ||||
|             dont_show_again: 'Jangan tampilkan lagi', | ||||
|             change_schema: 'Ubah', | ||||
|             none: 'Tidak ada', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Gagal menyalin', | ||||
| @@ -113,14 +115,11 @@ export const id_ID: LanguageTranslation = { | ||||
|         copied: 'Tersalin!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Skema:', | ||||
|             filter_by_schema: 'Saring berdasarkan skema', | ||||
|             search_schema: 'Cari skema...', | ||||
|             no_schemas_found: 'Tidak ada skema yang ditemukan.', | ||||
|             view_all_options: 'Tampilkan Semua Pilihan...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tabel', | ||||
|                 add_table: 'Tambah Tabel', | ||||
|                 add_view: 'Tambah Tampilan', | ||||
|                 filter: 'Saring', | ||||
|                 collapse: 'Lipat Semua', | ||||
|                 // TODO: Translate | ||||
| @@ -146,16 +145,23 @@ export const id_ID: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Atribut Kolom', | ||||
|                         unique: 'Unik', | ||||
|                         auto_increment: 'Kenaikan Otomatis', | ||||
|                         comments: 'Komentar', | ||||
|                         no_comments: 'Tidak ada komentar', | ||||
|                         delete_field: 'Hapus Kolom', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'Presisi', | ||||
|                         scale: 'Skala', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'Atribut Indeks', | ||||
|                         name: 'Nama', | ||||
|                         unique: 'Unik', | ||||
|                         index_type: 'Tipe Indeks', | ||||
|                         delete_index: 'Hapus Indeks', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -172,12 +178,15 @@ export const id_ID: LanguageTranslation = { | ||||
|                     description: 'Buat tabel untuk memulai', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Hubungan', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Saring', | ||||
|                 add_relationship: 'Tambah Hubungan', | ||||
|                 collapse: 'Lipat Semua', | ||||
|                 add_relationship: 'Tambah Hubungan', | ||||
|                 relationships: 'Hubungan', | ||||
|                 dependencies: 'Dependensi', | ||||
|                 relationship: { | ||||
|                     relationship: 'Hubungan', | ||||
|                     primary: 'Tabel Primer', | ||||
|                     foreign: 'Tabel Referensi', | ||||
|                     cardinality: 'Kardinalitas', | ||||
| @@ -187,16 +196,8 @@ export const id_ID: LanguageTranslation = { | ||||
|                         delete_relationship: 'Hapus', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Tidak ada hubungan', | ||||
|                     description: 'Buat hubungan untuk menghubungkan tabel', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Dependensi', | ||||
|                 filter: 'Saring', | ||||
|                 collapse: 'Lipat Semua', | ||||
|                 dependency: { | ||||
|                     dependency: 'Dependensi', | ||||
|                     table: 'Tabel', | ||||
|                     dependent_table: 'Tampilan Dependen', | ||||
|                     delete_dependency: 'Hapus', | ||||
| @@ -206,8 +207,8 @@ export const id_ID: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Tidak ada dependensi', | ||||
|                     description: 'Buat tampilan untuk memulai', | ||||
|                     title: 'Tidak ada hubungan', | ||||
|                     description: 'Buat hubungan untuk memulai', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -250,9 +251,12 @@ export const id_ID: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -267,7 +271,13 @@ export const id_ID: LanguageTranslation = { | ||||
|             undo: 'Undo', | ||||
|             redo: 'Redo', | ||||
|             reorder_diagram: 'Atur Ulang Diagram', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: 'Sorot Tabel yang Tumpang Tindih', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -314,6 +324,12 @@ export const id_ID: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Batal', | ||||
|             open: 'Buka', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Buka', | ||||
|                 duplicate: 'Duplikat', | ||||
|                 delete: 'Hapus Diagram', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -399,6 +415,14 @@ export const id_ID: LanguageTranslation = { | ||||
|             confirm: 'Ubah', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'Buat Skema Baru', | ||||
|             description: | ||||
|                 'Belum ada skema yang tersedia. Buat skema pertama Anda untuk mengatur tabel-tabel Anda.', | ||||
|             create: 'Buat', | ||||
|             cancel: 'Batal', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: 'Bantu kami meningkatkan!', | ||||
|             description: | ||||
| @@ -455,6 +479,7 @@ export const id_ID: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Tabel Baru', | ||||
|             new_view: 'Tampilan Baru', | ||||
|             new_relationship: 'Hubungan Baru', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -476,6 +501,9 @@ export const id_ID: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Bahasa', | ||||
|         }, | ||||
|  | ||||
|         on: 'Aktif', | ||||
|         off: 'Nonaktif', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const ja: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: '新規', | ||||
|             browse: '参照', | ||||
|             tables: 'テーブル', | ||||
|             refs: '参照', | ||||
|             areas: 'エリア', | ||||
|             dependencies: '依存関係', | ||||
|             custom_types: 'カスタムタイプ', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'ファイル', | ||||
|                 new: '新規', | ||||
|                 open: '開く', | ||||
|             actions: { | ||||
|                 actions: 'アクション', | ||||
|                 new: '新しいダイアグラム', | ||||
|                 browse: '参照...', | ||||
|                 save: '保存', | ||||
|                 import: 'データベースをインポート', | ||||
|                 export_sql: 'SQLをエクスポート', | ||||
|                 export_as: '形式を指定してエクスポート', | ||||
|                 delete_diagram: 'ダイアグラムを削除', | ||||
|                 exit: '終了', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: '編集', | ||||
| @@ -26,7 +34,10 @@ export const ja: LanguageTranslation = { | ||||
|                 hide_sidebar: 'サイドバーを非表示', | ||||
|                 hide_cardinality: 'カーディナリティを非表示', | ||||
|                 show_cardinality: 'カーディナリティを表示', | ||||
|                 hide_field_attributes: 'フィールド属性を非表示', | ||||
|                 show_field_attributes: 'フィールド属性を表示', | ||||
|                 zoom_on_scroll: 'スクロールでズーム', | ||||
|                 show_views: 'データベースビュー', | ||||
|                 theme: 'テーマ', | ||||
|                 // TODO: Translate | ||||
|                 show_dependencies: 'Show Dependencies', | ||||
| @@ -72,15 +83,6 @@ export const ja: LanguageTranslation = { | ||||
|             cancel: 'キャンセル', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: '複数のスキーマ', | ||||
|             description: | ||||
|                 'このダイアグラムには{{schemasCount}}個のスキーマがあります。現在表示中: {{formattedSchemas}}。', | ||||
|             dont_show_again: '再表示しない', | ||||
|             change_schema: '変更', | ||||
|             none: 'なし', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'コピー失敗', | ||||
| @@ -117,14 +119,11 @@ export const ja: LanguageTranslation = { | ||||
|         copied: 'Copied!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'スキーマ:', | ||||
|             filter_by_schema: 'スキーマでフィルタ', | ||||
|             search_schema: 'スキーマを検索...', | ||||
|             no_schemas_found: 'スキーマが見つかりません。', | ||||
|             view_all_options: 'すべてのオプションを表示...', | ||||
|             tables_section: { | ||||
|                 tables: 'テーブル', | ||||
|                 add_table: 'テーブルを追加', | ||||
|                 add_view: 'ビューを追加', | ||||
|                 filter: 'フィルタ', | ||||
|                 collapse: 'すべて折りたたむ', | ||||
|                 // TODO: Translate | ||||
| @@ -150,16 +149,23 @@ export const ja: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'フィールド属性', | ||||
|                         unique: 'ユニーク', | ||||
|                         auto_increment: 'オートインクリメント', | ||||
|                         comments: 'コメント', | ||||
|                         no_comments: 'コメントがありません', | ||||
|                         delete_field: 'フィールドを削除', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: '精度', | ||||
|                         scale: '小数点以下桁数', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'インデックス属性', | ||||
|                         name: '名前', | ||||
|                         unique: 'ユニーク', | ||||
|                         index_type: 'インデックスタイプ', | ||||
|                         delete_index: 'インデックスを削除', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -176,12 +182,15 @@ export const ja: LanguageTranslation = { | ||||
|                     description: 'テーブルを作成して開始してください', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'リレーションシップ', | ||||
|             refs_section: { | ||||
|                 refs: '参照', | ||||
|                 filter: 'フィルタ', | ||||
|                 add_relationship: 'リレーションシップを追加', | ||||
|                 collapse: 'すべて折りたたむ', | ||||
|                 add_relationship: 'リレーションシップを追加', | ||||
|                 relationships: 'リレーションシップ', | ||||
|                 dependencies: '依存関係', | ||||
|                 relationship: { | ||||
|                     relationship: 'リレーションシップ', | ||||
|                     primary: '主テーブル', | ||||
|                     foreign: '参照テーブル', | ||||
|                     cardinality: 'カーディナリティ', | ||||
| @@ -191,29 +200,20 @@ export const ja: LanguageTranslation = { | ||||
|                         delete_relationship: '削除', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'リレーションシップがありません', | ||||
|                     description: | ||||
|                         'テーブルを接続するためにリレーションシップを作成してください', | ||||
|                 }, | ||||
|             }, | ||||
|             // TODO: Translate | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Dependencies', | ||||
|                 filter: 'Filter', | ||||
|                 collapse: 'Collapse All', | ||||
|                 dependency: { | ||||
|                     table: 'Table', | ||||
|                     dependent_table: 'Dependent View', | ||||
|                     delete_dependency: 'Delete', | ||||
|                     dependency: '依存関係', | ||||
|                     table: 'テーブル', | ||||
|                     dependent_table: '依存ビュー', | ||||
|                     delete_dependency: '削除', | ||||
|                     dependency_actions: { | ||||
|                         title: 'Actions', | ||||
|                         delete_dependency: 'Delete', | ||||
|                         title: '操作', | ||||
|                         delete_dependency: '削除', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'No dependencies', | ||||
|                     description: 'Create a view to get started', | ||||
|                     title: 'リレーションシップがありません', | ||||
|                     description: | ||||
|                         '開始するためにリレーションシップを作成してください', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -256,9 +256,12 @@ export const ja: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -275,6 +278,10 @@ export const ja: LanguageTranslation = { | ||||
|             reorder_diagram: 'ダイアグラムを並べ替え', | ||||
|             // TODO: Translate | ||||
|             highlight_overlapping_tables: 'Highlight Overlapping Tables', | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -322,6 +329,12 @@ export const ja: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'キャンセル', | ||||
|             open: '開く', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: '開く', | ||||
|                 duplicate: '複製', | ||||
|                 delete: 'ダイアグラムを削除', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -408,6 +421,14 @@ export const ja: LanguageTranslation = { | ||||
|             confirm: '変更', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: '新しいスキーマを作成', | ||||
|             description: | ||||
|                 'スキーマがまだ存在しません。テーブルを整理するために最初のスキーマを作成してください。', | ||||
|             create: '作成', | ||||
|             cancel: 'キャンセル', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: '改善をサポートしてください!', | ||||
|             description: | ||||
| @@ -463,6 +484,7 @@ export const ja: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: '新しいテーブル', | ||||
|             new_view: '新しいビュー', | ||||
|             new_relationship: '新しいリレーションシップ', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -485,6 +507,9 @@ export const ja: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: '言語', | ||||
|         }, | ||||
|  | ||||
|         on: 'オン', | ||||
|         off: 'オフ', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const ko_KR: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: '새로 만들기', | ||||
|             browse: '찾아보기', | ||||
|             tables: '테이블', | ||||
|             refs: 'Refs', | ||||
|             areas: '영역', | ||||
|             dependencies: '종속성', | ||||
|             custom_types: '사용자 지정 타입', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: '파일', | ||||
|             actions: { | ||||
|                 actions: '작업', | ||||
|                 new: '새 다이어그램', | ||||
|                 open: '열기', | ||||
|                 browse: '찾아보기...', | ||||
|                 save: '저장', | ||||
|                 import: '데이터베이스 가져오기', | ||||
|                 export_sql: 'SQL로 저장', | ||||
|                 export_as: '다른 형식으로 저장', | ||||
|                 delete_diagram: '다이어그램 삭제', | ||||
|                 exit: '종료', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: '편집', | ||||
| @@ -26,7 +34,10 @@ export const ko_KR: LanguageTranslation = { | ||||
|                 hide_sidebar: '사이드바 숨기기', | ||||
|                 hide_cardinality: '카디널리티 숨기기', | ||||
|                 show_cardinality: '카디널리티 보이기', | ||||
|                 hide_field_attributes: '필드 속성 숨기기', | ||||
|                 show_field_attributes: '필드 속성 보이기', | ||||
|                 zoom_on_scroll: '스크롤 시 확대', | ||||
|                 show_views: '데이터베이스 뷰', | ||||
|                 theme: '테마', | ||||
|                 show_dependencies: '종속성 보이기', | ||||
|                 hide_dependencies: '종속성 숨기기', | ||||
| @@ -70,15 +81,6 @@ export const ko_KR: LanguageTranslation = { | ||||
|             cancel: '취소', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: '다중 스키마', | ||||
|             description: | ||||
|                 '현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.', | ||||
|             dont_show_again: '다시 보여주지 마세요', | ||||
|             change_schema: '변경', | ||||
|             none: '없음', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: '복사 실패', | ||||
| @@ -113,14 +115,11 @@ export const ko_KR: LanguageTranslation = { | ||||
|         copied: '복사됨!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: '스키마:', | ||||
|             filter_by_schema: '스키마로 필터링', | ||||
|             search_schema: '스키마 검색...', | ||||
|             no_schemas_found: '스키마를 찾을 수 없습니다.', | ||||
|             view_all_options: '전체 옵션 보기...', | ||||
|             tables_section: { | ||||
|                 tables: '테이블', | ||||
|                 add_table: '테이블 추가', | ||||
|                 add_view: '뷰 추가', | ||||
|                 filter: '필터', | ||||
|                 collapse: '모두 접기', | ||||
|                 // TODO: Translate | ||||
| @@ -146,16 +145,23 @@ export const ko_KR: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: '필드 속성', | ||||
|                         unique: '유니크 여부', | ||||
|                         auto_increment: '자동 증가', | ||||
|                         comments: '주석', | ||||
|                         no_comments: '주석 없음', | ||||
|                         delete_field: '필드 삭제', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: '정밀도', | ||||
|                         scale: '소수점 자릿수', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: '인덱스 속성', | ||||
|                         name: '인덱스 명', | ||||
|                         unique: '유니크 여부', | ||||
|                         index_type: '인덱스 타입', | ||||
|                         delete_index: '인덱스 삭제', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -172,12 +178,15 @@ export const ko_KR: LanguageTranslation = { | ||||
|                     description: '테이블을 만들어 시작하세요.', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: '연관 관계', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: '필터', | ||||
|                 add_relationship: '연관 관계 추가', | ||||
|                 collapse: '모두 접기', | ||||
|                 add_relationship: '연관 관계 추가', | ||||
|                 relationships: '연관 관계', | ||||
|                 dependencies: '종속성', | ||||
|                 relationship: { | ||||
|                     relationship: '연관 관계', | ||||
|                     primary: '주 테이블', | ||||
|                     foreign: '참조 테이블', | ||||
|                     cardinality: '카디널리티', | ||||
| @@ -187,16 +196,8 @@ export const ko_KR: LanguageTranslation = { | ||||
|                         delete_relationship: '연관 관계 삭제', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: '연관 관계', | ||||
|                     description: '테이블 연결을 위해 연관 관계를 생성하세요', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: '종속성', | ||||
|                 filter: '필터', | ||||
|                 collapse: '모두 접기', | ||||
|                 dependency: { | ||||
|                     dependency: '종속성', | ||||
|                     table: '테이블', | ||||
|                     dependent_table: '뷰 테이블', | ||||
|                     delete_dependency: '삭제', | ||||
| @@ -206,8 +207,8 @@ export const ko_KR: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: '뷰 테이블 없음', | ||||
|                     description: '뷰 테이블을 만들어 시작하세요.', | ||||
|                     title: '연관 관계 없음', | ||||
|                     description: '연관 관계를 만들어 시작하세요.', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -250,9 +251,12 @@ export const ko_KR: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -267,7 +271,13 @@ export const ko_KR: LanguageTranslation = { | ||||
|             undo: '실행 취소', | ||||
|             redo: '다시 실행', | ||||
|             reorder_diagram: '다이어그램 재정렬', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: '겹치는 테이블 강조 표시', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -314,6 +324,12 @@ export const ko_KR: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: '취소', | ||||
|             open: '열기', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: '열기', | ||||
|                 duplicate: '복제', | ||||
|                 delete: '다이어그램 삭제', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -399,6 +415,14 @@ export const ko_KR: LanguageTranslation = { | ||||
|             confirm: '변경', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: '새 스키마 생성', | ||||
|             description: | ||||
|                 '아직 스키마가 없습니다. 테이블을 정리하기 위해 첫 번째 스키마를 생성하세요.', | ||||
|             create: '생성', | ||||
|             cancel: '취소', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: '개선할 수 있도록 도와주세요!', | ||||
|             description: | ||||
| @@ -452,6 +476,7 @@ export const ko_KR: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: '새 테이블', | ||||
|             new_view: '새 뷰', | ||||
|             new_relationship: '새 연관관계', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -473,6 +498,9 @@ export const ko_KR: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: '언어', | ||||
|         }, | ||||
|  | ||||
|         on: '켜기', | ||||
|         off: '끄기', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const mr: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'नवीन', | ||||
|             browse: 'ब्राउज', | ||||
|             tables: 'टेबल', | ||||
|             refs: 'Refs', | ||||
|             areas: 'क्षेत्रे', | ||||
|             dependencies: 'अवलंबने', | ||||
|             custom_types: 'कस्टम प्रकार', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'फाइल', | ||||
|                 new: 'नवीन', | ||||
|                 open: 'उघडा', | ||||
|             actions: { | ||||
|                 actions: 'क्रिया', | ||||
|                 new: 'नवीन आरेख', | ||||
|                 browse: 'ब्राउज करा...', | ||||
|                 save: 'जतन करा', | ||||
|                 import: 'डेटाबेस इम्पोर्ट करा', | ||||
|                 export_sql: 'SQL एक्स्पोर्ट करा', | ||||
|                 export_as: 'म्हणून एक्स्पोर्ट करा', | ||||
|                 delete_diagram: 'आरेख हटवा', | ||||
|                 exit: 'बाहेर पडा', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'संपादन करा', | ||||
| @@ -26,7 +34,10 @@ export const mr: LanguageTranslation = { | ||||
|                 hide_sidebar: 'साइडबार लपवा', | ||||
|                 hide_cardinality: 'कार्डिनॅलिटी लपवा', | ||||
|                 show_cardinality: 'कार्डिनॅलिटी दाखवा', | ||||
|                 hide_field_attributes: 'फील्ड गुणधर्म लपवा', | ||||
|                 show_field_attributes: 'फील्ड गुणधर्म दाखवा', | ||||
|                 zoom_on_scroll: 'स्क्रोलवर झूम करा', | ||||
|                 show_views: 'डेटाबेस व्ह्यूज', | ||||
|                 theme: 'थीम', | ||||
|                 show_dependencies: 'डिपेंडेन्सि दाखवा', | ||||
|                 hide_dependencies: 'डिपेंडेन्सि लपवा', | ||||
| @@ -71,15 +82,6 @@ export const mr: LanguageTranslation = { | ||||
|             cancel: 'रद्द करा', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'एकाधिक स्कीमा', | ||||
|             description: | ||||
|                 '{{schemasCount}} स्कीमा या आरेखात आहेत. सध्या दाखवत आहोत: {{formattedSchemas}}.', | ||||
|             dont_show_again: 'पुन्हा दाखवू नका', | ||||
|             change_schema: 'बदला', | ||||
|             none: 'काहीही नाही', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'कॉपी अयशस्वी', | ||||
| @@ -116,14 +118,11 @@ export const mr: LanguageTranslation = { | ||||
|         copied: 'Copied!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'स्कीमा:', | ||||
|             filter_by_schema: 'स्कीमा द्वारे फिल्टर करा', | ||||
|             search_schema: 'स्कीमा शोधा...', | ||||
|             no_schemas_found: 'कोणतेही स्कीमा सापडले नाहीत.', | ||||
|             view_all_options: 'सर्व पर्याय पहा...', | ||||
|             tables_section: { | ||||
|                 tables: 'टेबल्स', | ||||
|                 add_table: 'टेबल जोडा', | ||||
|                 add_view: 'व्ह्यू जोडा', | ||||
|                 filter: 'फिल्टर', | ||||
|                 collapse: 'सर्व संकुचित करा', | ||||
|                 // TODO: Translate | ||||
| @@ -149,16 +148,23 @@ export const mr: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'फील्ड गुणधर्म', | ||||
|                         unique: 'युनिक', | ||||
|                         auto_increment: 'ऑटो इंक्रिमेंट', | ||||
|                         comments: 'टिप्पण्या', | ||||
|                         no_comments: 'कोणत्याही टिप्पणी नाहीत', | ||||
|                         delete_field: 'फील्ड हटवा', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'अचूकता', | ||||
|                         scale: 'प्रमाण', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'इंडेक्स गुणधर्म', | ||||
|                         name: 'नाव', | ||||
|                         unique: 'युनिक', | ||||
|                         index_type: 'इंडेक्स प्रकार', | ||||
|                         delete_index: 'इंडेक्स हटवा', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -176,12 +182,15 @@ export const mr: LanguageTranslation = { | ||||
|                     description: 'सुरू करण्यासाठी एक टेबल तयार करा', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'रिलेशनशिप', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'फिल्टर', | ||||
|                 add_relationship: 'रिलेशनशिप जोडा', | ||||
|                 collapse: 'सर्व संकुचित करा', | ||||
|                 add_relationship: 'रिलेशनशिप जोडा', | ||||
|                 relationships: 'रिलेशनशिप', | ||||
|                 dependencies: 'डिपेंडेन्सि', | ||||
|                 relationship: { | ||||
|                     relationship: 'रिलेशनशिप', | ||||
|                     primary: 'प्राथमिक टेबल', | ||||
|                     foreign: 'रेफरंस टेबल', | ||||
|                     cardinality: 'कार्डिनॅलिटी', | ||||
| @@ -191,17 +200,8 @@ export const mr: LanguageTranslation = { | ||||
|                         delete_relationship: 'हटवा', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'कोणतेही रिलेशनशिप नाहीत', | ||||
|                     description: | ||||
|                         'टेबल्स कनेक्ट करण्यासाठी एक रिलेशनशिप तयार करा', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'डिपेंडेन्सि', | ||||
|                 filter: 'फिल्टर', | ||||
|                 collapse: 'सर्व संकुचित करा', | ||||
|                 dependency: { | ||||
|                     dependency: 'डिपेंडेन्सि', | ||||
|                     table: 'टेबल', | ||||
|                     dependent_table: 'डिपेंडेन्सि दृश्य', | ||||
|                     delete_dependency: 'हटवा', | ||||
| @@ -211,8 +211,8 @@ export const mr: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'कोणत्याही डिपेंडेन्सि नाहीत', | ||||
|                     description: 'सुरू करण्यासाठी एक दृश्य तयार करा', | ||||
|                     title: 'कोणतेही रिलेशनशिप नाहीत', | ||||
|                     description: 'सुरू करण्यासाठी एक रिलेशनशिप तयार करा', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -255,9 +255,12 @@ export const mr: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -272,7 +275,13 @@ export const mr: LanguageTranslation = { | ||||
|             undo: 'पूर्ववत करा', | ||||
|             redo: 'पुन्हा करा', | ||||
|             reorder_diagram: 'आरेख पुनःक्रमित करा', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: 'ओव्हरलॅपिंग टेबल्स हायलाइट करा', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -321,6 +330,12 @@ export const mr: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'रद्द करा', | ||||
|             open: 'उघडा', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'उघडा', | ||||
|                 duplicate: 'डुप्लिकेट', | ||||
|                 delete: 'आरेख हटवा', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -407,6 +422,14 @@ export const mr: LanguageTranslation = { | ||||
|             confirm: 'बदला', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'नवीन स्कीमा तयार करा', | ||||
|             description: | ||||
|                 'अजून कोणतीही स्कीमा अस्तित्वात नाही. आपल्या टेबल्स व्यवस्थित करण्यासाठी आपली पहिली स्कीमा तयार करा.', | ||||
|             create: 'तयार करा', | ||||
|             cancel: 'रद्द करा', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: 'आम्हाला सुधारण्यास मदत करा!', | ||||
|             description: | ||||
| @@ -465,6 +488,7 @@ export const mr: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'नवीन टेबल', | ||||
|             new_view: 'नवीन व्ह्यू', | ||||
|             new_relationship: 'नवीन रिलेशनशिप', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -488,6 +512,9 @@ export const mr: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'भाषा बदला', | ||||
|         }, | ||||
|  | ||||
|         on: 'चालू', | ||||
|         off: 'बंद', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const ne: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'नयाँ', | ||||
|             browse: 'ब्राउज', | ||||
|             tables: 'टेबलहरू', | ||||
|             refs: 'Refs', | ||||
|             areas: 'क्षेत्रहरू', | ||||
|             dependencies: 'निर्भरताहरू', | ||||
|             custom_types: 'कस्टम प्रकारहरू', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'फाइल', | ||||
|                 new: 'नयाँ', | ||||
|                 open: 'खोल्नुहोस्', | ||||
|             actions: { | ||||
|                 actions: 'कार्यहरू', | ||||
|                 new: 'नयाँ डायाग्राम', | ||||
|                 browse: 'ब्राउज गर्नुहोस्...', | ||||
|                 save: 'सुरक्षित गर्नुहोस्', | ||||
|                 import: 'डाटाबेस आयात गर्नुहोस्', | ||||
|                 export_sql: 'SQL निर्यात गर्नुहोस्', | ||||
|                 export_as: 'निर्यात गर्नुहोस्', | ||||
|                 delete_diagram: 'डायाग्राम हटाउनुहोस्', | ||||
|                 exit: 'बाहिर निस्कनुहोस्', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'सम्पादन', | ||||
| @@ -26,7 +34,10 @@ export const ne: LanguageTranslation = { | ||||
|                 hide_sidebar: 'साइडबार लुकाउनुहोस्', | ||||
|                 hide_cardinality: 'कार्डिन्यालिटी लुकाउनुहोस्', | ||||
|                 show_cardinality: 'कार्डिन्यालिटी देखाउनुहोस्', | ||||
|                 hide_field_attributes: 'फिल्ड विशेषताहरू लुकाउनुहोस्', | ||||
|                 show_field_attributes: 'फिल्ड विशेषताहरू देखाउनुहोस्', | ||||
|                 zoom_on_scroll: 'स्क्रोलमा जुम गर्नुहोस्', | ||||
|                 show_views: 'डाटाबेस भ्यूहरू', | ||||
|                 theme: 'थिम', | ||||
|                 show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्', | ||||
|                 hide_dependencies: 'डिपेन्डेन्सीहरू लुकाउनुहोस्', | ||||
| @@ -71,15 +82,6 @@ export const ne: LanguageTranslation = { | ||||
|             cancel: 'रद्द गर्नुहोस्', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'विविध स्कीमहरू', | ||||
|             description: | ||||
|                 '{{schemasCount}} डायाग्राममा स्कीमहरू। हालको रूपमा देखाइएको छ: {{formattedSchemas}}।', | ||||
|             dont_show_again: 'फेरि देखाउन नदिनुहोस्', | ||||
|             change_schema: 'स्कीम परिवर्तन गर्नुहोस्', | ||||
|             none: 'कुनै पनि छैन', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'प्रतिलिपि असफल', | ||||
| @@ -114,14 +116,11 @@ export const ne: LanguageTranslation = { | ||||
|         copied: 'प्रतिलिपि गरियो!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'स्कीम:', | ||||
|             filter_by_schema: 'स्कीम अनुसार फिल्टर गर्नुहोस्', | ||||
|             search_schema: 'स्कीम खोज्नुहोस्...', | ||||
|             no_schemas_found: 'कुनै स्कीमहरू फेला परेनन्', | ||||
|             view_all_options: 'सबै विकल्पहरू हेर्नुहोस्', | ||||
|             tables_section: { | ||||
|                 tables: 'तालिकाहरू', | ||||
|                 add_table: 'तालिका थप्नुहोस्', | ||||
|                 add_view: 'भ्यू थप्नुहोस्', | ||||
|                 filter: 'फिल्टर', | ||||
|                 collapse: 'सबै लुकाउनुहोस्', | ||||
|                 // TODO: Translate | ||||
| @@ -147,16 +146,23 @@ export const ne: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'क्षेत्र विशेषताहरू', | ||||
|                         unique: 'अनन्य', | ||||
|                         auto_increment: 'स्वचालित वृद्धि', | ||||
|                         comments: 'टिप्पणीहरू', | ||||
|                         no_comments: 'कुनै टिप्पणीहरू छैनन्', | ||||
|                         delete_field: 'क्षेत्र हटाउनुहोस्', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'परिशुद्धता', | ||||
|                         scale: 'स्केल', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'सूचक विशेषताहरू', | ||||
|                         name: 'नाम', | ||||
|                         unique: 'अनन्य', | ||||
|                         index_type: 'इन्डेक्स प्रकार', | ||||
|                         delete_index: 'सूचक हटाउनुहोस्', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -173,12 +179,15 @@ export const ne: LanguageTranslation = { | ||||
|                     description: 'सुरु गर्नका लागि एक तालिका बनाउनुहोस्', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'सम्बन्धहरू', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'फिल्टर', | ||||
|                 add_relationship: 'सम्बन्ध थप्नुहोस्', | ||||
|                 collapse: 'सबै लुकाउनुहोस्', | ||||
|                 add_relationship: 'सम्बन्ध थप्नुहोस्', | ||||
|                 relationships: 'सम्बन्धहरू', | ||||
|                 dependencies: 'डिपेन्डेन्सीहरू', | ||||
|                 relationship: { | ||||
|                     relationship: 'सम्बन्ध', | ||||
|                     primary: 'मुख्य तालिका', | ||||
|                     foreign: 'परिचित तालिका', | ||||
|                     cardinality: 'कार्डिन्यालिटी', | ||||
| @@ -188,16 +197,8 @@ export const ne: LanguageTranslation = { | ||||
|                         delete_relationship: 'हटाउनुहोस्', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'कुनै सम्बन्धहरू छैनन्', | ||||
|                     description: 'तालिकाहरू जोड्नका लागि एक सम्बन्ध बनाउनुहोस्', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'डिपेन्डेन्सीहरू', | ||||
|                 filter: 'फिल्टर', | ||||
|                 collapse: 'सबै लुकाउनुहोस्', | ||||
|                 dependency: { | ||||
|                     dependency: 'डिपेन्डेन्सी', | ||||
|                     table: 'तालिका', | ||||
|                     dependent_table: 'विचलित तालिका', | ||||
|                     delete_dependency: 'हटाउनुहोस्', | ||||
| @@ -207,9 +208,8 @@ export const ne: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'कुनै डिपेन्डेन्सीहरू छैनन्', | ||||
|                     description: | ||||
|                         'डिपेन्डेन्सीहरू देखाउनका लागि एक व्यू बनाउनुहोस्', | ||||
|                     title: 'कुनै सम्बन्धहरू छैनन्', | ||||
|                     description: 'सुरु गर्नका लागि एक सम्बन्ध बनाउनुहोस्', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -252,9 +252,12 @@ export const ne: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -269,8 +272,14 @@ export const ne: LanguageTranslation = { | ||||
|             undo: 'पूर्ववत', | ||||
|             redo: 'पुनः गर्नुहोस्', | ||||
|             reorder_diagram: 'पुनः क्रमबद्ध गर्नुहोस्', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: | ||||
|                 'अतिरिक्त तालिकाहरू हाइलाइट गर्नुहोस्', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -318,6 +327,12 @@ export const ne: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'रद्द गर्नुहोस्', | ||||
|             open: 'खोल्नुहोस्', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'खोल्नुहोस्', | ||||
|                 duplicate: 'डुप्लिकेट', | ||||
|                 delete: 'डायग्राम मेटाउनुहोस्', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -404,6 +419,14 @@ export const ne: LanguageTranslation = { | ||||
|             confirm: 'परिवर्तन गर्नुहोस्', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'नयाँ स्कीम सिर्जना गर्नुहोस्', | ||||
|             description: | ||||
|                 'अहिलेसम्म कुनै स्कीम अस्तित्वमा छैन। आफ्ना तालिकाहरू व्यवस्थित गर्न आफ्नो पहिलो स्कीम सिर्जना गर्नुहोस्।', | ||||
|             create: 'सिर्जना गर्नुहोस्', | ||||
|             cancel: 'रद्द गर्नुहोस्', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: 'हामीलाई अझ राम्रो हुन मदत गर्नुहोस!', | ||||
|             description: | ||||
| @@ -459,6 +482,7 @@ export const ne: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'नयाँ तालिका', | ||||
|             new_view: 'नयाँ भ्यू', | ||||
|             new_relationship: 'नयाँ सम्बन्ध', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -480,6 +504,9 @@ export const ne: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'भाषा परिवर्तन गर्नुहोस्', | ||||
|         }, | ||||
|  | ||||
|         on: 'सक्रिय', | ||||
|         off: 'निष्क्रिय', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const pt_BR: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Novo', | ||||
|             browse: 'Navegar', | ||||
|             tables: 'Tabelas', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Áreas', | ||||
|             dependencies: 'Dependências', | ||||
|             custom_types: 'Tipos Personalizados', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Arquivo', | ||||
|                 new: 'Novo', | ||||
|                 open: 'Abrir', | ||||
|             actions: { | ||||
|                 actions: 'Ações', | ||||
|                 new: 'Novo Diagrama', | ||||
|                 browse: 'Navegar...', | ||||
|                 save: 'Salvar', | ||||
|                 import: 'Importar Banco de Dados', | ||||
|                 export_sql: 'Exportar SQL', | ||||
|                 export_as: 'Exportar como', | ||||
|                 delete_diagram: 'Excluir Diagrama', | ||||
|                 exit: 'Sair', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Editar', | ||||
| @@ -26,7 +34,10 @@ export const pt_BR: LanguageTranslation = { | ||||
|                 hide_sidebar: 'Ocultar Barra Lateral', | ||||
|                 hide_cardinality: 'Ocultar Cardinalidade', | ||||
|                 show_cardinality: 'Mostrar Cardinalidade', | ||||
|                 hide_field_attributes: 'Ocultar Atributos de Campo', | ||||
|                 show_field_attributes: 'Mostrar Atributos de Campo', | ||||
|                 zoom_on_scroll: 'Zoom ao Rolar', | ||||
|                 show_views: 'Visualizações do Banco de Dados', | ||||
|                 theme: 'Tema', | ||||
|                 show_dependencies: 'Mostrar Dependências', | ||||
|                 hide_dependencies: 'Ocultar Dependências', | ||||
| @@ -71,15 +82,6 @@ export const pt_BR: LanguageTranslation = { | ||||
|             cancel: 'Cancelar', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Múltiplos Esquemas', | ||||
|             description: | ||||
|                 '{{schemasCount}} esquemas neste diagrama. Atualmente exibindo: {{formattedSchemas}}.', | ||||
|             dont_show_again: 'Não mostrar novamente', | ||||
|             change_schema: 'Alterar', | ||||
|             none: 'nenhum', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Falha na cópia', | ||||
| @@ -114,14 +116,11 @@ export const pt_BR: LanguageTranslation = { | ||||
|         copied: 'Copiado!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Esquema:', | ||||
|             filter_by_schema: 'Filtrar por esquema', | ||||
|             search_schema: 'Buscar esquema...', | ||||
|             no_schemas_found: 'Nenhum esquema encontrado.', | ||||
|             view_all_options: 'Ver todas as Opções...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tabelas', | ||||
|                 add_table: 'Adicionar Tabela', | ||||
|                 add_view: 'Adicionar Visualização', | ||||
|                 filter: 'Filtrar', | ||||
|                 collapse: 'Colapsar Todas', | ||||
|                 // TODO: Translate | ||||
| @@ -147,16 +146,23 @@ export const pt_BR: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Atributos do Campo', | ||||
|                         unique: 'Único', | ||||
|                         auto_increment: 'Incremento Automático', | ||||
|                         comments: 'Comentários', | ||||
|                         no_comments: 'Sem comentários', | ||||
|                         delete_field: 'Excluir Campo', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'Precisão', | ||||
|                         scale: 'Escala', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'Atributos do Índice', | ||||
|                         name: 'Nome', | ||||
|                         unique: 'Único', | ||||
|                         index_type: 'Tipo de Índice', | ||||
|                         delete_index: 'Excluir Índice', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -173,12 +179,15 @@ export const pt_BR: LanguageTranslation = { | ||||
|                     description: 'Crie uma tabela para começar', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Relacionamentos', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Filtrar', | ||||
|                 add_relationship: 'Adicionar Relacionamento', | ||||
|                 collapse: 'Colapsar Todas', | ||||
|                 add_relationship: 'Adicionar Relacionamento', | ||||
|                 relationships: 'Relacionamentos', | ||||
|                 dependencies: 'Dependências', | ||||
|                 relationship: { | ||||
|                     relationship: 'Relacionamento', | ||||
|                     primary: 'Tabela Primária', | ||||
|                     foreign: 'Tabela Referenciada', | ||||
|                     cardinality: 'Cardinalidade', | ||||
| @@ -188,16 +197,8 @@ export const pt_BR: LanguageTranslation = { | ||||
|                         delete_relationship: 'Excluir', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Sem relacionamentos', | ||||
|                     description: 'Crie um relacionamento para conectar tabelas', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Dependências', | ||||
|                 filter: 'Filtrar', | ||||
|                 collapse: 'Colapsar Todas', | ||||
|                 dependency: { | ||||
|                     dependency: 'Dependência', | ||||
|                     table: 'Tabela', | ||||
|                     dependent_table: 'Visualização Dependente', | ||||
|                     delete_dependency: 'Excluir', | ||||
| @@ -207,8 +208,8 @@ export const pt_BR: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Sem dependências', | ||||
|                     description: 'Crie uma visualização para começar', | ||||
|                     title: 'Sem relacionamentos', | ||||
|                     description: 'Crie um relacionamento para começar', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -251,9 +252,12 @@ export const pt_BR: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -268,7 +272,13 @@ export const pt_BR: LanguageTranslation = { | ||||
|             undo: 'Desfazer', | ||||
|             redo: 'Refazer', | ||||
|             reorder_diagram: 'Reordenar Diagrama', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: 'Destacar Tabelas Sobrepostas', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -316,6 +326,12 @@ export const pt_BR: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Cancelar', | ||||
|             open: 'Abrir', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Abrir', | ||||
|                 duplicate: 'Duplicar', | ||||
|                 delete: 'Excluir Diagrama', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -402,6 +418,14 @@ export const pt_BR: LanguageTranslation = { | ||||
|             confirm: 'Alterar', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'Criar Novo Esquema', | ||||
|             description: | ||||
|                 'Ainda não existem esquemas. Crie seu primeiro esquema para organizar suas tabelas.', | ||||
|             create: 'Criar', | ||||
|             cancel: 'Cancelar', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: 'Ajude-nos a melhorar!', | ||||
|             description: | ||||
| @@ -457,6 +481,7 @@ export const pt_BR: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Nova Tabela', | ||||
|             new_view: 'Nova Visualização', | ||||
|             new_relationship: 'Novo Relacionamento', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -479,6 +504,9 @@ export const pt_BR: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Idioma', | ||||
|         }, | ||||
|  | ||||
|         on: 'Ligado', | ||||
|         off: 'Desligado', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const ru: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Новая', | ||||
|             browse: 'Обзор', | ||||
|             tables: 'Таблицы', | ||||
|             refs: 'Ссылки', | ||||
|             areas: 'Области', | ||||
|             dependencies: 'Зависимости', | ||||
|             custom_types: 'Пользовательские типы', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Файл', | ||||
|                 new: 'Создать', | ||||
|                 open: 'Открыть', | ||||
|             actions: { | ||||
|                 actions: 'Действия', | ||||
|                 new: 'Новая диаграмма', | ||||
|                 browse: 'Обзор...', | ||||
|                 save: 'Сохранить', | ||||
|                 import: 'Импортировать базу данных', | ||||
|                 export_sql: 'Экспорт SQL', | ||||
|                 export_as: 'Экспортировать как', | ||||
|                 delete_diagram: 'Удалить диаграмму', | ||||
|                 exit: 'Выход', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Изменение', | ||||
| @@ -26,7 +34,10 @@ export const ru: LanguageTranslation = { | ||||
|                 hide_sidebar: 'Скрыть боковую панель', | ||||
|                 hide_cardinality: 'Скрыть виды связи', | ||||
|                 show_cardinality: 'Показать виды связи', | ||||
|                 show_field_attributes: 'Показать атрибуты поля', | ||||
|                 hide_field_attributes: 'Скрыть атрибуты поля', | ||||
|                 zoom_on_scroll: 'Увеличение при прокрутке', | ||||
|                 show_views: 'Представления базы данных', | ||||
|                 theme: 'Тема', | ||||
|                 show_dependencies: 'Показать зависимости', | ||||
|                 hide_dependencies: 'Скрыть зависимости', | ||||
| @@ -69,15 +80,6 @@ export const ru: LanguageTranslation = { | ||||
|             cancel: 'Отменить', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Множественные схемы', | ||||
|             description: | ||||
|                 '{{schemasCount}} схем в этой диаграмме. В данный момент отображается: {{formattedSchemas}}.', | ||||
|             dont_show_again: 'Больше не показывать', | ||||
|             change_schema: 'Изменить', | ||||
|             none: 'никто', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Ошибка копирования', | ||||
| @@ -111,14 +113,11 @@ export const ru: LanguageTranslation = { | ||||
|         show_less: 'Показать меньше', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Схема:', | ||||
|             filter_by_schema: 'Фильтр по схеме', | ||||
|             search_schema: 'Схема поиска...', | ||||
|             no_schemas_found: 'Схемы не найдены.', | ||||
|             view_all_options: 'Просмотреть все варианты...', | ||||
|             tables_section: { | ||||
|                 tables: 'Таблицы', | ||||
|                 add_table: 'Добавить таблицу', | ||||
|                 add_view: 'Добавить представление', | ||||
|                 filter: 'Фильтр', | ||||
|                 collapse: 'Свернуть все', | ||||
|                 clear: 'Очистить фильтр', | ||||
| @@ -144,15 +143,22 @@ export const ru: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Атрибуты поля', | ||||
|                         unique: 'Уникальный', | ||||
|                         auto_increment: 'Автоинкремент', | ||||
|                         comments: 'Комментарии', | ||||
|                         no_comments: 'Нет комментария', | ||||
|                         delete_field: 'Удалить поле', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         character_length: 'Макс. длина', | ||||
|                         precision: 'Точность', | ||||
|                         scale: 'Масштаб', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'Атрибуты индекса', | ||||
|                         name: 'Имя', | ||||
|                         unique: 'Уникальный', | ||||
|                         index_type: 'Тип индекса', | ||||
|                         delete_index: 'Удалить индекс', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -169,12 +175,15 @@ export const ru: LanguageTranslation = { | ||||
|                     description: 'Создайте таблицу, чтобы начать', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Отношения', | ||||
|             refs_section: { | ||||
|                 refs: 'Ссылки', | ||||
|                 filter: 'Фильтр', | ||||
|                 add_relationship: 'Добавить отношение', | ||||
|                 collapse: 'Свернуть все', | ||||
|                 add_relationship: 'Добавить отношение', | ||||
|                 relationships: 'Отношения', | ||||
|                 dependencies: 'Зависимости', | ||||
|                 relationship: { | ||||
|                     relationship: 'Отношение', | ||||
|                     primary: 'Основная таблица', | ||||
|                     foreign: 'Справочная таблица', | ||||
|                     cardinality: 'Тип множественной связи', | ||||
| @@ -184,18 +193,10 @@ export const ru: LanguageTranslation = { | ||||
|                         delete_relationship: 'Удалить', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Нет отношений', | ||||
|                     description: 'Создайте связь для соединения таблиц', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Зависимости', | ||||
|                 filter: 'Фильтр', | ||||
|                 collapse: 'Свернуть все', | ||||
|                 dependency: { | ||||
|                     table: 'Стол', | ||||
|                     dependent_table: 'Зависимый вид', | ||||
|                     dependency: 'Зависимость', | ||||
|                     table: 'Таблица', | ||||
|                     dependent_table: 'Зависимое представление', | ||||
|                     delete_dependency: 'Удалить', | ||||
|                     dependency_actions: { | ||||
|                         title: 'Действия', | ||||
| @@ -203,8 +204,8 @@ export const ru: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Нет зависимостей', | ||||
|                     description: 'Создайте представление, чтобы начать', | ||||
|                     title: 'Нет отношений', | ||||
|                     description: 'Создайте отношение, чтобы начать', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -248,9 +249,12 @@ export const ru: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -265,7 +269,13 @@ export const ru: LanguageTranslation = { | ||||
|             undo: 'Отменить', | ||||
|             redo: 'Вернуть', | ||||
|             reorder_diagram: 'Переупорядочить диаграмму', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: 'Выделение перекрывающихся таблиц', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -313,6 +323,12 @@ export const ru: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Отмена', | ||||
|             open: 'Открыть', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Открыть', | ||||
|                 duplicate: 'Дублировать', | ||||
|                 delete: 'Удалить диаграмму', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -399,6 +415,14 @@ export const ru: LanguageTranslation = { | ||||
|             confirm: 'Изменить', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'Создать новую схему', | ||||
|             description: | ||||
|                 'Схемы еще не существуют. Создайте вашу первую схему, чтобы организовать таблицы.', | ||||
|             create: 'Создать', | ||||
|             cancel: 'Отменить', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: 'Помогите нам стать лучше!', | ||||
|             description: | ||||
| @@ -453,6 +477,7 @@ export const ru: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Создать таблицу', | ||||
|             new_view: 'Новое представление', | ||||
|             new_relationship: 'Создать отношение', | ||||
|             new_area: 'Новая область', | ||||
|         }, | ||||
| @@ -474,6 +499,9 @@ export const ru: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Сменить язык', | ||||
|         }, | ||||
|  | ||||
|         on: 'Вкл', | ||||
|         off: 'Выкл', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const te: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'కొత్తది', | ||||
|             browse: 'బ్రాఉజ్', | ||||
|             tables: 'టేబల్లు', | ||||
|             refs: 'సంబంధాలు', | ||||
|             areas: 'ప్రదేశాలు', | ||||
|             dependencies: 'ఆధారతలు', | ||||
|             custom_types: 'కస్టమ్ టైప్స్', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'ఫైల్', | ||||
|                 new: 'కొత్తది', | ||||
|                 open: 'తెరవు', | ||||
|             actions: { | ||||
|                 actions: 'చర్యలు', | ||||
|                 new: 'కొత్త డైగ్రాం', | ||||
|                 browse: 'బ్రాఉజ్ చేయండి...', | ||||
|                 save: 'సేవ్', | ||||
|                 import: 'డేటాబేస్ను దిగుమతి చేసుకోండి', | ||||
|                 export_sql: 'SQL ఎగుమతి', | ||||
|                 export_as: 'వగా ఎగుమతి చేయండి', | ||||
|                 delete_diagram: 'చిత్రాన్ని తొలగించండి', | ||||
|                 exit: 'నిష్క్రమించు', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'సవరించు', | ||||
| @@ -26,7 +34,10 @@ export const te: LanguageTranslation = { | ||||
|                 hide_sidebar: 'సైడ్బార్ దాచండి', | ||||
|                 hide_cardinality: 'కార్డినాలిటీని దాచండి', | ||||
|                 show_cardinality: 'కార్డినాలిటీని చూపించండి', | ||||
|                 show_field_attributes: 'ఫీల్డ్ గుణాలను చూపించు', | ||||
|                 hide_field_attributes: 'ఫీల్డ్ గుణాలను దాచండి', | ||||
|                 zoom_on_scroll: 'స్క్రోల్పై జూమ్', | ||||
|                 show_views: 'డేటాబేస్ వ్యూలు', | ||||
|                 theme: 'థీమ్', | ||||
|                 show_dependencies: 'ఆధారాలు చూపించండి', | ||||
|                 hide_dependencies: 'ఆధారాలను దాచండి', | ||||
| @@ -71,15 +82,6 @@ export const te: LanguageTranslation = { | ||||
|             cancel: 'రద్దు', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'బహుళ స్కీమాలు', | ||||
|             description: | ||||
|                 '{{schemasCount}} స్కీమాలు ఈ చిత్రంలో ఉన్నాయి. ప్రస్తుత స్కీమాలు: {{formattedSchemas}}.', | ||||
|             dont_show_again: 'మరలా చూపించవద్దు', | ||||
|             change_schema: 'మార్చు', | ||||
|             none: 'ఎదరికాదు', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'కాపీ విఫలమైంది', | ||||
| @@ -114,14 +116,11 @@ export const te: LanguageTranslation = { | ||||
|         copied: 'కాపీ చేయబడింది!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'స్కీమా:', | ||||
|             filter_by_schema: 'స్కీమా ద్వారా ఫిల్టర్ చేయండి', | ||||
|             search_schema: 'స్కీమా కోసం శోధించండి...', | ||||
|             no_schemas_found: 'ఏ స్కీమాలు కూడా కనుగొనబడలేదు.', | ||||
|             view_all_options: 'అన్ని ఎంపికలను చూడండి...', | ||||
|             tables_section: { | ||||
|                 tables: 'పట్టికలు', | ||||
|                 add_table: 'పట్టికను జోడించు', | ||||
|                 add_view: 'వ్యూ జోడించండి', | ||||
|                 filter: 'ఫిల్టర్', | ||||
|                 collapse: 'అన్ని కూల్ చేయి', | ||||
|                 // TODO: Translate | ||||
| @@ -147,16 +146,23 @@ export const te: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'ఫీల్డ్ గుణాలు', | ||||
|                         unique: 'అద్వితీయ', | ||||
|                         auto_increment: 'ఆటో ఇంక్రిమెంట్', | ||||
|                         comments: 'వ్యాఖ్యలు', | ||||
|                         no_comments: 'వ్యాఖ్యలు లేవు', | ||||
|                         delete_field: 'ఫీల్డ్ తొలగించు', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'సూక్ష్మత', | ||||
|                         scale: 'స్కేల్', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'ఇండెక్స్ గుణాలు', | ||||
|                         name: 'పేరు', | ||||
|                         unique: 'అద్వితీయ', | ||||
|                         index_type: 'ఇండెక్స్ రకం', | ||||
|                         delete_index: 'ఇండెక్స్ తొలగించు', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -174,12 +180,15 @@ export const te: LanguageTranslation = { | ||||
|                     description: 'ప్రారంభించడానికి ఒక పట్టిక సృష్టించండి', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'సంబంధాలు', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'ఫిల్టర్', | ||||
|                 add_relationship: 'సంబంధం జోడించు', | ||||
|                 collapse: 'అన్ని కూల్ చేయి', | ||||
|                 add_relationship: 'సంబంధం జోడించు', | ||||
|                 relationships: 'సంబంధాలు', | ||||
|                 dependencies: 'ఆధారాలు', | ||||
|                 relationship: { | ||||
|                     relationship: 'సంబంధం', | ||||
|                     primary: 'ప్రాథమిక పట్టిక', | ||||
|                     foreign: 'సూచించబడిన పట్టిక', | ||||
|                     cardinality: 'కార్డినాలిటీ', | ||||
| @@ -189,16 +198,8 @@ export const te: LanguageTranslation = { | ||||
|                         delete_relationship: 'సంబంధం తొలగించు', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'సంబంధాలు లేవు', | ||||
|                     description: 'పట్టికలను అనుసంధించడానికి సంబంధం సృష్టించండి', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'ఆధారాలు', | ||||
|                 filter: 'ఫిల్టర్', | ||||
|                 collapse: 'అన్ని కూల్ చేయి', | ||||
|                 dependency: { | ||||
|                     dependency: 'ఆధారం', | ||||
|                     table: 'పట్టిక', | ||||
|                     dependent_table: 'ఆధారిత వీక్షణ', | ||||
|                     delete_dependency: 'ఆధారాన్ని తొలగించు', | ||||
| @@ -208,8 +209,8 @@ export const te: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'ఆధారాలు లేవు', | ||||
|                     description: 'ప్రారంభించడానికి ఒక వీక్షణ సృష్టించండి', | ||||
|                     title: 'సంబంధాలు లేవు', | ||||
|                     description: 'ప్రారంభించడానికి ఒక సంబంధం సృష్టించండి', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -252,9 +253,12 @@ export const te: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -269,7 +273,13 @@ export const te: LanguageTranslation = { | ||||
|             undo: 'తిరిగి చేయు', | ||||
|             redo: 'మరలా చేయు', | ||||
|             reorder_diagram: 'చిత్రాన్ని పునఃసరిచేయండి', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: 'అవకాశించు పట్టికలను హైలైట్ చేయండి', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -317,6 +327,12 @@ export const te: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'రద్దు', | ||||
|             open: 'తెరవు', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'తెరవు', | ||||
|                 duplicate: 'నకలు', | ||||
|                 delete: 'డైగ్రామ్ తొలగించు', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -403,6 +419,14 @@ export const te: LanguageTranslation = { | ||||
|             confirm: 'మార్చు', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'కొత్త స్కీమా సృష్టించండి', | ||||
|             description: | ||||
|                 'ఇంకా ఏ స్కీమాలు లేవు. మీ పట్టికలను వ్యవస్థీకరించడానికి మీ మొదటి స్కీమాను సృష్టించండి.', | ||||
|             create: 'సృష్టించు', | ||||
|             cancel: 'రద్దు', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: 'మా సహాయంతో మెరుగుపరచండి!', | ||||
|             description: | ||||
| @@ -461,6 +485,7 @@ export const te: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'కొత్త పట్టిక', | ||||
|             new_view: 'కొత్త వ్యూ', | ||||
|             new_relationship: 'కొత్త సంబంధం', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -484,6 +509,9 @@ export const te: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'భాష మార్చు', | ||||
|         }, | ||||
|  | ||||
|         on: 'ఆన్', | ||||
|         off: 'ఆఫ్', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const tr: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Yeni', | ||||
|             browse: 'Gözat', | ||||
|             tables: 'Tablolar', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Alanlar', | ||||
|             dependencies: 'Bağımlılıklar', | ||||
|             custom_types: 'Özel Tipler', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Dosya', | ||||
|                 new: 'Yeni', | ||||
|                 open: 'Aç', | ||||
|             actions: { | ||||
|                 actions: 'Eylemler', | ||||
|                 new: 'Yeni Diyagram', | ||||
|                 browse: 'Gözat...', | ||||
|                 save: 'Kaydet', | ||||
|                 import: 'Veritabanı İçe Aktar', | ||||
|                 export_sql: 'SQL Olarak Dışa Aktar', | ||||
|                 export_as: 'Olarak Dışa Aktar', | ||||
|                 delete_diagram: 'Diyagramı Sil', | ||||
|                 exit: 'Çıkış', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Düzenle', | ||||
| @@ -26,7 +34,10 @@ export const tr: LanguageTranslation = { | ||||
|                 hide_sidebar: 'Kenar Çubuğunu Gizle', | ||||
|                 hide_cardinality: 'Kardinaliteyi Gizle', | ||||
|                 show_cardinality: 'Kardinaliteyi Göster', | ||||
|                 show_field_attributes: 'Alan Özelliklerini Göster', | ||||
|                 hide_field_attributes: 'Alan Özelliklerini Gizle', | ||||
|                 zoom_on_scroll: 'Kaydırarak Yakınlaştır', | ||||
|                 show_views: 'Veritabanı Görünümleri', | ||||
|                 theme: 'Tema', | ||||
|                 show_dependencies: 'Bağımlılıkları Göster', | ||||
|                 hide_dependencies: 'Bağımlılıkları Gizle', | ||||
| @@ -71,15 +82,6 @@ export const tr: LanguageTranslation = { | ||||
|             cancel: 'İptal', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Birden Fazla Şema', | ||||
|             description: | ||||
|                 'Bu diyagramda {{schemasCount}} şema var. Şu anda görüntülenen: {{formattedSchemas}}.', | ||||
|             dont_show_again: 'Tekrar gösterme', | ||||
|             change_schema: 'Değiştir', | ||||
|             none: 'yok', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Kopyalama başarısız', | ||||
| @@ -113,14 +115,11 @@ export const tr: LanguageTranslation = { | ||||
|         copy_to_clipboard: 'Panoya Kopyala', | ||||
|         copied: 'Kopyalandı!', | ||||
|         side_panel: { | ||||
|             schema: 'Şema:', | ||||
|             filter_by_schema: 'Şemaya Göre Filtrele', | ||||
|             search_schema: 'Şema ara...', | ||||
|             no_schemas_found: 'Şema bulunamadı.', | ||||
|             view_all_options: 'Tüm Seçenekleri Gör...', | ||||
|             tables_section: { | ||||
|                 tables: 'Tablolar', | ||||
|                 add_table: 'Tablo Ekle', | ||||
|                 add_view: 'Görünüm Ekle', | ||||
|                 filter: 'Filtrele', | ||||
|                 collapse: 'Hepsini Daralt', | ||||
|                 // TODO: Translate | ||||
| @@ -146,16 +145,23 @@ export const tr: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Alan Özellikleri', | ||||
|                         unique: 'Tekil', | ||||
|                         auto_increment: 'Otomatik Artış', | ||||
|                         comments: 'Yorumlar', | ||||
|                         no_comments: 'Yorum yok', | ||||
|                         delete_field: 'Alanı Sil', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'Hassasiyet', | ||||
|                         scale: 'Ölçek', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'İndeks Özellikleri', | ||||
|                         name: 'Ad', | ||||
|                         unique: 'Tekil', | ||||
|                         index_type: 'İndeks Türü', | ||||
|                         delete_index: 'İndeksi Sil', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -173,12 +179,15 @@ export const tr: LanguageTranslation = { | ||||
|                     description: 'Başlamak için bir tablo oluşturun', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'İlişkiler', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Filtrele', | ||||
|                 add_relationship: 'İlişki Ekle', | ||||
|                 collapse: 'Hepsini Daralt', | ||||
|                 add_relationship: 'İlişki Ekle', | ||||
|                 relationships: 'İlişkiler', | ||||
|                 dependencies: 'Bağımlılıklar', | ||||
|                 relationship: { | ||||
|                     relationship: 'İlişki', | ||||
|                     primary: 'Birincil Tablo', | ||||
|                     foreign: 'Referans Tablo', | ||||
|                     cardinality: 'Kardinalite', | ||||
| @@ -188,16 +197,8 @@ export const tr: LanguageTranslation = { | ||||
|                         delete_relationship: 'Sil', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'İlişki yok', | ||||
|                     description: 'Tabloları bağlamak için bir ilişki oluşturun', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Bağımlılıklar', | ||||
|                 filter: 'Filtrele', | ||||
|                 collapse: 'Hepsini Daralt', | ||||
|                 dependency: { | ||||
|                     dependency: 'Bağımlılık', | ||||
|                     table: 'Tablo', | ||||
|                     dependent_table: 'Bağımlı Görünüm', | ||||
|                     delete_dependency: 'Sil', | ||||
| @@ -207,8 +208,8 @@ export const tr: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Bağımlılık yok', | ||||
|                     description: 'Başlamak için bir görünüm oluşturun', | ||||
|                     title: 'İlişki yok', | ||||
|                     description: 'Başlamak için bir ilişki oluşturun', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -251,9 +252,12 @@ export const tr: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -267,7 +271,13 @@ export const tr: LanguageTranslation = { | ||||
|             undo: 'Geri Al', | ||||
|             redo: 'Yinele', | ||||
|             reorder_diagram: 'Diyagramı Yeniden Sırala', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: 'Çakışan Tabloları Vurgula', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|         new_diagram_dialog: { | ||||
|             database_selection: { | ||||
| @@ -312,6 +322,12 @@ export const tr: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'İptal', | ||||
|             open: 'Aç', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Aç', | ||||
|                 duplicate: 'Kopyala', | ||||
|                 delete: 'Diyagramı Sil', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -392,6 +408,14 @@ export const tr: LanguageTranslation = { | ||||
|             cancel: 'İptal', | ||||
|             confirm: 'Değiştir', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'Yeni Şema Oluştur', | ||||
|             description: | ||||
|                 'Henüz hiç şema mevcut değil. Tablolarınızı düzenlemek için ilk şemanızı oluşturun.', | ||||
|             create: 'Oluştur', | ||||
|             cancel: 'İptal', | ||||
|         }, | ||||
|         star_us_dialog: { | ||||
|             title: 'Bize yardım et!', | ||||
|             description: | ||||
| @@ -446,6 +470,7 @@ export const tr: LanguageTranslation = { | ||||
|         }, | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Yeni Tablo', | ||||
|             new_view: 'Yeni Görünüm', | ||||
|             new_relationship: 'Yeni İlişki', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -468,6 +493,9 @@ export const tr: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Dil', | ||||
|         }, | ||||
|  | ||||
|         on: 'Açık', | ||||
|         off: 'Kapalı', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const uk: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Нова', | ||||
|             browse: 'Огляд', | ||||
|             tables: 'Таблиці', | ||||
|             refs: 'Зв’язки', | ||||
|             areas: 'Області', | ||||
|             dependencies: 'Залежності', | ||||
|             custom_types: 'Користувацькі типи', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Файл', | ||||
|                 new: 'Новий', | ||||
|                 open: 'Відкрити', | ||||
|             actions: { | ||||
|                 actions: 'Дії', | ||||
|                 new: 'Нова діаграма', | ||||
|                 browse: 'Огляд...', | ||||
|                 save: 'Зберегти', | ||||
|                 import: 'Імпорт бази даних', | ||||
|                 export_sql: 'Експорт SQL', | ||||
|                 export_as: 'Експортувати як', | ||||
|                 delete_diagram: 'Видалити діаграму', | ||||
|                 exit: 'Вийти', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Редагувати', | ||||
| @@ -26,7 +34,10 @@ export const uk: LanguageTranslation = { | ||||
|                 hide_sidebar: 'Приховати бічну панель', | ||||
|                 hide_cardinality: 'Приховати потужність', | ||||
|                 show_cardinality: 'Показати кардинальність', | ||||
|                 show_field_attributes: 'Показати атрибути полів', | ||||
|                 hide_field_attributes: 'Приховати атрибути полів', | ||||
|                 zoom_on_scroll: 'Масштабувати прокручуванням', | ||||
|                 show_views: 'Представлення бази даних', | ||||
|                 theme: 'Тема', | ||||
|                 show_dependencies: 'Показати залежності', | ||||
|                 hide_dependencies: 'Приховати залежності', | ||||
| @@ -69,15 +80,6 @@ export const uk: LanguageTranslation = { | ||||
|             cancel: 'Скасувати', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Кілька схем', | ||||
|             description: | ||||
|                 '{{schemasCount}} схеми на цій діаграмі. Зараз відображається: {{formattedSchemas}}.', | ||||
|             dont_show_again: 'Більше не показувати', | ||||
|             change_schema: 'Зміна', | ||||
|             none: 'немає', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Помилка копіювання', | ||||
| @@ -112,14 +114,11 @@ export const uk: LanguageTranslation = { | ||||
|         copied: 'Скопійовано!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Схема:', | ||||
|             filter_by_schema: 'Фільтрувати за схемою', | ||||
|             search_schema: 'Пошук схеми…', | ||||
|             no_schemas_found: 'Схеми не знайдено.', | ||||
|             view_all_options: 'Переглянути всі параметри…', | ||||
|             tables_section: { | ||||
|                 tables: 'Таблиці', | ||||
|                 add_table: 'Додати таблицю', | ||||
|                 add_view: 'Додати представлення', | ||||
|                 filter: 'Фільтр', | ||||
|                 collapse: 'Згорнути все', | ||||
|                 // TODO: Translate | ||||
| @@ -145,16 +144,23 @@ export const uk: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Атрибути полів', | ||||
|                         unique: 'Унікальне', | ||||
|                         auto_increment: 'Автоінкремент', | ||||
|                         comments: 'Коментарі', | ||||
|                         no_comments: 'Немає коментарів', | ||||
|                         delete_field: 'Видалити поле', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'Точність', | ||||
|                         scale: 'Масштаб', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'Атрибути індексу', | ||||
|                         name: 'Назва індекса', | ||||
|                         unique: 'Унікальний', | ||||
|                         index_type: 'Тип індексу', | ||||
|                         delete_index: 'Видалити індекс', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -171,12 +177,15 @@ export const uk: LanguageTranslation = { | ||||
|                     description: 'Щоб почати, створіть таблицю', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Звʼязки', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Фільтр', | ||||
|                 add_relationship: 'Додати звʼязок', | ||||
|                 collapse: 'Згорнути все', | ||||
|                 add_relationship: 'Додати звʼязок', | ||||
|                 relationships: 'Звʼязки', | ||||
|                 dependencies: 'Залежності', | ||||
|                 relationship: { | ||||
|                     relationship: 'Звʼязок', | ||||
|                     primary: 'Первинна таблиця', | ||||
|                     foreign: 'Посилання на таблицю', | ||||
|                     cardinality: 'Звʼязок', | ||||
| @@ -186,16 +195,8 @@ export const uk: LanguageTranslation = { | ||||
|                         delete_relationship: 'Видалити', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Звʼязків немає', | ||||
|                     description: 'Створіть звʼязок для зʼєднання таблиць', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Залежності', | ||||
|                 filter: 'Фільтр', | ||||
|                 collapse: 'Згорнути все', | ||||
|                 dependency: { | ||||
|                     dependency: 'Залежність', | ||||
|                     table: 'Таблиця', | ||||
|                     dependent_table: 'Залежне подання', | ||||
|                     delete_dependency: 'Видалити', | ||||
| @@ -205,8 +206,8 @@ export const uk: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Жодних залежностей', | ||||
|                     description: 'Створіть подання, щоб почати', | ||||
|                     title: 'Жодних зв’язків', | ||||
|                     description: 'Створіть зв’язок, щоб почати', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -249,9 +250,12 @@ export const uk: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -266,7 +270,13 @@ export const uk: LanguageTranslation = { | ||||
|             undo: 'Скасувати', | ||||
|             redo: 'Повторити', | ||||
|             reorder_diagram: 'Перевпорядкувати діаграму', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: 'Показати таблиці, що перекриваються', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -314,6 +324,12 @@ export const uk: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Скасувати', | ||||
|             open: 'Відкрити', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Відкрити', | ||||
|                 duplicate: 'Дублювати', | ||||
|                 delete: 'Видалити діаграму', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -400,6 +416,14 @@ export const uk: LanguageTranslation = { | ||||
|             confirm: 'Змінити', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'Створити нову схему', | ||||
|             description: | ||||
|                 'Поки що не існує жодної схеми. Створіть свою першу схему, щоб організувати ваші таблиці.', | ||||
|             create: 'Створити', | ||||
|             cancel: 'Скасувати', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: 'Допоможіть нам покращитися!', | ||||
|             description: 'Поставне на зірку на GitHub? Це лише один клік!', | ||||
| @@ -452,6 +476,7 @@ export const uk: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Нова таблиця', | ||||
|             new_view: 'Нове представлення', | ||||
|             new_relationship: 'Новий звʼязок', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -473,6 +498,9 @@ export const uk: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Мова', | ||||
|         }, | ||||
|  | ||||
|         on: 'Увімк', | ||||
|         off: 'Вимк', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const vi: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: 'Mới', | ||||
|             browse: 'Duyệt', | ||||
|             tables: 'Bảng', | ||||
|             refs: 'Refs', | ||||
|             areas: 'Khu vực', | ||||
|             dependencies: 'Phụ thuộc', | ||||
|             custom_types: 'Kiểu tùy chỉnh', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: 'Tệp', | ||||
|                 new: 'Tạo mới', | ||||
|                 open: 'Mở', | ||||
|             actions: { | ||||
|                 actions: 'Hành động', | ||||
|                 new: 'Sơ đồ mới', | ||||
|                 browse: 'Duyệt...', | ||||
|                 save: 'Lưu', | ||||
|                 import: 'Nhập cơ sở dữ liệu', | ||||
|                 export_sql: 'Xuất SQL', | ||||
|                 export_as: 'Xuất thành', | ||||
|                 delete_diagram: 'Xóa sơ đồ', | ||||
|                 exit: 'Thoát', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: 'Sửa', | ||||
| @@ -26,7 +34,10 @@ export const vi: LanguageTranslation = { | ||||
|                 hide_sidebar: 'Ẩn thanh bên', | ||||
|                 hide_cardinality: 'Ẩn số lượng', | ||||
|                 show_cardinality: 'Hiển thị số lượng', | ||||
|                 show_field_attributes: 'Hiển thị thuộc tính trường', | ||||
|                 hide_field_attributes: 'Ẩn thuộc tính trường', | ||||
|                 zoom_on_scroll: 'Thu phóng khi cuộn', | ||||
|                 show_views: 'Chế độ xem Cơ sở dữ liệu', | ||||
|                 theme: 'Chủ đề', | ||||
|                 show_dependencies: 'Hiển thị các phụ thuộc', | ||||
|                 hide_dependencies: 'Ẩn các phụ thuộc', | ||||
| @@ -70,15 +81,6 @@ export const vi: LanguageTranslation = { | ||||
|             cancel: 'Hủy', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: 'Có nhiều lược đồ', | ||||
|             description: | ||||
|                 'Có {{schemasCount}} lược đồ trong sơ đồ này. Hiện đang hiển thị: {{formattedSchemas}}.', | ||||
|             dont_show_again: 'Không hiển thị lại', | ||||
|             change_schema: 'Thay đổi', | ||||
|             none: 'không có', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: 'Sao chép thất bại', | ||||
| @@ -113,14 +115,11 @@ export const vi: LanguageTranslation = { | ||||
|         copied: 'Đã sao chép!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Lược đồ:', | ||||
|             filter_by_schema: 'Lọc bởi lược đồ', | ||||
|             search_schema: 'Tìm kiếm lược đồ...', | ||||
|             no_schemas_found: 'Không tìm thấy lược đồ.', | ||||
|             view_all_options: 'Xem tất cả tùy chọn...', | ||||
|             tables_section: { | ||||
|                 tables: 'Bảng', | ||||
|                 add_table: 'Thêm bảng', | ||||
|                 add_view: 'Thêm Chế độ xem', | ||||
|                 filter: 'Lọc', | ||||
|                 collapse: 'Thu gọn tất cả', | ||||
|                 // TODO: Translate | ||||
| @@ -146,16 +145,23 @@ export const vi: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: 'Thuộc tính trường', | ||||
|                         unique: 'Giá trị duy nhất', | ||||
|                         auto_increment: 'Tự động tăng', | ||||
|                         comments: 'Bình luận', | ||||
|                         no_comments: 'Không có bình luận', | ||||
|                         delete_field: 'Xóa trường', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: 'Độ chính xác', | ||||
|                         scale: 'Tỷ lệ', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: 'Thuộc tính chỉ mục', | ||||
|                         name: 'Tên', | ||||
|                         unique: 'Giá trị duy nhất', | ||||
|                         index_type: 'Loại chỉ mục', | ||||
|                         delete_index: 'Xóa chỉ mục', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -172,12 +178,15 @@ export const vi: LanguageTranslation = { | ||||
|                     description: 'Tạo một bảng để bắt đầu', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: 'Quan hệ', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: 'Lọc', | ||||
|                 add_relationship: 'Thêm quan hệ', | ||||
|                 collapse: 'Thu gọn tất cả', | ||||
|                 add_relationship: 'Thêm quan hệ', | ||||
|                 relationships: 'Quan hệ', | ||||
|                 dependencies: 'Phụ thuộc', | ||||
|                 relationship: { | ||||
|                     relationship: 'Quan hệ', | ||||
|                     primary: 'Bảng khóa chính', | ||||
|                     foreign: 'Bảng khóa ngoại', | ||||
|                     cardinality: 'Quan hệ', | ||||
| @@ -187,16 +196,8 @@ export const vi: LanguageTranslation = { | ||||
|                         delete_relationship: 'Xóa', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Không có quan hệ', | ||||
|                     description: 'Tạo quan hệ để kết nối các bảng', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: 'Phụ thuộc', | ||||
|                 filter: 'Lọc', | ||||
|                 collapse: 'Thu gọn tất cả', | ||||
|                 dependency: { | ||||
|                     dependency: 'Phụ thuộc', | ||||
|                     table: 'Bảng', | ||||
|                     dependent_table: 'Bảng xem phụ thuộc', | ||||
|                     delete_dependency: 'Xóa', | ||||
| @@ -206,8 +207,8 @@ export const vi: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: 'Không có phụ thuộc', | ||||
|                     description: 'Tạo bảng xem phụ thuộc để bắt đầu', | ||||
|                     title: 'Không có quan hệ', | ||||
|                     description: 'Tạo một quan hệ để bắt đầu', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -250,9 +251,12 @@ export const vi: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -267,7 +271,13 @@ export const vi: LanguageTranslation = { | ||||
|             undo: 'Hoàn tác', | ||||
|             redo: 'Làm lại', | ||||
|             reorder_diagram: 'Sắp xếp lại sơ đồ', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: 'Làm nổi bật các bảng chồng chéo', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -314,6 +324,12 @@ export const vi: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: 'Hủy', | ||||
|             open: 'Mở', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: 'Mở', | ||||
|                 duplicate: 'Nhân bản', | ||||
|                 delete: 'Xóa sơ đồ', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -399,6 +415,14 @@ export const vi: LanguageTranslation = { | ||||
|             confirm: 'Xác nhận', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: 'Tạo lược đồ mới', | ||||
|             description: | ||||
|                 'Chưa có lược đồ nào. Tạo lược đồ đầu tiên của bạn để tổ chức các bảng.', | ||||
|             create: 'Tạo', | ||||
|             cancel: 'Hủy', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: 'Hãy giúp chúng tôi cải thiện!', | ||||
|             description: | ||||
| @@ -453,6 +477,7 @@ export const vi: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: 'Tạo bảng mới', | ||||
|             new_view: 'Chế độ xem Mới', | ||||
|             new_relationship: 'Tạo quan hệ mới', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -474,6 +499,9 @@ export const vi: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: 'Ngôn ngữ', | ||||
|         }, | ||||
|  | ||||
|         on: 'Bật', | ||||
|         off: 'Tắt', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const zh_CN: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: '新建', | ||||
|             browse: '浏览', | ||||
|             tables: '表', | ||||
|             refs: '引用', | ||||
|             areas: '区域', | ||||
|             dependencies: '依赖关系', | ||||
|             custom_types: '自定义类型', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: '文件', | ||||
|                 new: '新建', | ||||
|                 open: '打开', | ||||
|             actions: { | ||||
|                 actions: '操作', | ||||
|                 new: '新建关系图', | ||||
|                 browse: '浏览...', | ||||
|                 save: '保存', | ||||
|                 import: '导入数据库', | ||||
|                 export_sql: '导出 SQL 语句', | ||||
|                 export_as: '导出为', | ||||
|                 delete_diagram: '删除关系图', | ||||
|                 exit: '退出', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: '编辑', | ||||
| @@ -26,7 +34,10 @@ export const zh_CN: LanguageTranslation = { | ||||
|                 hide_sidebar: '隐藏侧边栏', | ||||
|                 hide_cardinality: '隐藏基数', | ||||
|                 show_cardinality: '展示基数', | ||||
|                 show_field_attributes: '展示字段属性', | ||||
|                 hide_field_attributes: '隐藏字段属性', | ||||
|                 zoom_on_scroll: '滚动缩放', | ||||
|                 show_views: '数据库视图', | ||||
|                 theme: '主题', | ||||
|                 show_dependencies: '展示依赖', | ||||
|                 hide_dependencies: '隐藏依赖', | ||||
| @@ -67,15 +78,6 @@ export const zh_CN: LanguageTranslation = { | ||||
|             cancel: '取消', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: '多个模式', | ||||
|             description: | ||||
|                 '此关系图中有 {{schemasCount}} 个模式,当前显示:{{formattedSchemas}}。', | ||||
|             dont_show_again: '不再展示', | ||||
|             change_schema: '更改', | ||||
|             none: '无', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: '复制失败', | ||||
| @@ -110,14 +112,11 @@ export const zh_CN: LanguageTranslation = { | ||||
|         copied: '复制了!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: '模式:', | ||||
|             filter_by_schema: '按模式筛选', | ||||
|             search_schema: '搜索模式...', | ||||
|             no_schemas_found: '未找到模式。', | ||||
|             view_all_options: '查看所有选项...', | ||||
|             tables_section: { | ||||
|                 tables: '表', | ||||
|                 add_table: '添加表', | ||||
|                 add_view: '添加视图', | ||||
|                 filter: '筛选', | ||||
|                 collapse: '全部折叠', | ||||
|                 // TODO: Translate | ||||
| @@ -143,16 +142,23 @@ export const zh_CN: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: '字段属性', | ||||
|                         unique: '唯一', | ||||
|                         auto_increment: '自动递增', | ||||
|                         comments: '注释', | ||||
|                         no_comments: '空', | ||||
|                         delete_field: '删除字段', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: '精度', | ||||
|                         scale: '小数位', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: '索引属性', | ||||
|                         name: '名称', | ||||
|                         unique: '唯一', | ||||
|                         index_type: '索引类型', | ||||
|                         delete_index: '删除索引', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -169,12 +175,15 @@ export const zh_CN: LanguageTranslation = { | ||||
|                     description: '新建表以开始', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: '关系', | ||||
|             refs_section: { | ||||
|                 refs: '引用', | ||||
|                 filter: '筛选', | ||||
|                 add_relationship: '添加关系', | ||||
|                 collapse: '全部折叠', | ||||
|                 add_relationship: '添加关系', | ||||
|                 relationships: '关系', | ||||
|                 dependencies: '依赖关系', | ||||
|                 relationship: { | ||||
|                     relationship: '关系', | ||||
|                     primary: '主表', | ||||
|                     foreign: '被引用表', | ||||
|                     cardinality: '基数', | ||||
| @@ -184,16 +193,8 @@ export const zh_CN: LanguageTranslation = { | ||||
|                         delete_relationship: '删除', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: '无关系', | ||||
|                     description: '创建关系以连接表', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: '依赖关系', | ||||
|                 filter: '筛选', | ||||
|                 collapse: '全部折叠', | ||||
|                 dependency: { | ||||
|                     dependency: '依赖', | ||||
|                     table: '表', | ||||
|                     dependent_table: '依赖视图', | ||||
|                     delete_dependency: '删除', | ||||
| @@ -203,8 +204,8 @@ export const zh_CN: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: '无依赖', | ||||
|                     description: '创建视图以开始', | ||||
|                     title: '无关系', | ||||
|                     description: '创建关系以开始', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -247,9 +248,12 @@ export const zh_CN: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -264,7 +268,13 @@ export const zh_CN: LanguageTranslation = { | ||||
|             undo: '撤销', | ||||
|             redo: '重做', | ||||
|             reorder_diagram: '重新排列关系图', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: '突出显示重叠的表', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -311,6 +321,12 @@ export const zh_CN: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: '取消', | ||||
|             open: '打开', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: '打开', | ||||
|                 duplicate: '复制', | ||||
|                 delete: '删除图表', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -395,6 +411,13 @@ export const zh_CN: LanguageTranslation = { | ||||
|             confirm: '更改', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: '创建新模式', | ||||
|             description: '尚未存在任何模式。创建您的第一个模式来组织您的表。', | ||||
|             create: '创建', | ||||
|             cancel: '取消', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: '帮助我们改进!', | ||||
|             description: '您想在 GitHub 上为我们加注星标吗?只需点击一下即可!', | ||||
| @@ -449,6 +472,7 @@ export const zh_CN: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: '新建表', | ||||
|             new_view: '新建视图', | ||||
|             new_relationship: '新建关系', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -470,6 +494,9 @@ export const zh_CN: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: '语言', | ||||
|         }, | ||||
|  | ||||
|         on: '开启', | ||||
|         off: '关闭', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,17 +2,25 @@ import type { LanguageMetadata, LanguageTranslation } from '../types'; | ||||
|  | ||||
| export const zh_TW: LanguageTranslation = { | ||||
|     translation: { | ||||
|         editor_sidebar: { | ||||
|             new_diagram: '新建', | ||||
|             browse: '瀏覽', | ||||
|             tables: '表格', | ||||
|             refs: 'Refs', | ||||
|             areas: '區域', | ||||
|             dependencies: '相依性', | ||||
|             custom_types: '自定義類型', | ||||
|         }, | ||||
|         menu: { | ||||
|             file: { | ||||
|                 file: '檔案', | ||||
|                 new: '新增', | ||||
|                 open: '開啟', | ||||
|             actions: { | ||||
|                 actions: '操作', | ||||
|                 new: '新增圖表', | ||||
|                 browse: '瀏覽...', | ||||
|                 save: '儲存', | ||||
|                 import: '匯入資料庫', | ||||
|                 export_sql: '匯出 SQL', | ||||
|                 export_as: '匯出為特定格式', | ||||
|                 delete_diagram: '刪除圖表', | ||||
|                 exit: '退出', | ||||
|             }, | ||||
|             edit: { | ||||
|                 edit: '編輯', | ||||
| @@ -26,7 +34,10 @@ export const zh_TW: LanguageTranslation = { | ||||
|                 hide_sidebar: '隱藏側邊欄', | ||||
|                 hide_cardinality: '隱藏基數', | ||||
|                 show_cardinality: '顯示基數', | ||||
|                 hide_field_attributes: '隱藏欄位屬性', | ||||
|                 show_field_attributes: '顯示欄位屬性', | ||||
|                 zoom_on_scroll: '滾動縮放', | ||||
|                 show_views: '資料庫檢視', | ||||
|                 theme: '主題', | ||||
|                 show_dependencies: '顯示相依性', | ||||
|                 hide_dependencies: '隱藏相依性', | ||||
| @@ -67,15 +78,6 @@ export const zh_TW: LanguageTranslation = { | ||||
|             cancel: '取消', | ||||
|         }, | ||||
|  | ||||
|         multiple_schemas_alert: { | ||||
|             title: '多重 Schema', | ||||
|             description: | ||||
|                 '此圖表中包含 {{schemasCount}} 個 Schema,目前顯示:{{formattedSchemas}}。', | ||||
|             dont_show_again: '不再顯示', | ||||
|             change_schema: '變更', | ||||
|             none: '無', | ||||
|         }, | ||||
|  | ||||
|         copy_to_clipboard_toast: { | ||||
|             unsupported: { | ||||
|                 title: '複製失敗', | ||||
| @@ -110,14 +112,11 @@ export const zh_TW: LanguageTranslation = { | ||||
|         copied: '已複製!', | ||||
|  | ||||
|         side_panel: { | ||||
|             schema: 'Schema:', | ||||
|             filter_by_schema: '依 Schema 篩選', | ||||
|             search_schema: '搜尋 Schema...', | ||||
|             no_schemas_found: '未找到 Schema。', | ||||
|             view_all_options: '顯示所有選項...', | ||||
|             tables_section: { | ||||
|                 tables: '表格', | ||||
|                 add_table: '新增表格', | ||||
|                 add_view: '新增檢視', | ||||
|                 filter: '篩選', | ||||
|                 collapse: '全部摺疊', | ||||
|                 // TODO: Translate | ||||
| @@ -143,16 +142,23 @@ export const zh_TW: LanguageTranslation = { | ||||
|                     field_actions: { | ||||
|                         title: '欄位屬性', | ||||
|                         unique: '唯一', | ||||
|                         auto_increment: '自動遞增', | ||||
|                         comments: '註解', | ||||
|                         no_comments: '無註解', | ||||
|                         delete_field: '刪除欄位', | ||||
|                         // TODO: Translate | ||||
|                         default_value: 'Default Value', | ||||
|                         no_default: 'No default', | ||||
|                         // TODO: Translate | ||||
|                         character_length: 'Max Length', | ||||
|                         precision: '精度', | ||||
|                         scale: '小數位', | ||||
|                     }, | ||||
|                     index_actions: { | ||||
|                         title: '索引屬性', | ||||
|                         name: '名稱', | ||||
|                         unique: '唯一', | ||||
|                         index_type: '索引類型', | ||||
|                         delete_index: '刪除索引', | ||||
|                     }, | ||||
|                     table_actions: { | ||||
| @@ -169,12 +175,15 @@ export const zh_TW: LanguageTranslation = { | ||||
|                     description: '請新增表格以開始', | ||||
|                 }, | ||||
|             }, | ||||
|             relationships_section: { | ||||
|                 relationships: '關聯', | ||||
|             refs_section: { | ||||
|                 refs: 'Refs', | ||||
|                 filter: '篩選', | ||||
|                 add_relationship: '新增關聯', | ||||
|                 collapse: '全部摺疊', | ||||
|                 add_relationship: '新增關聯', | ||||
|                 relationships: '關聯', | ||||
|                 dependencies: '相依性', | ||||
|                 relationship: { | ||||
|                     relationship: '關聯', | ||||
|                     primary: '主表格', | ||||
|                     foreign: '參照表格', | ||||
|                     cardinality: '基數', | ||||
| @@ -184,16 +193,8 @@ export const zh_TW: LanguageTranslation = { | ||||
|                         delete_relationship: '刪除', | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: '尚無關聯', | ||||
|                     description: '請新增關聯以連接表格', | ||||
|                 }, | ||||
|             }, | ||||
|             dependencies_section: { | ||||
|                 dependencies: '相依性', | ||||
|                 filter: '篩選', | ||||
|                 collapse: '全部摺疊', | ||||
|                 dependency: { | ||||
|                     dependency: '相依性', | ||||
|                     table: '表格', | ||||
|                     dependent_table: '相依檢視', | ||||
|                     delete_dependency: '刪除', | ||||
| @@ -203,8 +204,8 @@ export const zh_TW: LanguageTranslation = { | ||||
|                     }, | ||||
|                 }, | ||||
|                 empty_state: { | ||||
|                     title: '尚無相依性', | ||||
|                     description: '請建立檢視以開始', | ||||
|                     title: '尚無關聯', | ||||
|                     description: '請建立關聯以開始', | ||||
|                 }, | ||||
|             }, | ||||
|  | ||||
| @@ -247,9 +248,12 @@ export const zh_TW: LanguageTranslation = { | ||||
|                     field_name_placeholder: 'Field name', | ||||
|                     field_type_placeholder: 'Select type', | ||||
|                     add_field: 'Add Field', | ||||
|                     no_fields_tooltip: 'No fields defined for this custom type', | ||||
|                     custom_type_actions: { | ||||
|                         title: 'Actions', | ||||
|                         highlight_fields: 'Highlight Fields', | ||||
|                         delete_custom_type: 'Delete', | ||||
|                         clear_field_highlight: 'Clear Highlight', | ||||
|                     }, | ||||
|                     delete_custom_type: 'Delete Type', | ||||
|                 }, | ||||
| @@ -264,7 +268,13 @@ export const zh_TW: LanguageTranslation = { | ||||
|             undo: '復原', | ||||
|             redo: '重做', | ||||
|             reorder_diagram: '重新排列圖表', | ||||
|             // TODO: Translate | ||||
|             clear_custom_type_highlight: 'Clear highlight for "{{typeName}}"', | ||||
|             custom_type_highlight_tooltip: | ||||
|                 'Highlighting "{{typeName}}" - Click to clear', | ||||
|             highlight_overlapping_tables: '突出顯示重疊表格', | ||||
|             // TODO: Translate | ||||
|             filter: 'Filter Tables', | ||||
|         }, | ||||
|  | ||||
|         new_diagram_dialog: { | ||||
| @@ -310,6 +320,12 @@ export const zh_TW: LanguageTranslation = { | ||||
|             }, | ||||
|             cancel: '取消', | ||||
|             open: '開啟', | ||||
|  | ||||
|             diagram_actions: { | ||||
|                 open: '開啟', | ||||
|                 duplicate: '複製', | ||||
|                 delete: '刪除圖表', | ||||
|             }, | ||||
|         }, | ||||
|  | ||||
|         export_sql_dialog: { | ||||
| @@ -394,6 +410,14 @@ export const zh_TW: LanguageTranslation = { | ||||
|             confirm: '變更', | ||||
|         }, | ||||
|  | ||||
|         create_table_schema_dialog: { | ||||
|             title: '建立新 Schema', | ||||
|             description: | ||||
|                 '尚未存在任何 Schema。建立您的第一個 Schema 來組織您的表格。', | ||||
|             create: '建立', | ||||
|             cancel: '取消', | ||||
|         }, | ||||
|  | ||||
|         star_us_dialog: { | ||||
|             title: '協助我們改善!', | ||||
|             description: '請在 GitHub 上給我們一顆星,只需點擊一下!', | ||||
| @@ -448,6 +472,7 @@ export const zh_TW: LanguageTranslation = { | ||||
|  | ||||
|         canvas_context_menu: { | ||||
|             new_table: '新建表格', | ||||
|             new_view: '新檢視', | ||||
|             new_relationship: '新建關聯', | ||||
|             // TODO: Translate | ||||
|             new_area: 'New Area', | ||||
| @@ -469,6 +494,9 @@ export const zh_TW: LanguageTranslation = { | ||||
|         language_select: { | ||||
|             change_language: '變更語言', | ||||
|         }, | ||||
|  | ||||
|         on: '開啟', | ||||
|         off: '關閉', | ||||
|     }, | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import type { DBCustomType } from './domain'; | ||||
| import type { Area } from './domain/area'; | ||||
| import type { DBDependency } from './domain/db-dependency'; | ||||
| import type { DBField } from './domain/db-field'; | ||||
| @@ -48,6 +49,10 @@ const generateIdsMapFromDiagram = ( | ||||
|         idsMap.set(area.id, generateId()); | ||||
|     }); | ||||
|  | ||||
|     diagram.customTypes?.forEach((customType) => { | ||||
|         idsMap.set(customType.id, generateId()); | ||||
|     }); | ||||
|  | ||||
|     return idsMap; | ||||
| }; | ||||
|  | ||||
| @@ -124,7 +129,7 @@ export const cloneDiagram = ( | ||||
|     } = { | ||||
|         generateId: defaultGenerateId, | ||||
|     } | ||||
| ): Diagram => { | ||||
| ): { diagram: Diagram; idsMap: Map<string, string> } => { | ||||
|     const { generateId } = options; | ||||
|     const diagramId = generateId(); | ||||
|  | ||||
| @@ -213,14 +218,38 @@ export const cloneDiagram = ( | ||||
|             }) | ||||
|             .filter((area): area is Area => area !== null) ?? []; | ||||
|  | ||||
|     const customTypes: DBCustomType[] = | ||||
|         diagram.customTypes | ||||
|             ?.map((customType) => { | ||||
|                 const id = getNewId(customType.id); | ||||
|                 if (!id) { | ||||
|                     return null; | ||||
|                 } | ||||
|                 return { | ||||
|                     ...customType, | ||||
|                     id, | ||||
|                 } satisfies DBCustomType; | ||||
|             }) | ||||
|             .filter( | ||||
|                 (customType): customType is DBCustomType => customType !== null | ||||
|             ) ?? []; | ||||
|  | ||||
|     return { | ||||
|         ...diagram, | ||||
|         id: diagramId, | ||||
|         dependencies, | ||||
|         relationships, | ||||
|         tables, | ||||
|         areas, | ||||
|         createdAt: new Date(), | ||||
|         updatedAt: new Date(), | ||||
|         diagram: { | ||||
|             ...diagram, | ||||
|             id: diagramId, | ||||
|             dependencies, | ||||
|             relationships, | ||||
|             tables, | ||||
|             areas, | ||||
|             customTypes, | ||||
|             createdAt: diagram.createdAt | ||||
|                 ? new Date(diagram.createdAt) | ||||
|                 : new Date(), | ||||
|             updatedAt: diagram.updatedAt | ||||
|                 ? new Date(diagram.updatedAt) | ||||
|                 : new Date(), | ||||
|         }, | ||||
|         idsMap, | ||||
|     }; | ||||
| }; | ||||
|   | ||||
| @@ -19,3 +19,5 @@ export const randomColor = () => { | ||||
|  | ||||
| export const viewColor = '#b0b0b0'; | ||||
| export const materializedViewColor = '#7d7d7d'; | ||||
| export const defaultTableColor = '#8eb7ff'; | ||||
| export const defaultAreaColor = '#b067e9'; | ||||
|   | ||||
| @@ -48,18 +48,30 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [ | ||||
|     { name: 'mediumblob', id: 'mediumblob' }, | ||||
|     { name: 'tinyblob', id: 'tinyblob' }, | ||||
|     { name: 'blob', id: 'blob' }, | ||||
|     { name: 'varchar', id: 'varchar', hasCharMaxLength: true }, | ||||
|     { name: 'char', id: 'char', hasCharMaxLength: true }, | ||||
|     { | ||||
|         name: 'varchar', | ||||
|         id: 'varchar', | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } }, | ||||
|     { name: 'char large object', id: 'char_large_object' }, | ||||
|     { name: 'char varying', id: 'char_varying', hasCharMaxLength: true }, | ||||
|     { | ||||
|         name: 'char varying', | ||||
|         id: 'char_varying', | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { name: 'character large object', id: 'character_large_object' }, | ||||
|     { | ||||
|         name: 'character varying', | ||||
|         id: 'character_varying', | ||||
|         hasCharMaxLength: true, | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { name: 'nchar large object', id: 'nchar_large_object' }, | ||||
|     { name: 'nchar varying', id: 'nchar_varying', hasCharMaxLength: true }, | ||||
|     { | ||||
|         name: 'nchar varying', | ||||
|         id: 'nchar_varying', | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { | ||||
|         name: 'national character large object', | ||||
|         id: 'national_character_large_object', | ||||
| @@ -67,22 +79,34 @@ export const clickhouseDataTypes: readonly DataTypeData[] = [ | ||||
|     { | ||||
|         name: 'national character varying', | ||||
|         id: 'national_character_varying', | ||||
|         hasCharMaxLength: true, | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { | ||||
|         name: 'national char varying', | ||||
|         id: 'national_char_varying', | ||||
|         hasCharMaxLength: true, | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { | ||||
|         name: 'national character', | ||||
|         id: 'national_character', | ||||
|         hasCharMaxLength: true, | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { | ||||
|         name: 'national char', | ||||
|         id: 'national_char', | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { name: 'national char', id: 'national_char', hasCharMaxLength: true }, | ||||
|     { name: 'binary large object', id: 'binary_large_object' }, | ||||
|     { name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true }, | ||||
|     { name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true }, | ||||
|     { | ||||
|         name: 'binary varying', | ||||
|         id: 'binary_varying', | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { | ||||
|         name: 'fixedstring', | ||||
|         id: 'fixedstring', | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { name: 'string', id: 'string' }, | ||||
|  | ||||
|     // Date Types | ||||
|   | ||||
| @@ -14,9 +14,23 @@ export interface DataType { | ||||
|     name: string; | ||||
| } | ||||
|  | ||||
| export interface DataTypeData extends DataType { | ||||
| export interface FieldAttributeRange { | ||||
|     max: number; | ||||
|     min: number; | ||||
|     default: number; | ||||
| } | ||||
|  | ||||
| interface FieldAttributes { | ||||
|     hasCharMaxLength?: boolean; | ||||
|     hasCharMaxLengthOption?: boolean; | ||||
|     precision?: FieldAttributeRange; | ||||
|     scale?: FieldAttributeRange; | ||||
|     maxLength?: number; | ||||
| } | ||||
|  | ||||
| export interface DataTypeData extends DataType { | ||||
|     usageLevel?: 1 | 2; // Level 1 is most common, Level 2 is second most common | ||||
|     fieldAttributes?: FieldAttributes; | ||||
| } | ||||
|  | ||||
| export const dataTypeSchema: z.ZodType<DataType> = z.object({ | ||||
| @@ -132,3 +146,22 @@ export const findDataTypeDataById = ( | ||||
|  | ||||
|     return dataTypesOptions.find((dataType) => dataType.id === id); | ||||
| }; | ||||
|  | ||||
| export const supportsAutoIncrementDataType = ( | ||||
|     dataTypeName: string | ||||
| ): boolean => { | ||||
|     return [ | ||||
|         'integer', | ||||
|         'int', | ||||
|         'bigint', | ||||
|         'smallint', | ||||
|         'tinyint', | ||||
|         'mediumint', | ||||
|         'serial', | ||||
|         'bigserial', | ||||
|         'smallserial', | ||||
|         'number', | ||||
|         'numeric', | ||||
|         'decimal', | ||||
|     ].includes(dataTypeName.toLocaleLowerCase()); | ||||
| }; | ||||
|   | ||||
| @@ -2,7 +2,12 @@ import type { DataTypeData } from './data-types'; | ||||
|  | ||||
| export const genericDataTypes: readonly DataTypeData[] = [ | ||||
|     // Level 1 - Most commonly used types | ||||
|     { name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 }, | ||||
|     { | ||||
|         name: 'varchar', | ||||
|         id: 'varchar', | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|         usageLevel: 1, | ||||
|     }, | ||||
|     { name: 'int', id: 'int', usageLevel: 1 }, | ||||
|     { name: 'text', id: 'text', usageLevel: 1 }, | ||||
|     { name: 'boolean', id: 'boolean', usageLevel: 1 }, | ||||
| @@ -10,23 +15,62 @@ export const genericDataTypes: readonly DataTypeData[] = [ | ||||
|     { name: 'timestamp', id: 'timestamp', usageLevel: 1 }, | ||||
|  | ||||
|     // Level 2 - Second most common types | ||||
|     { name: 'decimal', id: 'decimal', usageLevel: 2 }, | ||||
|     { | ||||
|         name: 'decimal', | ||||
|         id: 'decimal', | ||||
|         usageLevel: 2, | ||||
|         fieldAttributes: { | ||||
|             precision: { | ||||
|                 max: 999, | ||||
|                 min: 1, | ||||
|                 default: 10, | ||||
|             }, | ||||
|             scale: { | ||||
|                 max: 999, | ||||
|                 min: 0, | ||||
|                 default: 2, | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     { name: 'datetime', id: 'datetime', usageLevel: 2 }, | ||||
|     { name: 'json', id: 'json', usageLevel: 2 }, | ||||
|     { name: 'uuid', id: 'uuid', usageLevel: 2 }, | ||||
|  | ||||
|     // Less common types | ||||
|     { name: 'bigint', id: 'bigint' }, | ||||
|     { name: 'binary', id: 'binary', hasCharMaxLength: true }, | ||||
|     { | ||||
|         name: 'binary', | ||||
|         id: 'binary', | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { name: 'blob', id: 'blob' }, | ||||
|     { name: 'char', id: 'char', hasCharMaxLength: true }, | ||||
|     { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } }, | ||||
|     { name: 'double', id: 'double' }, | ||||
|     { name: 'enum', id: 'enum' }, | ||||
|     { name: 'float', id: 'float' }, | ||||
|     { name: 'numeric', id: 'numeric' }, | ||||
|     { | ||||
|         name: 'numeric', | ||||
|         id: 'numeric', | ||||
|         fieldAttributes: { | ||||
|             precision: { | ||||
|                 max: 999, | ||||
|                 min: 1, | ||||
|                 default: 10, | ||||
|             }, | ||||
|             scale: { | ||||
|                 max: 999, | ||||
|                 min: 0, | ||||
|                 default: 2, | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     { name: 'real', id: 'real' }, | ||||
|     { name: 'set', id: 'set' }, | ||||
|     { name: 'smallint', id: 'smallint' }, | ||||
|     { name: 'time', id: 'time' }, | ||||
|     { name: 'varbinary', id: 'varbinary', hasCharMaxLength: true }, | ||||
|     { | ||||
|         name: 'varbinary', | ||||
|         id: 'varbinary', | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
| ] as const; | ||||
|   | ||||
| @@ -4,12 +4,32 @@ export const mariadbDataTypes: readonly DataTypeData[] = [ | ||||
|     // Level 1 - Most commonly used types | ||||
|     { name: 'int', id: 'int', usageLevel: 1 }, | ||||
|     { name: 'bigint', id: 'bigint', usageLevel: 1 }, | ||||
|     { name: 'decimal', id: 'decimal', usageLevel: 1 }, | ||||
|     { | ||||
|         name: 'decimal', | ||||
|         id: 'decimal', | ||||
|         usageLevel: 1, | ||||
|         fieldAttributes: { | ||||
|             precision: { | ||||
|                 max: 65, | ||||
|                 min: 1, | ||||
|                 default: 10, | ||||
|             }, | ||||
|             scale: { | ||||
|                 max: 30, | ||||
|                 min: 0, | ||||
|                 default: 0, | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     { name: 'boolean', id: 'boolean', usageLevel: 1 }, | ||||
|     { name: 'datetime', id: 'datetime', usageLevel: 1 }, | ||||
|     { name: 'date', id: 'date', usageLevel: 1 }, | ||||
|     { name: 'timestamp', id: 'timestamp', usageLevel: 1 }, | ||||
|     { name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 }, | ||||
|     { | ||||
|         name: 'varchar', | ||||
|         id: 'varchar', | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { name: 'text', id: 'text', usageLevel: 1 }, | ||||
|  | ||||
|     // Level 2 - Second most common types | ||||
| @@ -20,16 +40,39 @@ export const mariadbDataTypes: readonly DataTypeData[] = [ | ||||
|     { name: 'tinyint', id: 'tinyint' }, | ||||
|     { name: 'smallint', id: 'smallint' }, | ||||
|     { name: 'mediumint', id: 'mediumint' }, | ||||
|     { name: 'numeric', id: 'numeric' }, | ||||
|     { | ||||
|         name: 'numeric', | ||||
|         id: 'numeric', | ||||
|         fieldAttributes: { | ||||
|             precision: { | ||||
|                 max: 65, | ||||
|                 min: 1, | ||||
|                 default: 10, | ||||
|             }, | ||||
|             scale: { | ||||
|                 max: 30, | ||||
|                 min: 0, | ||||
|                 default: 0, | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     { name: 'float', id: 'float' }, | ||||
|     { name: 'double', id: 'double' }, | ||||
|     { name: 'bit', id: 'bit' }, | ||||
|     { name: 'bool', id: 'bool' }, | ||||
|     { name: 'time', id: 'time' }, | ||||
|     { name: 'year', id: 'year' }, | ||||
|     { name: 'char', id: 'char', hasCharMaxLength: true }, | ||||
|     { name: 'binary', id: 'binary', hasCharMaxLength: true }, | ||||
|     { name: 'varbinary', id: 'varbinary', hasCharMaxLength: true }, | ||||
|     { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } }, | ||||
|     { | ||||
|         name: 'binary', | ||||
|         id: 'binary', | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { | ||||
|         name: 'varbinary', | ||||
|         id: 'varbinary', | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { name: 'tinyblob', id: 'tinyblob' }, | ||||
|     { name: 'blob', id: 'blob' }, | ||||
|     { name: 'mediumblob', id: 'mediumblob' }, | ||||
|   | ||||
| @@ -3,7 +3,12 @@ import type { DataTypeData } from './data-types'; | ||||
| export const mysqlDataTypes: readonly DataTypeData[] = [ | ||||
|     // Level 1 - Most commonly used types | ||||
|     { name: 'int', id: 'int', usageLevel: 1 }, | ||||
|     { name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 }, | ||||
|     { | ||||
|         name: 'varchar', | ||||
|         id: 'varchar', | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|         usageLevel: 1, | ||||
|     }, | ||||
|     { name: 'text', id: 'text', usageLevel: 1 }, | ||||
|     { name: 'boolean', id: 'boolean', usageLevel: 1 }, | ||||
|     { name: 'timestamp', id: 'timestamp', usageLevel: 1 }, | ||||
| @@ -11,7 +16,23 @@ export const mysqlDataTypes: readonly DataTypeData[] = [ | ||||
|  | ||||
|     // Level 2 - Second most common types | ||||
|     { name: 'bigint', id: 'bigint', usageLevel: 2 }, | ||||
|     { name: 'decimal', id: 'decimal', usageLevel: 2 }, | ||||
|     { | ||||
|         name: 'decimal', | ||||
|         id: 'decimal', | ||||
|         usageLevel: 2, | ||||
|         fieldAttributes: { | ||||
|             precision: { | ||||
|                 max: 65, | ||||
|                 min: 1, | ||||
|                 default: 10, | ||||
|             }, | ||||
|             scale: { | ||||
|                 max: 30, | ||||
|                 min: 0, | ||||
|                 default: 0, | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     { name: 'datetime', id: 'datetime', usageLevel: 2 }, | ||||
|     { name: 'json', id: 'json', usageLevel: 2 }, | ||||
|  | ||||
| @@ -22,7 +43,7 @@ export const mysqlDataTypes: readonly DataTypeData[] = [ | ||||
|     { name: 'float', id: 'float' }, | ||||
|     { name: 'double', id: 'double' }, | ||||
|     { name: 'bit', id: 'bit' }, | ||||
|     { name: 'char', id: 'char', hasCharMaxLength: true }, | ||||
|     { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } }, | ||||
|     { name: 'tinytext', id: 'tinytext' }, | ||||
|     { name: 'mediumtext', id: 'mediumtext' }, | ||||
|     { name: 'longtext', id: 'longtext' }, | ||||
|   | ||||
| @@ -2,15 +2,30 @@ import type { DataTypeData } from './data-types'; | ||||
|  | ||||
| export const oracleDataTypes: readonly DataTypeData[] = [ | ||||
|     // Character types | ||||
|     { name: 'VARCHAR2', id: 'varchar2', usageLevel: 1, hasCharMaxLength: true }, | ||||
|     { | ||||
|         name: 'VARCHAR2', | ||||
|         id: 'varchar2', | ||||
|         usageLevel: 1, | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { | ||||
|         name: 'NVARCHAR2', | ||||
|         id: 'nvarchar2', | ||||
|         usageLevel: 1, | ||||
|         hasCharMaxLength: true, | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { | ||||
|         name: 'CHAR', | ||||
|         id: 'char', | ||||
|         usageLevel: 2, | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { | ||||
|         name: 'NCHAR', | ||||
|         id: 'nchar', | ||||
|         usageLevel: 2, | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { name: 'CHAR', id: 'char', usageLevel: 2, hasCharMaxLength: true }, | ||||
|     { name: 'NCHAR', id: 'nchar', usageLevel: 2, hasCharMaxLength: true }, | ||||
|     { name: 'CLOB', id: 'clob', usageLevel: 2 }, | ||||
|     { name: 'NCLOB', id: 'nclob', usageLevel: 2 }, | ||||
|  | ||||
| @@ -49,7 +64,12 @@ export const oracleDataTypes: readonly DataTypeData[] = [ | ||||
|     { name: 'BFILE', id: 'bfile', usageLevel: 2 }, | ||||
|  | ||||
|     // Other types | ||||
|     { name: 'RAW', id: 'raw', usageLevel: 2, hasCharMaxLength: true }, | ||||
|     { | ||||
|         name: 'RAW', | ||||
|         id: 'raw', | ||||
|         usageLevel: 2, | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { name: 'LONG RAW', id: 'long_raw', usageLevel: 2 }, | ||||
|     { name: 'ROWID', id: 'rowid', usageLevel: 2 }, | ||||
|     { name: 'UROWID', id: 'urowid', usageLevel: 2 }, | ||||
|   | ||||
| @@ -3,7 +3,12 @@ import type { DataTypeData } from './data-types'; | ||||
| export const postgresDataTypes: readonly DataTypeData[] = [ | ||||
|     // Level 1 - Most commonly used types | ||||
|     { name: 'integer', id: 'integer', usageLevel: 1 }, | ||||
|     { name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 }, | ||||
|     { | ||||
|         name: 'varchar', | ||||
|         id: 'varchar', | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|         usageLevel: 1, | ||||
|     }, | ||||
|     { name: 'text', id: 'text', usageLevel: 1 }, | ||||
|     { name: 'boolean', id: 'boolean', usageLevel: 1 }, | ||||
|     { name: 'timestamp', id: 'timestamp', usageLevel: 1 }, | ||||
| @@ -11,7 +16,23 @@ export const postgresDataTypes: readonly DataTypeData[] = [ | ||||
|  | ||||
|     // Level 2 - Second most common types | ||||
|     { name: 'bigint', id: 'bigint', usageLevel: 2 }, | ||||
|     { name: 'decimal', id: 'decimal', usageLevel: 2 }, | ||||
|     { | ||||
|         name: 'decimal', | ||||
|         id: 'decimal', | ||||
|         usageLevel: 2, | ||||
|         fieldAttributes: { | ||||
|             precision: { | ||||
|                 max: 131072, | ||||
|                 min: 0, | ||||
|                 default: 10, | ||||
|             }, | ||||
|             scale: { | ||||
|                 max: 16383, | ||||
|                 min: 0, | ||||
|                 default: 2, | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     { name: 'serial', id: 'serial', usageLevel: 2 }, | ||||
|     { name: 'json', id: 'json', usageLevel: 2 }, | ||||
|     { name: 'jsonb', id: 'jsonb', usageLevel: 2 }, | ||||
| @@ -23,18 +44,33 @@ export const postgresDataTypes: readonly DataTypeData[] = [ | ||||
|     }, | ||||
|  | ||||
|     // Less common types | ||||
|     { name: 'numeric', id: 'numeric' }, | ||||
|     { | ||||
|         name: 'numeric', | ||||
|         id: 'numeric', | ||||
|         fieldAttributes: { | ||||
|             precision: { | ||||
|                 max: 131072, | ||||
|                 min: 0, | ||||
|                 default: 10, | ||||
|             }, | ||||
|             scale: { | ||||
|                 max: 16383, | ||||
|                 min: 0, | ||||
|                 default: 2, | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     { name: 'real', id: 'real' }, | ||||
|     { name: 'double precision', id: 'double_precision' }, | ||||
|     { name: 'smallserial', id: 'smallserial' }, | ||||
|     { name: 'bigserial', id: 'bigserial' }, | ||||
|     { name: 'money', id: 'money' }, | ||||
|     { name: 'smallint', id: 'smallint' }, | ||||
|     { name: 'char', id: 'char', hasCharMaxLength: true }, | ||||
|     { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } }, | ||||
|     { | ||||
|         name: 'character varying', | ||||
|         id: 'character_varying', | ||||
|         hasCharMaxLength: true, | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { name: 'time', id: 'time' }, | ||||
|     { name: 'timestamp without time zone', id: 'timestamp_without_time_zone' }, | ||||
|   | ||||
| @@ -4,32 +4,93 @@ export const sqlServerDataTypes: readonly DataTypeData[] = [ | ||||
|     // Level 1 - Most commonly used types | ||||
|     { name: 'int', id: 'int', usageLevel: 1 }, | ||||
|     { name: 'bit', id: 'bit', usageLevel: 1 }, | ||||
|     { name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 }, | ||||
|     { name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true, usageLevel: 1 }, | ||||
|     { | ||||
|         name: 'varchar', | ||||
|         id: 'varchar', | ||||
|         fieldAttributes: { | ||||
|             hasCharMaxLength: true, | ||||
|             hasCharMaxLengthOption: true, | ||||
|             maxLength: 8000, | ||||
|         }, | ||||
|         usageLevel: 1, | ||||
|     }, | ||||
|     { | ||||
|         name: 'nvarchar', | ||||
|         id: 'nvarchar', | ||||
|         fieldAttributes: { | ||||
|             hasCharMaxLength: true, | ||||
|             hasCharMaxLengthOption: true, | ||||
|             maxLength: 4000, | ||||
|         }, | ||||
|         usageLevel: 1, | ||||
|     }, | ||||
|     { name: 'text', id: 'text', usageLevel: 1 }, | ||||
|     { name: 'datetime', id: 'datetime', usageLevel: 1 }, | ||||
|     { name: 'date', id: 'date', usageLevel: 1 }, | ||||
|  | ||||
|     // Level 2 - Second most common types | ||||
|     { name: 'bigint', id: 'bigint', usageLevel: 2 }, | ||||
|     { name: 'decimal', id: 'decimal', usageLevel: 2 }, | ||||
|     { | ||||
|         name: 'decimal', | ||||
|         id: 'decimal', | ||||
|         usageLevel: 2, | ||||
|         fieldAttributes: { | ||||
|             precision: { | ||||
|                 max: 38, | ||||
|                 min: 1, | ||||
|                 default: 18, | ||||
|             }, | ||||
|             scale: { | ||||
|                 max: 38, | ||||
|                 min: 0, | ||||
|                 default: 0, | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     { name: 'datetime2', id: 'datetime2', usageLevel: 2 }, | ||||
|     { name: 'uniqueidentifier', id: 'uniqueidentifier', usageLevel: 2 }, | ||||
|     { name: 'json', id: 'json', usageLevel: 2 }, | ||||
|  | ||||
|     // Less common types | ||||
|     { name: 'numeric', id: 'numeric' }, | ||||
|     { | ||||
|         name: 'numeric', | ||||
|         id: 'numeric', | ||||
|         fieldAttributes: { | ||||
|             precision: { | ||||
|                 max: 38, | ||||
|                 min: 1, | ||||
|                 default: 18, | ||||
|             }, | ||||
|             scale: { | ||||
|                 max: 38, | ||||
|                 min: 0, | ||||
|                 default: 0, | ||||
|             }, | ||||
|         }, | ||||
|     }, | ||||
|     { name: 'smallint', id: 'smallint' }, | ||||
|     { name: 'smallmoney', id: 'smallmoney' }, | ||||
|     { name: 'tinyint', id: 'tinyint' }, | ||||
|     { name: 'money', id: 'money' }, | ||||
|     { name: 'float', id: 'float' }, | ||||
|     { name: 'real', id: 'real' }, | ||||
|     { name: 'char', id: 'char', hasCharMaxLength: true }, | ||||
|     { name: 'nchar', id: 'nchar', hasCharMaxLength: true }, | ||||
|     { name: 'char', id: 'char', fieldAttributes: { hasCharMaxLength: true } }, | ||||
|     { name: 'nchar', id: 'nchar', fieldAttributes: { hasCharMaxLength: true } }, | ||||
|     { name: 'ntext', id: 'ntext' }, | ||||
|     { name: 'binary', id: 'binary', hasCharMaxLength: true }, | ||||
|     { name: 'varbinary', id: 'varbinary', hasCharMaxLength: true }, | ||||
|     { | ||||
|         name: 'binary', | ||||
|         id: 'binary', | ||||
|         fieldAttributes: { hasCharMaxLength: true }, | ||||
|     }, | ||||
|     { | ||||
|         name: 'varbinary', | ||||
|         id: 'varbinary', | ||||
|         fieldAttributes: { | ||||
|             hasCharMaxLength: true, | ||||
|             hasCharMaxLengthOption: true, | ||||
|             maxLength: 8000, | ||||
|         }, | ||||
|     }, | ||||
|     { name: 'image', id: 'image' }, | ||||
|     { name: 'datetimeoffset', id: 'datetimeoffset' }, | ||||
|     { name: 'smalldatetime', id: 'smalldatetime' }, | ||||
|   | ||||
| @@ -10,25 +10,48 @@ export const sqliteDataTypes: readonly DataTypeData[] = [ | ||||
|  | ||||
|     // SQLite type aliases and common types | ||||
|     { name: 'int', id: 'int', usageLevel: 1 }, | ||||
|     { name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 }, | ||||
|     { name: 'timestamp', id: 'timestamp', usageLevel: 1 }, | ||||
|     { name: 'date', id: 'date', usageLevel: 1 }, | ||||
|     { name: 'datetime', id: 'datetime', usageLevel: 1 }, | ||||
|     { name: 'boolean', id: 'boolean', usageLevel: 1 }, | ||||
|     { | ||||
|         name: 'varchar', | ||||
|         id: 'varchar', | ||||
|         fieldAttributes: { | ||||
|             hasCharMaxLength: true, | ||||
|         }, | ||||
|         usageLevel: 1, | ||||
|     }, | ||||
|     { | ||||
|         name: 'timestamp', | ||||
|         id: 'timestamp', | ||||
|         usageLevel: 1, | ||||
|     }, | ||||
|  | ||||
|     // Level 2 - Second most common types | ||||
|     { name: 'numeric', id: 'numeric', usageLevel: 2 }, | ||||
|     { name: 'decimal', id: 'decimal', usageLevel: 2 }, | ||||
|     { name: 'float', id: 'float', usageLevel: 2 }, | ||||
|     { | ||||
|         name: 'decimal', | ||||
|         id: 'decimal', | ||||
|         usageLevel: 2, | ||||
|     }, | ||||
|     { name: 'double', id: 'double', usageLevel: 2 }, | ||||
|     { name: 'json', id: 'json', usageLevel: 2 }, | ||||
|  | ||||
|     // Less common types (all map to SQLite storage classes) | ||||
|     { name: 'char', id: 'char', hasCharMaxLength: true }, | ||||
|     { | ||||
|         name: 'char', | ||||
|         id: 'char', | ||||
|         fieldAttributes: { | ||||
|             hasCharMaxLength: true, | ||||
|         }, | ||||
|         usageLevel: 2, | ||||
|     }, | ||||
|     { name: 'binary', id: 'binary' }, | ||||
|     { name: 'varbinary', id: 'varbinary' }, | ||||
|     { name: 'smallint', id: 'smallint' }, | ||||
|     { name: 'bigint', id: 'bigint' }, | ||||
|     { name: 'bool', id: 'bool' }, | ||||
|     { name: 'boolean', id: 'boolean' }, // Added for smartquery compatibility | ||||
|     { name: 'time', id: 'time' }, | ||||
|     { name: 'date', id: 'date' }, // Added for smartquery compatibility | ||||
|     { name: 'datetime', id: 'datetime' }, // Added for smartquery compatibility | ||||
| ] as const; | ||||
|   | ||||
| @@ -4,4 +4,5 @@ export const defaultSchemas: { [key in DatabaseType]?: string } = { | ||||
|     [DatabaseType.POSTGRESQL]: 'public', | ||||
|     [DatabaseType.SQL_SERVER]: 'dbo', | ||||
|     [DatabaseType.CLICKHOUSE]: 'default', | ||||
|     [DatabaseType.COCKROACHDB]: 'public', | ||||
| }; | ||||
|   | ||||
							
								
								
									
										947
									
								
								src/lib/data/export-metadata/__tests__/export-sql-dbml.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										947
									
								
								src/lib/data/export-metadata/__tests__/export-sql-dbml.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,947 @@ | ||||
| import { describe, it, expect } from 'vitest'; | ||||
| import { exportBaseSQL } from '../export-sql-script'; | ||||
| import { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import type { Diagram } from '@/lib/domain/diagram'; | ||||
| import type { DBTable } from '@/lib/domain/db-table'; | ||||
| import type { DBField } from '@/lib/domain/db-field'; | ||||
|  | ||||
| describe('DBML Export - SQL Generation Tests', () => { | ||||
|     // Helper to generate test IDs and timestamps | ||||
|     let idCounter = 0; | ||||
|     const testId = () => `test-id-${++idCounter}`; | ||||
|     const testTime = Date.now(); | ||||
|  | ||||
|     // Helper to create a field with all required properties | ||||
|     const createField = (overrides: Partial<DBField>): DBField => | ||||
|         ({ | ||||
|             id: testId(), | ||||
|             name: 'field', | ||||
|             type: { id: 'text', name: 'text' }, | ||||
|             primaryKey: false, | ||||
|             nullable: true, | ||||
|             unique: false, | ||||
|             createdAt: testTime, | ||||
|             ...overrides, | ||||
|         }) as DBField; | ||||
|  | ||||
|     // Helper to create a table with all required properties | ||||
|     const createTable = (overrides: Partial<DBTable>): DBTable => | ||||
|         ({ | ||||
|             id: testId(), | ||||
|             name: 'table', | ||||
|             fields: [], | ||||
|             indexes: [], | ||||
|             createdAt: testTime, | ||||
|             x: 0, | ||||
|             y: 0, | ||||
|             width: 200, | ||||
|             ...overrides, | ||||
|         }) as DBTable; | ||||
|  | ||||
|     // Helper to create a diagram with all required properties | ||||
|     const createDiagram = (overrides: Partial<Diagram>): Diagram => | ||||
|         ({ | ||||
|             id: testId(), | ||||
|             name: 'diagram', | ||||
|             databaseType: DatabaseType.GENERIC, | ||||
|             tables: [], | ||||
|             relationships: [], | ||||
|             createdAt: testTime, | ||||
|             updatedAt: testTime, | ||||
|             ...overrides, | ||||
|         }) as Diagram; | ||||
|  | ||||
|     describe('Composite Primary Keys', () => { | ||||
|         it('should handle tables with composite primary keys correctly', () => { | ||||
|             const tableId = testId(); | ||||
|             const field1Id = testId(); | ||||
|             const field2Id = testId(); | ||||
|  | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Enchanted Library', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [ | ||||
|                     createTable({ | ||||
|                         id: tableId, | ||||
|                         name: 'spell_components', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: field1Id, | ||||
|                                 name: 'spell_id', | ||||
|                                 type: { id: 'uuid', name: 'uuid' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: field2Id, | ||||
|                                 name: 'component_id', | ||||
|                                 type: { id: 'uuid', name: 'uuid' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'quantity', | ||||
|                                 type: { id: 'integer', name: 'integer' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                                 default: '1', | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [], | ||||
|                         color: '#FFD700', | ||||
|                     }), | ||||
|                 ], | ||||
|                 relationships: [], | ||||
|             }); | ||||
|  | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
|  | ||||
|             // Should contain composite primary key syntax | ||||
|             expect(sql).toContain('PRIMARY KEY (spell_id, component_id)'); | ||||
|             // Should NOT contain individual PRIMARY KEY constraints | ||||
|             expect(sql).not.toMatch(/spell_id\s+uuid\s+NOT NULL\s+PRIMARY KEY/); | ||||
|             expect(sql).not.toMatch( | ||||
|                 /component_id\s+uuid\s+NOT NULL\s+PRIMARY KEY/ | ||||
|             ); | ||||
|         }); | ||||
|  | ||||
|         it('should not create duplicate index for composite primary key', () => { | ||||
|             const tableId = testId(); | ||||
|             const field1Id = testId(); | ||||
|             const field2Id = testId(); | ||||
|             const field3Id = testId(); | ||||
|  | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Landlord System', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [ | ||||
|                     createTable({ | ||||
|                         id: tableId, | ||||
|                         name: 'users_master_table', | ||||
|                         schema: 'landlord', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: field1Id, | ||||
|                                 name: 'master_user_id', | ||||
|                                 type: { id: 'bigint', name: 'bigint' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: field2Id, | ||||
|                                 name: 'tenant_id', | ||||
|                                 type: { id: 'bigint', name: 'bigint' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: field3Id, | ||||
|                                 name: 'tenant_user_id', | ||||
|                                 type: { id: 'bigint', name: 'bigint' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'enabled', | ||||
|                                 type: { id: 'boolean', name: 'boolean' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: true, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [ | ||||
|                             { | ||||
|                                 id: testId(), | ||||
|                                 name: 'idx_users_master_table_master_user_id_tenant_id_tenant_user_id', | ||||
|                                 unique: false, | ||||
|                                 fieldIds: [field1Id, field2Id, field3Id], | ||||
|                                 createdAt: testTime, | ||||
|                             }, | ||||
|                             { | ||||
|                                 id: testId(), | ||||
|                                 name: 'index_1', | ||||
|                                 unique: true, | ||||
|                                 fieldIds: [field2Id, field3Id], | ||||
|                                 createdAt: testTime, | ||||
|                             }, | ||||
|                         ], | ||||
|                     }), | ||||
|                 ], | ||||
|                 relationships: [], | ||||
|             }); | ||||
|  | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
|  | ||||
|             // Should contain composite primary key constraint | ||||
|             expect(sql).toContain( | ||||
|                 'PRIMARY KEY (master_user_id, tenant_id, tenant_user_id)' | ||||
|             ); | ||||
|  | ||||
|             // Should NOT contain the duplicate index for the primary key fields | ||||
|             expect(sql).not.toContain( | ||||
|                 'CREATE INDEX idx_users_master_table_master_user_id_tenant_id_tenant_user_id' | ||||
|             ); | ||||
|  | ||||
|             // Should still contain the unique index on subset of fields | ||||
|             expect(sql).toContain('CREATE UNIQUE INDEX index_1'); | ||||
|         }); | ||||
|  | ||||
|         it('should handle single primary keys inline', () => { | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Wizard Academy', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [ | ||||
|                     createTable({ | ||||
|                         id: testId(), | ||||
|                         name: 'wizards', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'id', | ||||
|                                 type: { id: 'uuid', name: 'uuid' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'name', | ||||
|                                 type: { id: 'varchar', name: 'varchar' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [], | ||||
|                         color: '#9370DB', | ||||
|                     }), | ||||
|                 ], | ||||
|                 relationships: [], | ||||
|             }); | ||||
|  | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
|  | ||||
|             // Should contain inline PRIMARY KEY | ||||
|             expect(sql).toMatch(/id\s+uuid\s+NOT NULL\s+PRIMARY KEY/); | ||||
|             // Should NOT contain separate PRIMARY KEY constraint | ||||
|             expect(sql).not.toContain('PRIMARY KEY (id)'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Default Value Handling', () => { | ||||
|         it('should skip invalid default values like "has default"', () => { | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Potion Shop', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [ | ||||
|                     createTable({ | ||||
|                         id: testId(), | ||||
|                         name: 'potions', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'id', | ||||
|                                 type: { id: 'uuid', name: 'uuid' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'is_active', | ||||
|                                 type: { id: 'boolean', name: 'boolean' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: true, | ||||
|                                 unique: false, | ||||
|                                 default: 'has default', | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'stock_count', | ||||
|                                 type: { id: 'integer', name: 'integer' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                                 default: 'DEFAULT has default', | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [], | ||||
|                         color: '#98FB98', | ||||
|                     }), | ||||
|                 ], | ||||
|                 relationships: [], | ||||
|             }); | ||||
|  | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
|  | ||||
|             // Should not contain invalid default values | ||||
|             expect(sql).not.toContain('DEFAULT has default'); | ||||
|             expect(sql).not.toContain('DEFAULT DEFAULT has default'); | ||||
|             // The fields should still be in the table | ||||
|             expect(sql).toContain('is_active boolean'); | ||||
|             expect(sql).toContain('stock_count integer NOT NULL'); // integer gets simplified to int | ||||
|         }); | ||||
|  | ||||
|         it('should handle valid default values correctly', () => { | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Treasure Vault', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [ | ||||
|                     createTable({ | ||||
|                         id: testId(), | ||||
|                         name: 'treasures', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'id', | ||||
|                                 type: { id: 'uuid', name: 'uuid' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'gold_value', | ||||
|                                 type: { id: 'numeric', name: 'numeric' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                                 default: '100.50', | ||||
|                                 precision: 10, | ||||
|                                 scale: 2, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'created_at', | ||||
|                                 type: { id: 'timestamp', name: 'timestamp' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: true, | ||||
|                                 unique: false, | ||||
|                                 default: 'now()', | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'currency', | ||||
|                                 type: { id: 'char', name: 'char' }, | ||||
|                                 characterMaximumLength: '3', | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                                 default: 'EUR', | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [], | ||||
|                         color: '#FFD700', | ||||
|                     }), | ||||
|                 ], | ||||
|                 relationships: [], | ||||
|             }); | ||||
|  | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
|  | ||||
|             // Should contain valid defaults | ||||
|             expect(sql).toContain('DEFAULT 100.50'); | ||||
|             expect(sql).toContain('DEFAULT now()'); | ||||
|             expect(sql).toContain('DEFAULT EUR'); | ||||
|         }); | ||||
|  | ||||
|         it('should handle NOW and similar default values', () => { | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Quest Log', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [ | ||||
|                     createTable({ | ||||
|                         id: testId(), | ||||
|                         name: 'quests', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'id', | ||||
|                                 type: { id: 'uuid', name: 'uuid' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'created_at', | ||||
|                                 type: { id: 'timestamp', name: 'timestamp' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: true, | ||||
|                                 unique: false, | ||||
|                                 default: 'NOW', | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'updated_at', | ||||
|                                 type: { id: 'timestamp', name: 'timestamp' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: true, | ||||
|                                 unique: false, | ||||
|                                 default: "('now')", | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [], | ||||
|                         color: '#4169E1', | ||||
|                     }), | ||||
|                 ], | ||||
|                 relationships: [], | ||||
|             }); | ||||
|  | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
|  | ||||
|             // Should convert NOW to NOW() and ('now') to now() | ||||
|             expect(sql).toContain('created_at timestamp DEFAULT NOW'); | ||||
|             expect(sql).toContain('updated_at timestamp DEFAULT now()'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Character Type Handling', () => { | ||||
|         it('should handle char types with and without length correctly', () => { | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Dragon Registry', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [ | ||||
|                     createTable({ | ||||
|                         id: testId(), | ||||
|                         name: 'dragons', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'id', | ||||
|                                 type: { id: 'uuid', name: 'uuid' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'element_code', | ||||
|                                 type: { id: 'char', name: 'char' }, | ||||
|                                 characterMaximumLength: '2', | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'status', | ||||
|                                 type: { id: 'char', name: 'char' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [], | ||||
|                         color: '#FF6347', | ||||
|                     }), | ||||
|                 ], | ||||
|                 relationships: [], | ||||
|             }); | ||||
|  | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
|  | ||||
|             // Should handle char with explicit length | ||||
|             expect(sql).toContain('element_code char(2)'); | ||||
|             // Should add default length for char without length | ||||
|             expect(sql).toContain('status char(1)'); | ||||
|         }); | ||||
|  | ||||
|         it('should not have spaces between char and parentheses', () => { | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Rune Inscriptions', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [ | ||||
|                     createTable({ | ||||
|                         id: testId(), | ||||
|                         name: 'runes', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'id', | ||||
|                                 type: { id: 'integer', name: 'integer' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'symbol', | ||||
|                                 type: { id: 'char', name: 'char' }, | ||||
|                                 characterMaximumLength: '5', | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: true, | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [], | ||||
|                         color: '#8B4513', | ||||
|                     }), | ||||
|                 ], | ||||
|                 relationships: [], | ||||
|             }); | ||||
|  | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
|  | ||||
|             // Should not contain "char (" with space | ||||
|             expect(sql).not.toContain('char ('); | ||||
|             expect(sql).toContain('char(5)'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Complex Table Structures', () => { | ||||
|         it('should handle tables with no primary key', () => { | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Alchemy Log', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [ | ||||
|                     createTable({ | ||||
|                         id: testId(), | ||||
|                         name: 'experiment_logs', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'experiment_id', | ||||
|                                 type: { id: 'uuid', name: 'uuid' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'alchemist_id', | ||||
|                                 type: { id: 'uuid', name: 'uuid' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'result', | ||||
|                                 type: { id: 'text', name: 'text' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: true, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'logged_at', | ||||
|                                 type: { id: 'timestamp', name: 'timestamp' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                                 default: 'now()', | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [], | ||||
|                         color: '#32CD32', | ||||
|                     }), | ||||
|                 ], | ||||
|                 relationships: [], | ||||
|             }); | ||||
|  | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
|  | ||||
|             // Should create a valid table without primary key | ||||
|             expect(sql).toContain('CREATE TABLE "experiment_logs"'); | ||||
|             expect(sql).not.toContain('PRIMARY KEY'); | ||||
|         }); | ||||
|  | ||||
|         it('should handle multiple tables with relationships', () => { | ||||
|             const guildTableId = testId(); | ||||
|             const memberTableId = testId(); | ||||
|             const guildIdFieldId = testId(); | ||||
|             const memberGuildIdFieldId = testId(); | ||||
|  | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Adventurer Guild System', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [ | ||||
|                     createTable({ | ||||
|                         id: guildTableId, | ||||
|                         name: 'guilds', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: guildIdFieldId, | ||||
|                                 name: 'id', | ||||
|                                 type: { id: 'uuid', name: 'uuid' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'name', | ||||
|                                 type: { id: 'varchar', name: 'varchar' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: true, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'founded_year', | ||||
|                                 type: { id: 'integer', name: 'integer' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: true, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [], | ||||
|                         x: 0, | ||||
|                         y: 0, | ||||
|                         color: '#4169E1', | ||||
|                     }), | ||||
|                     createTable({ | ||||
|                         id: memberTableId, | ||||
|                         name: 'guild_members', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'id', | ||||
|                                 type: { id: 'uuid', name: 'uuid' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: memberGuildIdFieldId, | ||||
|                                 name: 'guild_id', | ||||
|                                 type: { id: 'uuid', name: 'uuid' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'member_name', | ||||
|                                 type: { id: 'varchar', name: 'varchar' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'rank', | ||||
|                                 type: { id: 'varchar', name: 'varchar' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: true, | ||||
|                                 unique: false, | ||||
|                                 default: "'Novice'", | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [], | ||||
|                         x: 250, | ||||
|                         y: 0, | ||||
|                         color: '#FFD700', | ||||
|                     }), | ||||
|                 ], | ||||
|                 relationships: [ | ||||
|                     { | ||||
|                         id: testId(), | ||||
|                         name: 'fk_guild_members_guild', | ||||
|                         sourceTableId: memberTableId, | ||||
|                         targetTableId: guildTableId, | ||||
|                         sourceFieldId: memberGuildIdFieldId, | ||||
|                         targetFieldId: guildIdFieldId, | ||||
|                         sourceCardinality: 'many', | ||||
|                         targetCardinality: 'one', | ||||
|                         createdAt: testTime, | ||||
|                     }, | ||||
|                 ], | ||||
|             }); | ||||
|  | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
|  | ||||
|             // Should create both tables | ||||
|             expect(sql).toContain('CREATE TABLE "guilds"'); | ||||
|             expect(sql).toContain('CREATE TABLE "guild_members"'); | ||||
|             // Should create foreign key | ||||
|             expect(sql).toContain( | ||||
|                 'ALTER TABLE "guild_members" ADD CONSTRAINT fk_guild_members_guild FOREIGN KEY (guild_id) REFERENCES "guilds" (id);' | ||||
|             ); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Schema Support', () => { | ||||
|         it('should handle tables with schemas correctly', () => { | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Multi-Realm Database', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [ | ||||
|                     createTable({ | ||||
|                         id: testId(), | ||||
|                         name: 'portals', | ||||
|                         schema: 'transportation', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'id', | ||||
|                                 type: { id: 'uuid', name: 'uuid' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'destination', | ||||
|                                 type: { id: 'varchar', name: 'varchar' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [], | ||||
|                         color: '#9370DB', | ||||
|                     }), | ||||
|                     createTable({ | ||||
|                         id: testId(), | ||||
|                         name: 'spells', | ||||
|                         schema: 'magic', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'id', | ||||
|                                 type: { id: 'integer', name: 'integer' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'name', | ||||
|                                 type: { id: 'varchar', name: 'varchar' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: true, | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [], | ||||
|                         x: 250, | ||||
|                         y: 0, | ||||
|                         color: '#FF1493', | ||||
|                     }), | ||||
|                 ], | ||||
|                 relationships: [], | ||||
|             }); | ||||
|  | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
|  | ||||
|             // Should use schema-qualified table names | ||||
|             expect(sql).toContain('CREATE TABLE "transportation"."portals"'); | ||||
|             expect(sql).toContain('CREATE TABLE "magic"."spells"'); | ||||
|         }); | ||||
|     }); | ||||
|  | ||||
|     describe('Edge Cases', () => { | ||||
|         it('should handle empty tables array', () => { | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Empty Realm', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [], | ||||
|                 relationships: [], | ||||
|             }); | ||||
|  | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
|  | ||||
|             expect(sql).toBe(''); | ||||
|         }); | ||||
|  | ||||
|         it('should handle tables with empty fields', () => { | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Void Space', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [ | ||||
|                     createTable({ | ||||
|                         id: testId(), | ||||
|                         name: 'empty_table', | ||||
|                         fields: [], | ||||
|                         indexes: [], | ||||
|                         color: '#000000', | ||||
|                     }), | ||||
|                 ], | ||||
|                 relationships: [], | ||||
|             }); | ||||
|  | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
|  | ||||
|             // Should still create table structure | ||||
|             expect(sql).toContain('CREATE TABLE "empty_table"'); | ||||
|             expect(sql).toContain('(\n\n)'); | ||||
|         }); | ||||
|  | ||||
|         it('should handle special characters in default values', () => { | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Mystic Scrolls', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [ | ||||
|                     createTable({ | ||||
|                         id: testId(), | ||||
|                         name: 'scrolls', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'id', | ||||
|                                 type: { id: 'uuid', name: 'uuid' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'inscription', | ||||
|                                 type: { id: 'text', name: 'text' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: true, | ||||
|                                 unique: false, | ||||
|                                 default: "'Ancient\\'s Wisdom'", | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [], | ||||
|                         color: '#8B4513', | ||||
|                     }), | ||||
|                 ], | ||||
|                 relationships: [], | ||||
|             }); | ||||
|  | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
|  | ||||
|             // Should preserve escaped quotes | ||||
|             expect(sql).toContain("DEFAULT 'Ancient\\'s Wisdom'"); | ||||
|         }); | ||||
|  | ||||
|         it('should handle numeric precision and scale', () => { | ||||
|             const diagram: Diagram = createDiagram({ | ||||
|                 id: testId(), | ||||
|                 name: 'Treasury', | ||||
|                 databaseType: DatabaseType.POSTGRESQL, | ||||
|                 tables: [ | ||||
|                     createTable({ | ||||
|                         id: testId(), | ||||
|                         name: 'gold_reserves', | ||||
|                         fields: [ | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'id', | ||||
|                                 type: { id: 'integer', name: 'integer' }, | ||||
|                                 primaryKey: true, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'amount', | ||||
|                                 type: { id: 'numeric', name: 'numeric' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: false, | ||||
|                                 unique: false, | ||||
|                                 precision: 15, | ||||
|                                 scale: 2, | ||||
|                             }), | ||||
|                             createField({ | ||||
|                                 id: testId(), | ||||
|                                 name: 'interest_rate', | ||||
|                                 type: { id: 'numeric', name: 'numeric' }, | ||||
|                                 primaryKey: false, | ||||
|                                 nullable: true, | ||||
|                                 unique: false, | ||||
|                                 precision: 5, | ||||
|                             }), | ||||
|                         ], | ||||
|                         indexes: [], | ||||
|                         color: '#FFD700', | ||||
|                     }), | ||||
|                 ], | ||||
|                 relationships: [], | ||||
|             }); | ||||
|  | ||||
|             const sql = exportBaseSQL({ | ||||
|                 diagram, | ||||
|                 targetDatabaseType: DatabaseType.POSTGRESQL, | ||||
|                 isDBMLFlow: true, | ||||
|             }); | ||||
|  | ||||
|             // Should include precision and scale | ||||
|             expect(sql).toContain('amount numeric(15, 2)'); | ||||
|             // Should include precision only when scale is not provided | ||||
|             expect(sql).toContain('interest_rate numeric(5)'); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -48,6 +48,50 @@ export function exportFieldComment(comment: string): string { | ||||
|         .join(''); | ||||
| } | ||||
|  | ||||
| export function escapeSQLComment(comment: string): string { | ||||
|     if (!comment) { | ||||
|         return ''; | ||||
|     } | ||||
|  | ||||
|     // Escape single quotes by doubling them | ||||
|     let escaped = comment.replace(/'/g, "''"); | ||||
|  | ||||
|     // Replace newlines with spaces to prevent breaking SQL syntax | ||||
|     // Some databases support multi-line comments with specific syntax, | ||||
|     // but for maximum compatibility, we'll replace newlines with spaces | ||||
|     escaped = escaped.replace(/[\r\n]+/g, ' '); | ||||
|  | ||||
|     // Trim any excessive whitespace | ||||
|     escaped = escaped.replace(/\s+/g, ' ').trim(); | ||||
|  | ||||
|     return escaped; | ||||
| } | ||||
|  | ||||
| export function formatTableComment(comment: string): string { | ||||
|     if (!comment) { | ||||
|         return ''; | ||||
|     } | ||||
|  | ||||
|     // Split by newlines and add -- to each line | ||||
|     return ( | ||||
|         comment | ||||
|             .split('\n') | ||||
|             .map((line) => `-- ${line}`) | ||||
|             .join('\n') + '\n' | ||||
|     ); | ||||
| } | ||||
|  | ||||
| export function formatMSSQLTableComment(comment: string): string { | ||||
|     if (!comment) { | ||||
|         return ''; | ||||
|     } | ||||
|  | ||||
|     // For MSSQL, we use multi-line comment syntax | ||||
|     // Escape */ to prevent breaking the comment block | ||||
|     const escaped = comment.replace(/\*\//g, '* /'); | ||||
|     return `/**\n${escaped}\n*/\n`; | ||||
| } | ||||
|  | ||||
| export function getInlineFK(table: DBTable, diagram: Diagram): string { | ||||
|     if (!diagram.relationships) { | ||||
|         return ''; | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { | ||||
|     exportFieldComment, | ||||
|     formatMSSQLTableComment, | ||||
|     isFunction, | ||||
|     isKeyword, | ||||
|     strHasQuotes, | ||||
| @@ -72,7 +73,13 @@ function parseMSSQLDefault(field: DBField): string { | ||||
|     return `'${defaultValue}'`; | ||||
| } | ||||
|  | ||||
| export function exportMSSQL(diagram: Diagram): string { | ||||
| export function exportMSSQL({ | ||||
|     diagram, | ||||
|     onlyRelationships = false, | ||||
| }: { | ||||
|     diagram: Diagram; | ||||
|     onlyRelationships?: boolean; | ||||
| }): string { | ||||
|     if (!diagram.tables || !diagram.relationships) { | ||||
|         return ''; | ||||
|     } | ||||
| @@ -82,166 +89,263 @@ export function exportMSSQL(diagram: Diagram): string { | ||||
|  | ||||
|     // Create CREATE SCHEMA statements for all schemas | ||||
|     let sqlScript = ''; | ||||
|     const schemas = new Set<string>(); | ||||
|  | ||||
|     tables.forEach((table) => { | ||||
|         if (table.schema) { | ||||
|             schemas.add(table.schema); | ||||
|         } | ||||
|     }); | ||||
|     if (!onlyRelationships) { | ||||
|         const schemas = new Set<string>(); | ||||
|  | ||||
|     // Add schema creation statements | ||||
|     schemas.forEach((schema) => { | ||||
|         sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n    EXEC('CREATE SCHEMA [${schema}]');\nEND;\n\n`; | ||||
|     }); | ||||
|  | ||||
|     // Generate table creation SQL | ||||
|     sqlScript += tables | ||||
|         .map((table: DBTable) => { | ||||
|             // Skip views | ||||
|             if (table.isView) { | ||||
|                 return ''; | ||||
|         tables.forEach((table) => { | ||||
|             if (table.schema) { | ||||
|                 schemas.add(table.schema); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|             const tableName = table.schema | ||||
|                 ? `[${table.schema}].[${table.name}]` | ||||
|                 : `[${table.name}]`; | ||||
|         // Add schema creation statements | ||||
|         schemas.forEach((schema) => { | ||||
|             sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n    EXEC('CREATE SCHEMA [${schema}]');\nEND;\n`; | ||||
|         }); | ||||
|  | ||||
|             return `${ | ||||
|                 table.comments ? `/**\n${table.comments}\n*/\n` : '' | ||||
|             }CREATE TABLE ${tableName} (\n${table.fields | ||||
|                 .map((field: DBField) => { | ||||
|                     const fieldName = `[${field.name}]`; | ||||
|                     const typeName = field.type.name; | ||||
|         // Generate table creation SQL | ||||
|         sqlScript += tables | ||||
|             .map((table: DBTable) => { | ||||
|                 // Skip views | ||||
|                 if (table.isView) { | ||||
|                     return ''; | ||||
|                 } | ||||
|  | ||||
|                     // Handle SQL Server specific type formatting | ||||
|                     let typeWithSize = typeName; | ||||
|                     if (field.characterMaximumLength) { | ||||
|                         if ( | ||||
|                             typeName.toLowerCase() === 'varchar' || | ||||
|                             typeName.toLowerCase() === 'nvarchar' || | ||||
|                             typeName.toLowerCase() === 'char' || | ||||
|                             typeName.toLowerCase() === 'nchar' | ||||
|                         ) { | ||||
|                             typeWithSize = `${typeName}(${field.characterMaximumLength})`; | ||||
|                 const tableName = table.schema | ||||
|                     ? `[${table.schema}].[${table.name}]` | ||||
|                     : `[${table.name}]`; | ||||
|  | ||||
|                 return `${ | ||||
|                     table.comments | ||||
|                         ? formatMSSQLTableComment(table.comments) | ||||
|                         : '' | ||||
|                 }CREATE TABLE ${tableName} (\n${table.fields | ||||
|                     .map((field: DBField) => { | ||||
|                         const fieldName = `[${field.name}]`; | ||||
|                         const typeName = field.type.name; | ||||
|  | ||||
|                         // Handle SQL Server specific type formatting | ||||
|                         let typeWithSize = typeName; | ||||
|                         if (field.characterMaximumLength) { | ||||
|                             if ( | ||||
|                                 typeName.toLowerCase() === 'varchar' || | ||||
|                                 typeName.toLowerCase() === 'nvarchar' || | ||||
|                                 typeName.toLowerCase() === 'char' || | ||||
|                                 typeName.toLowerCase() === 'nchar' | ||||
|                             ) { | ||||
|                                 typeWithSize = `${typeName}(${field.characterMaximumLength})`; | ||||
|                             } | ||||
|                         } | ||||
|                     } else if (field.precision && field.scale) { | ||||
|                         if ( | ||||
|                             typeName.toLowerCase() === 'decimal' || | ||||
|                             typeName.toLowerCase() === 'numeric' | ||||
|                         ) { | ||||
|                             typeWithSize = `${typeName}(${field.precision}, ${field.scale})`; | ||||
|                         if (field.precision && field.scale) { | ||||
|                             if ( | ||||
|                                 typeName.toLowerCase() === 'decimal' || | ||||
|                                 typeName.toLowerCase() === 'numeric' | ||||
|                             ) { | ||||
|                                 typeWithSize = `${typeName}(${field.precision}, ${field.scale})`; | ||||
|                             } | ||||
|                         } else if (field.precision) { | ||||
|                             if ( | ||||
|                                 typeName.toLowerCase() === 'decimal' || | ||||
|                                 typeName.toLowerCase() === 'numeric' | ||||
|                             ) { | ||||
|                                 typeWithSize = `${typeName}(${field.precision})`; | ||||
|                             } | ||||
|                         } | ||||
|                     } else if (field.precision) { | ||||
|                         if ( | ||||
|                             typeName.toLowerCase() === 'decimal' || | ||||
|                             typeName.toLowerCase() === 'numeric' | ||||
|                         ) { | ||||
|                             typeWithSize = `${typeName}(${field.precision})`; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     const notNull = field.nullable ? '' : ' NOT NULL'; | ||||
|                         const notNull = field.nullable ? '' : ' NOT NULL'; | ||||
|  | ||||
|                     // Check if identity column | ||||
|                     const identity = field.default | ||||
|                         ?.toLowerCase() | ||||
|                         .includes('identity') | ||||
|                         ? ' IDENTITY(1,1)' | ||||
|                         : ''; | ||||
|                         // Check if identity column | ||||
|                         const identity = | ||||
|                             field.increment || | ||||
|                             field.default?.toLowerCase().includes('identity') | ||||
|                                 ? ' IDENTITY(1,1)' | ||||
|                                 : ''; | ||||
|  | ||||
|                     const unique = | ||||
|                         !field.primaryKey && field.unique ? ' UNIQUE' : ''; | ||||
|                         const unique = | ||||
|                             !field.primaryKey && field.unique ? ' UNIQUE' : ''; | ||||
|  | ||||
|                     // Handle default value using SQL Server specific parser | ||||
|                     const defaultValue = | ||||
|                         field.default && | ||||
|                         !field.default.toLowerCase().includes('identity') | ||||
|                             ? ` DEFAULT ${parseMSSQLDefault(field)}` | ||||
|                             : ''; | ||||
|                         // Handle default value using SQL Server specific parser | ||||
|                         const defaultValue = | ||||
|                             field.default && | ||||
|                             !field.increment && | ||||
|                             !field.default.toLowerCase().includes('identity') | ||||
|                                 ? ` DEFAULT ${parseMSSQLDefault(field)}` | ||||
|                                 : ''; | ||||
|  | ||||
|                     // Do not add PRIMARY KEY as a column constraint - will add as table constraint | ||||
|                     return `${exportFieldComment(field.comments ?? '')}    ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`; | ||||
|                 }) | ||||
|                 .join(',\n')}${ | ||||
|                 table.fields.filter((f) => f.primaryKey).length > 0 | ||||
|                     ? `,\n    PRIMARY KEY (${table.fields | ||||
|                           .filter((f) => f.primaryKey) | ||||
|                           .map((f) => `[${f.name}]`) | ||||
|                           .join(', ')})` | ||||
|                     : '' | ||||
|             }\n);\n\n${table.indexes | ||||
|                 .map((index) => { | ||||
|                     const indexName = table.schema | ||||
|                         ? `[${table.schema}_${index.name}]` | ||||
|                         : `[${index.name}]`; | ||||
|                     const indexFields = index.fieldIds | ||||
|                         .map((fieldId) => { | ||||
|                             const field = table.fields.find( | ||||
|                                 (f) => f.id === fieldId | ||||
|                             ); | ||||
|                             return field ? `[${field.name}]` : ''; | ||||
|                         // Do not add PRIMARY KEY as a column constraint - will add as table constraint | ||||
|                         return `${exportFieldComment(field.comments ?? '')}    ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`; | ||||
|                     }) | ||||
|                     .join(',\n')}${ | ||||
|                     table.fields.filter((f) => f.primaryKey).length > 0 | ||||
|                         ? `,\n    ${(() => { | ||||
|                               // Find PK index to get the constraint name | ||||
|                               const pkIndex = table.indexes.find( | ||||
|                                   (idx) => idx.isPrimaryKey | ||||
|                               ); | ||||
|                               return pkIndex?.name | ||||
|                                   ? `CONSTRAINT [${pkIndex.name}] ` | ||||
|                                   : ''; | ||||
|                           })()}PRIMARY KEY (${table.fields | ||||
|                               .filter((f) => f.primaryKey) | ||||
|                               .map((f) => `[${f.name}]`) | ||||
|                               .join(', ')})` | ||||
|                         : '' | ||||
|                 }\n);\n${(() => { | ||||
|                     const validIndexes = table.indexes | ||||
|                         .map((index) => { | ||||
|                             const indexName = table.schema | ||||
|                                 ? `[${table.schema}_${index.name}]` | ||||
|                                 : `[${index.name}]`; | ||||
|                             const indexFields = index.fieldIds | ||||
|                                 .map((fieldId) => { | ||||
|                                     const field = table.fields.find( | ||||
|                                         (f) => f.id === fieldId | ||||
|                                     ); | ||||
|                                     return field ? `[${field.name}]` : ''; | ||||
|                                 }) | ||||
|                                 .filter(Boolean); | ||||
|  | ||||
|                             // SQL Server has a limit of 32 columns in an index | ||||
|                             if (indexFields.length > 32) { | ||||
|                                 const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`; | ||||
|                                 console.warn( | ||||
|                                     `Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.` | ||||
|                                 ); | ||||
|                                 indexFields.length = 32; | ||||
|                                 return indexFields.length > 0 | ||||
|                                     ? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});` | ||||
|                                     : ''; | ||||
|                             } | ||||
|  | ||||
|                             return indexFields.length > 0 | ||||
|                                 ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});` | ||||
|                                 : ''; | ||||
|                         }) | ||||
|                         .filter(Boolean); | ||||
|  | ||||
|                     // SQL Server has a limit of 32 columns in an index | ||||
|                     if (indexFields.length > 32) { | ||||
|                         const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`; | ||||
|                         console.warn( | ||||
|                             `Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.` | ||||
|                         ); | ||||
|                         indexFields.length = 32; | ||||
|                         return indexFields.length > 0 | ||||
|                             ? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n` | ||||
|                             : ''; | ||||
|                     } | ||||
|  | ||||
|                     return indexFields.length > 0 | ||||
|                         ? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n` | ||||
|                     return validIndexes.length > 0 | ||||
|                         ? `\n-- Indexes\n${validIndexes.join('\n')}` | ||||
|                         : ''; | ||||
|                 }) | ||||
|                 .join('')}`; | ||||
|         }) | ||||
|         .filter(Boolean) // Remove empty strings (views) | ||||
|         .join('\n'); | ||||
|                 })()}\n`; | ||||
|             }) | ||||
|             .filter(Boolean) // Remove empty strings (views) | ||||
|             .join('\n'); | ||||
|     } | ||||
|  | ||||
|     // Generate foreign keys | ||||
|     sqlScript += `\n${relationships | ||||
|         .map((r: DBRelationship) => { | ||||
|             const sourceTable = tables.find((t) => t.id === r.sourceTableId); | ||||
|             const targetTable = tables.find((t) => t.id === r.targetTableId); | ||||
|     if (relationships.length > 0) { | ||||
|         sqlScript += '\n-- Foreign key constraints\n'; | ||||
|  | ||||
|             if ( | ||||
|                 !sourceTable || | ||||
|                 !targetTable || | ||||
|                 sourceTable.isView || | ||||
|                 targetTable.isView | ||||
|             ) { | ||||
|                 return ''; | ||||
|             } | ||||
|         // Process all relationships and create FK objects with schema info | ||||
|         const foreignKeys = relationships | ||||
|             .map((r: DBRelationship) => { | ||||
|                 const sourceTable = tables.find( | ||||
|                     (t) => t.id === r.sourceTableId | ||||
|                 ); | ||||
|                 const targetTable = tables.find( | ||||
|                     (t) => t.id === r.targetTableId | ||||
|                 ); | ||||
|  | ||||
|             const sourceField = sourceTable.fields.find( | ||||
|                 (f) => f.id === r.sourceFieldId | ||||
|             ); | ||||
|             const targetField = targetTable.fields.find( | ||||
|                 (f) => f.id === r.targetFieldId | ||||
|             ); | ||||
|                 if ( | ||||
|                     !sourceTable || | ||||
|                     !targetTable || | ||||
|                     sourceTable.isView || | ||||
|                     targetTable.isView | ||||
|                 ) { | ||||
|                     return ''; | ||||
|                 } | ||||
|  | ||||
|             if (!sourceField || !targetField) { | ||||
|                 return ''; | ||||
|             } | ||||
|                 const sourceField = sourceTable.fields.find( | ||||
|                     (f) => f.id === r.sourceFieldId | ||||
|                 ); | ||||
|                 const targetField = targetTable.fields.find( | ||||
|                     (f) => f.id === r.targetFieldId | ||||
|                 ); | ||||
|  | ||||
|             const sourceTableName = sourceTable.schema | ||||
|                 ? `[${sourceTable.schema}].[${sourceTable.name}]` | ||||
|                 : `[${sourceTable.name}]`; | ||||
|             const targetTableName = targetTable.schema | ||||
|                 ? `[${targetTable.schema}].[${targetTable.name}]` | ||||
|                 : `[${targetTable.name}]`; | ||||
|                 if (!sourceField || !targetField) { | ||||
|                     return ''; | ||||
|                 } | ||||
|  | ||||
|             return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT [${r.name}] FOREIGN KEY([${sourceField.name}]) REFERENCES ${targetTableName}([${targetField.name}]);\n`; | ||||
|         }) | ||||
|         .filter(Boolean) // Remove empty strings | ||||
|         .join('\n')}`; | ||||
|                 // Determine which table should have the foreign key based on cardinality | ||||
|                 let fkTable, fkField, refTable, refField; | ||||
|  | ||||
|                 if ( | ||||
|                     r.sourceCardinality === 'one' && | ||||
|                     r.targetCardinality === 'many' | ||||
|                 ) { | ||||
|                     // FK goes on target table | ||||
|                     fkTable = targetTable; | ||||
|                     fkField = targetField; | ||||
|                     refTable = sourceTable; | ||||
|                     refField = sourceField; | ||||
|                 } else if ( | ||||
|                     r.sourceCardinality === 'many' && | ||||
|                     r.targetCardinality === 'one' | ||||
|                 ) { | ||||
|                     // FK goes on source table | ||||
|                     fkTable = sourceTable; | ||||
|                     fkField = sourceField; | ||||
|                     refTable = targetTable; | ||||
|                     refField = targetField; | ||||
|                 } else if ( | ||||
|                     r.sourceCardinality === 'one' && | ||||
|                     r.targetCardinality === 'one' | ||||
|                 ) { | ||||
|                     // For 1:1, FK can go on either side, but typically goes on the table that references the other | ||||
|                     // We'll keep the current behavior for 1:1 | ||||
|                     fkTable = sourceTable; | ||||
|                     fkField = sourceField; | ||||
|                     refTable = targetTable; | ||||
|                     refField = targetField; | ||||
|                 } else { | ||||
|                     // Many-to-many relationships need a junction table, skip for now | ||||
|                     return ''; | ||||
|                 } | ||||
|  | ||||
|                 const fkTableName = fkTable.schema | ||||
|                     ? `[${fkTable.schema}].[${fkTable.name}]` | ||||
|                     : `[${fkTable.name}]`; | ||||
|                 const refTableName = refTable.schema | ||||
|                     ? `[${refTable.schema}].[${refTable.name}]` | ||||
|                     : `[${refTable.name}]`; | ||||
|  | ||||
|                 return { | ||||
|                     schema: fkTable.schema || 'dbo', | ||||
|                     sql: `ALTER TABLE ${fkTableName} ADD CONSTRAINT [${r.name}] FOREIGN KEY([${fkField.name}]) REFERENCES ${refTableName}([${refField.name}]);`, | ||||
|                 }; | ||||
|             }) | ||||
|             .filter(Boolean); // Remove empty objects | ||||
|  | ||||
|         // Group foreign keys by schema | ||||
|         const fksBySchema = foreignKeys.reduce( | ||||
|             (acc, fk) => { | ||||
|                 if (!fk) return acc; | ||||
|                 const schema = fk.schema; | ||||
|                 if (!acc[schema]) { | ||||
|                     acc[schema] = []; | ||||
|                 } | ||||
|                 acc[schema].push(fk.sql); | ||||
|                 return acc; | ||||
|             }, | ||||
|             {} as Record<string, string[]> | ||||
|         ); | ||||
|  | ||||
|         // Sort schemas and generate SQL with separators | ||||
|         const sortedSchemas = Object.keys(fksBySchema).sort(); | ||||
|         const fkSql = sortedSchemas | ||||
|             .map((schema, index) => { | ||||
|                 const schemaFks = fksBySchema[schema].join('\n'); | ||||
|                 if (index === 0) { | ||||
|                     return `-- Schema: ${schema}\n${schemaFks}`; | ||||
|                 } else { | ||||
|                     return `\n-- Schema: ${schema}\n${schemaFks}`; | ||||
|                 } | ||||
|             }) | ||||
|             .join('\n'); | ||||
|  | ||||
|         sqlScript += fkSql; | ||||
|     } | ||||
|  | ||||
|     return sqlScript; | ||||
| } | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user