mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-10-31 03:53:55 +00:00 
			
		
		
		
	Compare commits
	
		
			13 Commits
		
	
	
		
			jf/fix_mss
			...
			v1.8.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 467ff697c9 | ||
|  | d6919f3033 | ||
|  | 56382a9fdc | ||
|  | e06eb2a48e | ||
|  | 543b716c77 | ||
|  | b55d631146 | ||
|  | ef118929ad | ||
|  | 68f48190c9 | ||
|  | bba265ad43 | ||
|  | cbc4e85a14 | ||
|  | 26a0a5b550 | ||
|  | b935b7f251 | ||
|  | a1c0cf102a | 
							
								
								
									
										19
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| @@ -1,5 +1,24 @@ | ||||
| # Changelog | ||||
|  | ||||
| ## [1.8.1](https://github.com/chartdb/chartdb/compare/v1.8.0...v1.8.1) (2025-03-02) | ||||
|  | ||||
|  | ||||
| ### Bug Fixes | ||||
|  | ||||
| * **add-docs:** add link to ChartDB documentation ([#597](https://github.com/chartdb/chartdb/issues/597)) ([b55d631](https://github.com/chartdb/chartdb/commit/b55d631146ff3a1f7d63c800d44b5d3d3a223c76)) | ||||
| * components config ([#591](https://github.com/chartdb/chartdb/issues/591)) ([cbc4e85](https://github.com/chartdb/chartdb/commit/cbc4e85a14e24a43f9ff470518f8fe2845046bdb)) | ||||
| * **docker config:** Environment Variable Handling and Configuration Logic ([#605](https://github.com/chartdb/chartdb/issues/605)) ([d6919f3](https://github.com/chartdb/chartdb/commit/d6919f30336cc846fe6e6505b5a5278aa14dcce6)) | ||||
| * **empty-state:** show diff buttons on import-dbml when triggered by empty ([#574](https://github.com/chartdb/chartdb/issues/574)) ([4834247](https://github.com/chartdb/chartdb/commit/48342471ac231922f2ca4455b74a9879127a54f1)) | ||||
| * **i18n:** add [FR] translation ([#579](https://github.com/chartdb/chartdb/issues/579)) ([ab89bad](https://github.com/chartdb/chartdb/commit/ab89bad6d544ba4c339a3360eeec7d29e5579511)) | ||||
| * **img-export:** add ChartDB watermark to exported image ([#588](https://github.com/chartdb/chartdb/issues/588)) ([b935b7f](https://github.com/chartdb/chartdb/commit/b935b7f25111d5f72b7f8d7c552a4ea5974f791e)) | ||||
| * **import-mssql:** fix import/export scripts to handle data correctly ([#598](https://github.com/chartdb/chartdb/issues/598)) ([e06eb2a](https://github.com/chartdb/chartdb/commit/e06eb2a48e6bd3bcf352f4bcf128214c7da4c1b1)) | ||||
| * **menu-backup:** update export to be backup ([#590](https://github.com/chartdb/chartdb/issues/590)) ([26a0a5b](https://github.com/chartdb/chartdb/commit/26a0a5b550ef5e47e89b00d0232dc98936f63f23)) | ||||
| * open create new diagram when there is no diagram ([#594](https://github.com/chartdb/chartdb/issues/594)) ([ef11892](https://github.com/chartdb/chartdb/commit/ef118929ad5d5cbfae0290061bd8ea30bd262496)) | ||||
| * **open diagram:** in case there is no diagram, opens the dialog ([#593](https://github.com/chartdb/chartdb/issues/593)) ([68f4819](https://github.com/chartdb/chartdb/commit/68f48190c93f155398cca15dd7af2a025de2d45f)) | ||||
| * **side-panel:** simplify how to add field and index ([#573](https://github.com/chartdb/chartdb/issues/573)) ([a1c0cf1](https://github.com/chartdb/chartdb/commit/a1c0cf102add4fb235e913e75078139b3961341b)) | ||||
| * **sql_server_export:** use sql server export ([#600](https://github.com/chartdb/chartdb/issues/600)) ([56382a9](https://github.com/chartdb/chartdb/commit/56382a9fdc5e3044f8811873dd8a79f590771896)) | ||||
| * **sqlite-import:** import nuallable columns correctly + add json type ([#571](https://github.com/chartdb/chartdb/issues/571)) ([deb2184](https://github.com/chartdb/chartdb/commit/deb218423f77f0c0945a93005696456f62b00ce3)) | ||||
|  | ||||
| ## [1.8.0](https://github.com/chartdb/chartdb/compare/v1.7.0...v1.8.0) (2025-02-13) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,17 +1,20 @@ | ||||
| { | ||||
|   "$schema": "https://ui.shadcn.com/schema.json", | ||||
|   "style": "new-york", | ||||
|   "rsc": false, | ||||
|   "tsx": true, | ||||
|   "tailwind": { | ||||
|     "config": "tailwind.config.js", | ||||
|     "css": "src/globals.css", | ||||
|     "baseColor": "slate", | ||||
|     "cssVariables": true, | ||||
|     "prefix": "" | ||||
|   }, | ||||
|   "aliases": { | ||||
|     "components": "src/components", | ||||
|     "utils": "@/lib/utils" | ||||
|   } | ||||
|     "$schema": "https://ui.shadcn.com/schema.json", | ||||
|     "style": "new-york", | ||||
|     "rsc": false, | ||||
|     "tsx": true, | ||||
|     "tailwind": { | ||||
|         "config": "tailwind.config.js", | ||||
|         "css": "src/globals.css", | ||||
|         "baseColor": "slate", | ||||
|         "cssVariables": true, | ||||
|         "prefix": "" | ||||
|     }, | ||||
|     "aliases": { | ||||
|         "components": "src/components", | ||||
|         "utils": "src/lib/utils", | ||||
|         "ui": "src/components/ui", | ||||
|         "lib": "src/lib", | ||||
|         "hooks": "src/hooks" | ||||
|     } | ||||
| } | ||||
							
								
								
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,12 +1,12 @@ | ||||
| { | ||||
|     "name": "chartdb", | ||||
|     "version": "1.8.0", | ||||
|     "version": "1.8.1", | ||||
|     "lockfileVersion": 3, | ||||
|     "requires": true, | ||||
|     "packages": { | ||||
|         "": { | ||||
|             "name": "chartdb", | ||||
|             "version": "1.8.0", | ||||
|             "version": "1.8.1", | ||||
|             "dependencies": { | ||||
|                 "@ai-sdk/openai": "^0.0.51", | ||||
|                 "@dbml/core": "^3.9.5", | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|     "name": "chartdb", | ||||
|     "private": true, | ||||
|     "version": "1.8.0", | ||||
|     "version": "1.8.1", | ||||
|     "type": "module", | ||||
|     "scripts": { | ||||
|         "dev": "vite", | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/e | ||||
| import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/import-diagram-dialog'; | ||||
| import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog'; | ||||
| import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog'; | ||||
| import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog'; | ||||
|  | ||||
| export interface DialogContext { | ||||
|     // Create diagram dialog | ||||
| @@ -15,7 +16,9 @@ export interface DialogContext { | ||||
|     closeCreateDiagramDialog: () => void; | ||||
|  | ||||
|     // Open diagram dialog | ||||
|     openOpenDiagramDialog: () => void; | ||||
|     openOpenDiagramDialog: ( | ||||
|         params?: Omit<OpenDiagramDialogProps, 'dialog'> | ||||
|     ) => void; | ||||
|     closeOpenDiagramDialog: () => void; | ||||
|  | ||||
|     // Export SQL dialog | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react'; | ||||
| import type { DialogContext } from './dialog-context'; | ||||
| import { dialogContext } from './dialog-context'; | ||||
| import { CreateDiagramDialog } from '@/dialogs/create-diagram-dialog/create-diagram-dialog'; | ||||
| import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog'; | ||||
| import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog'; | ||||
| import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog'; | ||||
| import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog'; | ||||
| @@ -27,6 +28,17 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | ||||
| }) => { | ||||
|     const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false); | ||||
|     const [openOpenDiagramDialog, setOpenOpenDiagramDialog] = useState(false); | ||||
|     const [openDiagramDialogParams, setOpenDiagramDialogParams] = | ||||
|         useState<Omit<OpenDiagramDialogProps, 'dialog'>>(); | ||||
|  | ||||
|     const openOpenDiagramDialogHandler: DialogContext['openOpenDiagramDialog'] = | ||||
|         useCallback( | ||||
|             (props) => { | ||||
|                 setOpenDiagramDialogParams(props); | ||||
|                 setOpenOpenDiagramDialog(true); | ||||
|             }, | ||||
|             [setOpenOpenDiagramDialog] | ||||
|         ); | ||||
|  | ||||
|     const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] = | ||||
|         useState(false); | ||||
| @@ -120,7 +132,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|             value={{ | ||||
|                 openCreateDiagramDialog: () => setOpenNewDiagramDialog(true), | ||||
|                 closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false), | ||||
|                 openOpenDiagramDialog: () => setOpenOpenDiagramDialog(true), | ||||
|                 openOpenDiagramDialog: openOpenDiagramDialogHandler, | ||||
|                 closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false), | ||||
|                 openExportSQLDialog: openExportSQLDialogHandler, | ||||
|                 closeExportSQLDialog: () => setOpenExportSQLDialog(false), | ||||
| @@ -154,7 +166,10 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|         > | ||||
|             {children} | ||||
|             <CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} /> | ||||
|             <OpenDiagramDialog dialog={{ open: openOpenDiagramDialog }} /> | ||||
|             <OpenDiagramDialog | ||||
|                 dialog={{ open: openOpenDiagramDialog }} | ||||
|                 {...openDiagramDialogParams} | ||||
|             /> | ||||
|             <ExportSQLDialog | ||||
|                 dialog={{ open: openExportSQLDialog }} | ||||
|                 {...exportSQLDialogParams} | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import React, { useCallback, useMemo } from 'react'; | ||||
| import React, { useCallback, useMemo, useEffect, useState } from 'react'; | ||||
| import type { ExportImageContext, ImageType } from './export-image-context'; | ||||
| import { exportImageContext } from './export-image-context'; | ||||
| import { toJpeg, toPng, toSvg } from 'html-to-image'; | ||||
| @@ -6,6 +6,8 @@ import { useReactFlow } from '@xyflow/react'; | ||||
| import { useChartDB } from '@/hooks/use-chartdb'; | ||||
| import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner'; | ||||
| import { useTheme } from '@/hooks/use-theme'; | ||||
| import logoDark from '@/assets/logo-dark.png'; | ||||
| import logoLight from '@/assets/logo-light.png'; | ||||
|  | ||||
| export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     children, | ||||
| @@ -14,6 +16,24 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     const { setNodes, getViewport } = useReactFlow(); | ||||
|     const { effectiveTheme } = useTheme(); | ||||
|     const { diagramName } = useChartDB(); | ||||
|     const [logoBase64, setLogoBase64] = useState<string>(''); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         // Convert logo to base64 on component mount | ||||
|         const img = new Image(); | ||||
|         img.src = effectiveTheme === 'light' ? logoLight : logoDark; | ||||
|         img.onload = () => { | ||||
|             const canvas = document.createElement('canvas'); | ||||
|             canvas.width = img.width; | ||||
|             canvas.height = img.height; | ||||
|             const ctx = canvas.getContext('2d'); | ||||
|             if (ctx) { | ||||
|                 ctx.drawImage(img, 0, 0); | ||||
|                 const base64 = canvas.toDataURL('image/png'); | ||||
|                 setLogoBase64(base64); | ||||
|             } | ||||
|         }; | ||||
|     }, [effectiveTheme]); | ||||
|  | ||||
|     const downloadImage = useCallback( | ||||
|         (dataUrl: string, type: ImageType) => { | ||||
| @@ -128,16 +148,22 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                     'http://www.w3.org/2000/svg', | ||||
|                     'rect' | ||||
|                 ); | ||||
|                 const padding = 2000; | ||||
|                 backgroundRect.setAttribute('x', String(-viewport.x - padding)); | ||||
|                 backgroundRect.setAttribute('y', String(-viewport.y - padding)); | ||||
|                 const bgPadding = 2000; | ||||
|                 backgroundRect.setAttribute( | ||||
|                     'x', | ||||
|                     String(-viewport.x - bgPadding) | ||||
|                 ); | ||||
|                 backgroundRect.setAttribute( | ||||
|                     'y', | ||||
|                     String(-viewport.y - bgPadding) | ||||
|                 ); | ||||
|                 backgroundRect.setAttribute( | ||||
|                     'width', | ||||
|                     String(reactFlowBounds.width + 2 * padding) | ||||
|                     String(reactFlowBounds.width + 2 * bgPadding) | ||||
|                 ); | ||||
|                 backgroundRect.setAttribute( | ||||
|                     'height', | ||||
|                     String(reactFlowBounds.height + 2 * padding) | ||||
|                     String(reactFlowBounds.height + 2 * bgPadding) | ||||
|                 ); | ||||
|                 backgroundRect.setAttribute('fill', 'url(#background-pattern)'); | ||||
|                 tempSvg.appendChild(backgroundRect); | ||||
| @@ -148,28 +174,110 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|                 ); | ||||
|  | ||||
|                 try { | ||||
|                     const dataUrl = await imageCreateFn(viewportElement, { | ||||
|                         ...(type === 'jpeg' || type === 'png' | ||||
|                             ? { | ||||
|                                   backgroundColor: | ||||
|                                       effectiveTheme === 'light' | ||||
|                                           ? '#ffffff' | ||||
|                                           : '#141414', | ||||
|                               } | ||||
|                             : {}), | ||||
|                         width: reactFlowBounds.width, | ||||
|                         height: reactFlowBounds.height, | ||||
|                         style: { | ||||
|                             width: `${reactFlowBounds.width}px`, | ||||
|                             height: `${reactFlowBounds.height}px`, | ||||
|                             transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`, | ||||
|                         }, | ||||
|                         quality: 1, | ||||
|                         pixelRatio: scale, | ||||
|                         skipFonts: true, | ||||
|                     }); | ||||
|                     // Handle SVG export differently | ||||
|                     if (type === 'svg') { | ||||
|                         const dataUrl = await imageCreateFn(viewportElement, { | ||||
|                             width: reactFlowBounds.width, | ||||
|                             height: reactFlowBounds.height, | ||||
|                             style: { | ||||
|                                 width: `${reactFlowBounds.width}px`, | ||||
|                                 height: `${reactFlowBounds.height}px`, | ||||
|                                 transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`, | ||||
|                             }, | ||||
|                             quality: 1, | ||||
|                             pixelRatio: scale, | ||||
|                             skipFonts: true, | ||||
|                         }); | ||||
|                         downloadImage(dataUrl, type); | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
|                     downloadImage(dataUrl, type); | ||||
|                     // For PNG and JPEG, continue with the watermark process | ||||
|                     const initialDataUrl = await imageCreateFn( | ||||
|                         viewportElement, | ||||
|                         { | ||||
|                             backgroundColor: | ||||
|                                 effectiveTheme === 'light' | ||||
|                                     ? '#ffffff' | ||||
|                                     : '#141414', | ||||
|                             width: reactFlowBounds.width, | ||||
|                             height: reactFlowBounds.height, | ||||
|                             style: { | ||||
|                                 width: `${reactFlowBounds.width}px`, | ||||
|                                 height: `${reactFlowBounds.height}px`, | ||||
|                                 transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`, | ||||
|                             }, | ||||
|                             quality: 1, | ||||
|                             pixelRatio: scale, | ||||
|                             skipFonts: true, | ||||
|                         } | ||||
|                     ); | ||||
|  | ||||
|                     // Create a canvas to combine the diagram and watermark | ||||
|                     const canvas = document.createElement('canvas'); | ||||
|                     const ctx = canvas.getContext('2d'); | ||||
|  | ||||
|                     if (!ctx) { | ||||
|                         downloadImage(initialDataUrl, type); | ||||
|                         return; | ||||
|                     } | ||||
|  | ||||
|                     // Set canvas size to match the export size | ||||
|                     canvas.width = reactFlowBounds.width * scale; | ||||
|                     canvas.height = reactFlowBounds.height * scale; | ||||
|  | ||||
|                     // Load the exported diagram | ||||
|                     const diagramImage = new Image(); | ||||
|                     diagramImage.src = initialDataUrl; | ||||
|  | ||||
|                     await new Promise((resolve) => { | ||||
|                         diagramImage.onload = async () => { | ||||
|                             // Draw the diagram | ||||
|                             ctx.drawImage(diagramImage, 0, 0); | ||||
|  | ||||
|                             // Calculate logo size | ||||
|                             const logoHeight = Math.max( | ||||
|                                 24, | ||||
|                                 Math.floor(canvas.width * 0.024) | ||||
|                             ); | ||||
|                             const padding = Math.max( | ||||
|                                 12, | ||||
|                                 Math.floor(logoHeight * 0.5) | ||||
|                             ); | ||||
|  | ||||
|                             // Load and draw the logo | ||||
|                             const logoImage = new Image(); | ||||
|                             logoImage.src = logoBase64; | ||||
|  | ||||
|                             await new Promise((resolve) => { | ||||
|                                 logoImage.onload = () => { | ||||
|                                     // Calculate logo width while maintaining aspect ratio | ||||
|                                     const logoWidth = | ||||
|                                         (logoImage.width / logoImage.height) * | ||||
|                                         logoHeight; | ||||
|  | ||||
|                                     // Draw logo in bottom-left corner | ||||
|                                     ctx.globalAlpha = 0.9; | ||||
|                                     ctx.drawImage( | ||||
|                                         logoImage, | ||||
|                                         padding, | ||||
|                                         canvas.height - logoHeight - padding, | ||||
|                                         logoWidth, | ||||
|                                         logoHeight | ||||
|                                     ); | ||||
|                                     ctx.globalAlpha = 1; | ||||
|                                     resolve(null); | ||||
|                                 }; | ||||
|                             }); | ||||
|  | ||||
|                             // Convert canvas to data URL | ||||
|                             const finalDataUrl = canvas.toDataURL( | ||||
|                                 type === 'png' ? 'image/png' : 'image/jpeg' | ||||
|                             ); | ||||
|                             downloadImage(finalDataUrl, type); | ||||
|                             resolve(null); | ||||
|                         }; | ||||
|                     }); | ||||
|                 } finally { | ||||
|                     viewportElement.removeChild(tempSvg); | ||||
|                     hideLoader(); | ||||
| @@ -184,6 +292,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|             setNodes, | ||||
|             showLoader, | ||||
|             effectiveTheme, | ||||
|             logoBase64, | ||||
|         ] | ||||
|     ); | ||||
|  | ||||
|   | ||||
| @@ -39,7 +39,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({ | ||||
|     useHotkeys( | ||||
|         keyboardShortcutsForOS[KeyboardShortcutAction.OPEN_DIAGRAM] | ||||
|             .keyCombination, | ||||
|         openOpenDiagramDialog, | ||||
|         () => openOpenDiagramDialog(), | ||||
|         { | ||||
|             preventDefault: true, | ||||
|         }, | ||||
|   | ||||
| @@ -15,11 +15,10 @@ import { SelectBox } from '@/components/select-box/select-box'; | ||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| import { useChartDB } from '@/hooks/use-chartdb'; | ||||
| import { diagramToJSONOutput } from '@/lib/export-import-utils'; | ||||
| import { Spinner } from '@/components/spinner/spinner'; | ||||
| import { waitFor } from '@/lib/utils'; | ||||
| import { AlertCircle } from 'lucide-react'; | ||||
| import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert'; | ||||
| import { useExportDiagram } from '@/hooks/use-export-diagram'; | ||||
|  | ||||
| export interface ExportDiagramDialogProps extends BaseDialogProps {} | ||||
|  | ||||
| @@ -27,44 +26,27 @@ export const ExportDiagramDialog: React.FC<ExportDiagramDialogProps> = ({ | ||||
|     dialog, | ||||
| }) => { | ||||
|     const { t } = useTranslation(); | ||||
|     const { diagramName, currentDiagram } = useChartDB(); | ||||
|     const [isLoading, setIsLoading] = useState(false); | ||||
|     const { currentDiagram } = useChartDB(); | ||||
|     const { closeExportDiagramDialog } = useDialog(); | ||||
|     const [error, setError] = useState(false); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (!dialog.open) return; | ||||
|         setIsLoading(false); | ||||
|         setError(false); | ||||
|     }, [dialog.open]); | ||||
|  | ||||
|     const downloadOutput = useCallback( | ||||
|         (dataUrl: string) => { | ||||
|             const a = document.createElement('a'); | ||||
|             a.setAttribute('download', `ChartDB(${diagramName}).json`); | ||||
|             a.setAttribute('href', dataUrl); | ||||
|             a.click(); | ||||
|         }, | ||||
|         [diagramName] | ||||
|     ); | ||||
|     const { exportDiagram, isExporting: isLoading } = useExportDiagram(); | ||||
|  | ||||
|     const handleExport = useCallback(async () => { | ||||
|         setIsLoading(true); | ||||
|         await waitFor(1000); | ||||
|         try { | ||||
|             const json = diagramToJSONOutput(currentDiagram); | ||||
|             const blob = new Blob([json], { type: 'application/json' }); | ||||
|             const dataUrl = URL.createObjectURL(blob); | ||||
|             downloadOutput(dataUrl); | ||||
|             setIsLoading(false); | ||||
|             await exportDiagram({ diagram: currentDiagram }); | ||||
|             closeExportDiagramDialog(); | ||||
|         } catch (e) { | ||||
|             setError(true); | ||||
|             setIsLoading(false); | ||||
|  | ||||
|             throw e; | ||||
|         } | ||||
|     }, [downloadOutput, currentDiagram, closeExportDiagramDialog]); | ||||
|     }, [exportDiagram, currentDiagram, closeExportDiagramDialog]); | ||||
|  | ||||
|     const outputTypeOptions: SelectBoxOption[] = useMemo( | ||||
|         () => | ||||
|   | ||||
| @@ -28,10 +28,13 @@ import { useNavigate } from 'react-router-dom'; | ||||
| import type { BaseDialogProps } from '../common/base-dialog-props'; | ||||
| import { useDebounce } from '@/hooks/use-debounce'; | ||||
|  | ||||
| export interface OpenDiagramDialogProps extends BaseDialogProps {} | ||||
| export interface OpenDiagramDialogProps extends BaseDialogProps { | ||||
|     canClose?: boolean; | ||||
| } | ||||
|  | ||||
| export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | ||||
|     dialog, | ||||
|     canClose = true, | ||||
| }) => { | ||||
|     const { closeOpenDiagramDialog } = useDialog(); | ||||
|     const { t } = useTranslation(); | ||||
| @@ -122,14 +125,14 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | ||||
|         <Dialog | ||||
|             {...dialog} | ||||
|             onOpenChange={(open) => { | ||||
|                 if (!open) { | ||||
|                 if (!open && canClose) { | ||||
|                     closeOpenDiagramDialog(); | ||||
|                 } | ||||
|             }} | ||||
|         > | ||||
|             <DialogContent | ||||
|                 className="flex h-[30rem] max-h-screen flex-col overflow-y-auto md:min-w-[80vw] xl:min-w-[55vw]" | ||||
|                 showClose | ||||
|                 showClose={canClose} | ||||
|             > | ||||
|                 <DialogHeader> | ||||
|                     <DialogTitle>{t('open_diagram_dialog.title')}</DialogTitle> | ||||
| @@ -226,11 +229,15 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({ | ||||
|                 </DialogInternalContent> | ||||
|  | ||||
|                 <DialogFooter className="flex !justify-between gap-2"> | ||||
|                     <DialogClose asChild> | ||||
|                         <Button type="button" variant="secondary"> | ||||
|                             {t('open_diagram_dialog.cancel')} | ||||
|                         </Button> | ||||
|                     </DialogClose> | ||||
|                     {canClose ? ( | ||||
|                         <DialogClose asChild> | ||||
|                             <Button type="button" variant="secondary"> | ||||
|                                 {t('open_diagram_dialog.cancel')} | ||||
|                             </Button> | ||||
|                         </DialogClose> | ||||
|                     ) : ( | ||||
|                         <div /> | ||||
|                     )} | ||||
|                     <DialogClose asChild> | ||||
|                         <Button | ||||
|                             type="submit" | ||||
|   | ||||
							
								
								
									
										40
									
								
								src/hooks/use-export-diagram.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/hooks/use-export-diagram.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import { useCallback, useState } from 'react'; | ||||
| import { useDialog } from '@/hooks/use-dialog'; | ||||
| import { diagramToJSONOutput } from '@/lib/export-import-utils'; | ||||
| import { waitFor } from '@/lib/utils'; | ||||
| import type { Diagram } from '@/lib/domain/diagram'; | ||||
|  | ||||
| export const useExportDiagram = () => { | ||||
|     const [isLoading, setIsLoading] = useState(false); | ||||
|     const { closeExportDiagramDialog } = useDialog(); | ||||
|  | ||||
|     const downloadOutput = useCallback((name: string, dataUrl: string) => { | ||||
|         const a = document.createElement('a'); | ||||
|         a.setAttribute('download', `ChartDB(${name}).json`); | ||||
|         a.setAttribute('href', dataUrl); | ||||
|         a.click(); | ||||
|     }, []); | ||||
|  | ||||
|     const handleExport = useCallback( | ||||
|         async ({ diagram }: { diagram: Diagram }) => { | ||||
|             setIsLoading(true); | ||||
|             await waitFor(1000); | ||||
|             try { | ||||
|                 const json = diagramToJSONOutput(diagram); | ||||
|                 const blob = new Blob([json], { type: 'application/json' }); | ||||
|                 const dataUrl = URL.createObjectURL(blob); | ||||
|                 downloadOutput(diagram.name, dataUrl); | ||||
|                 setIsLoading(false); | ||||
|                 closeExportDiagramDialog(); | ||||
|             } finally { | ||||
|                 setIsLoading(false); | ||||
|             } | ||||
|         }, | ||||
|         [downloadOutput, closeExportDiagramDialog] | ||||
|     ); | ||||
|  | ||||
|     return { | ||||
|         exportDiagram: handleExport, | ||||
|         isExporting: isLoading, | ||||
|     }; | ||||
| }; | ||||
| @@ -34,13 +34,14 @@ export const ar: LanguageTranslation = { | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             share: { | ||||
|                 share: 'مشاركة', | ||||
|             backup: { | ||||
|                 backup: 'النسخ الاحتياطي', | ||||
|                 export_diagram: 'تصدير المخطط', | ||||
|                 import_diagram: 'استيراد المخطط', | ||||
|                 restore_diagram: 'استعادة المخطط', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'مساعدة', | ||||
|                 docs_website: 'الوثائق', | ||||
|                 visit_website: 'ChartDB قم بزيارة', | ||||
|                 join_discord: 'Discord انضم إلينا على', | ||||
|                 schedule_a_call: '!تحدث معنا', | ||||
|   | ||||
| @@ -35,13 +35,14 @@ export const bn: LanguageTranslation = { | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|  | ||||
|             share: { | ||||
|                 share: 'শেয়ার করুন', | ||||
|             backup: { | ||||
|                 backup: 'ব্যাকআপ', | ||||
|                 export_diagram: 'ডায়াগ্রাম রপ্তানি করুন', | ||||
|                 import_diagram: 'ডায়াগ্রাম আমদানি করুন', | ||||
|                 restore_diagram: 'ডায়াগ্রাম পুনরুদ্ধার করুন', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'সাহায্য', | ||||
|                 docs_website: 'ডকুমেন্টেশন', | ||||
|                 visit_website: 'ChartDB ওয়েবসাইটে যান', | ||||
|                 join_discord: 'আমাদের Discord-এ যোগ দিন', | ||||
|                 schedule_a_call: 'আমাদের সাথে কথা বলুন!', | ||||
|   | ||||
| @@ -35,13 +35,14 @@ export const de: LanguageTranslation = { | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             // TODO: Translate | ||||
|             share: { | ||||
|                 share: 'Share', | ||||
|             backup: { | ||||
|                 backup: 'Backup', | ||||
|                 export_diagram: 'Export Diagram', | ||||
|                 import_diagram: 'Import Diagram', | ||||
|                 restore_diagram: 'Restore Diagram', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'Hilfe', | ||||
|                 docs_website: 'Dokumentation', | ||||
|                 visit_website: 'ChartDB Webseite', | ||||
|                 join_discord: 'Auf Discord beitreten', | ||||
|                 schedule_a_call: 'Gespräch vereinbaren', | ||||
|   | ||||
| @@ -33,13 +33,14 @@ export const en = { | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             share: { | ||||
|                 share: 'Share', | ||||
|             backup: { | ||||
|                 backup: 'Backup', | ||||
|                 export_diagram: 'Export Diagram', | ||||
|                 import_diagram: 'Import Diagram', | ||||
|                 restore_diagram: 'Restore Diagram', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'Help', | ||||
|                 docs_website: 'Docs', | ||||
|                 visit_website: 'Visit ChartDB', | ||||
|                 join_discord: 'Join us on Discord', | ||||
|                 schedule_a_call: 'Talk with us!', | ||||
|   | ||||
| @@ -34,14 +34,14 @@ export const es: LanguageTranslation = { | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             // TODO: Translate | ||||
|             share: { | ||||
|                 share: 'Share', | ||||
|                 export_diagram: 'Export Diagram', | ||||
|                 import_diagram: 'Import Diagram', | ||||
|             backup: { | ||||
|                 backup: 'Respaldo', | ||||
|                 export_diagram: 'Exportar Diagrama', | ||||
|                 restore_diagram: 'Restaurar Diagrama', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'Ayuda', | ||||
|                 docs_website: 'Documentación', | ||||
|                 visit_website: 'Visitar ChartDB', | ||||
|                 join_discord: 'Únete a nosotros en Discord', | ||||
|                 schedule_a_call: '¡Habla con nosotros!', | ||||
|   | ||||
| @@ -33,13 +33,14 @@ export const fr: LanguageTranslation = { | ||||
|                 show_minimap: 'Afficher la Mini Carte', | ||||
|                 hide_minimap: 'Masquer la Mini Carte', | ||||
|             }, | ||||
|             share: { | ||||
|                 share: 'Partage', | ||||
|             backup: { | ||||
|                 backup: 'Sauvegarde', | ||||
|                 export_diagram: 'Exporter le diagramme', | ||||
|                 import_diagram: 'Importer un diagramme', | ||||
|                 restore_diagram: 'Restaurer le diagramme', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'Aide', | ||||
|                 docs_website: 'Documentation', | ||||
|                 visit_website: 'Visitez ChartDB', | ||||
|                 join_discord: 'Rejoignez-nous sur Discord', | ||||
|                 schedule_a_call: 'Parlez avec nous !', | ||||
|   | ||||
| @@ -35,13 +35,14 @@ export const gu: LanguageTranslation = { | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|  | ||||
|             share: { | ||||
|                 share: 'શેર કરો', | ||||
|             backup: { | ||||
|                 backup: 'બેકઅપ', | ||||
|                 export_diagram: 'ડાયાગ્રામ નિકાસ કરો', | ||||
|                 import_diagram: 'ડાયાગ્રામ આયાત કરો', | ||||
|                 restore_diagram: 'ડાયાગ્રામ પુનઃસ્થાપિત કરો', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'મદદ', | ||||
|                 docs_website: 'દસ્તાવેજીકરણ', | ||||
|                 visit_website: 'ChartDB વેબસાઇટ પર જાઓ', | ||||
|                 join_discord: 'અમારા Discordમાં જોડાઓ', | ||||
|                 schedule_a_call: 'અમારી સાથે વાત કરો!', | ||||
|   | ||||
| @@ -34,14 +34,14 @@ export const hi: LanguageTranslation = { | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             // TODO: Translate | ||||
|             share: { | ||||
|                 share: 'Share', | ||||
|                 export_diagram: 'Export Diagram', | ||||
|                 import_diagram: 'Import Diagram', | ||||
|             backup: { | ||||
|                 backup: 'बैकअप', | ||||
|                 export_diagram: 'आरेख निर्यात करें', | ||||
|                 restore_diagram: 'आरेख पुनर्स्थापित करें', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'मदद', | ||||
|                 docs_website: 'દસ્તાવેજીકરણ', | ||||
|                 visit_website: 'ChartDB वेबसाइट पर जाएँ', | ||||
|                 join_discord: 'हमसे Discord पर जुड़ें', | ||||
|                 schedule_a_call: 'हमसे बात करें!', | ||||
|   | ||||
| @@ -34,13 +34,14 @@ export const id_ID: LanguageTranslation = { | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             share: { | ||||
|                 share: 'Bagikan', | ||||
|             backup: { | ||||
|                 backup: 'Cadangan', | ||||
|                 export_diagram: 'Ekspor Diagram', | ||||
|                 import_diagram: 'Impor Diagram', | ||||
|                 restore_diagram: 'Pulihkan Diagram', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'Bantuan', | ||||
|                 docs_website: 'દસ્તાવેજીકરણ', | ||||
|                 visit_website: 'Kunjungi ChartDB', | ||||
|                 join_discord: 'Bergabunglah di Discord kami', | ||||
|                 schedule_a_call: 'Berbicara dengan kami!', | ||||
|   | ||||
| @@ -36,13 +36,14 @@ export const ja: LanguageTranslation = { | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             // TODO: Translate | ||||
|             share: { | ||||
|                 share: 'Share', | ||||
|             backup: { | ||||
|                 backup: 'Backup', | ||||
|                 export_diagram: 'Export Diagram', | ||||
|                 import_diagram: 'Import Diagram', | ||||
|                 restore_diagram: 'Restore Diagram', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'ヘルプ', | ||||
|                 docs_website: 'ドキュメント', | ||||
|                 visit_website: 'ChartDBにアクセス', | ||||
|                 join_discord: 'Discordに参加', | ||||
|                 schedule_a_call: '話しかけてください!', | ||||
|   | ||||
| @@ -34,13 +34,14 @@ export const ko_KR: LanguageTranslation = { | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             share: { | ||||
|                 share: '공유', | ||||
|             backup: { | ||||
|                 backup: '백업', | ||||
|                 export_diagram: '다이어그램 내보내기', | ||||
|                 import_diagram: '다이어그램 가져오기', | ||||
|                 restore_diagram: '다이어그램 복구', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: '도움말', | ||||
|                 docs_website: '선적 서류 비치', | ||||
|                 visit_website: 'ChartDB 사이트 방문', | ||||
|                 join_discord: 'Discord 가입', | ||||
|                 schedule_a_call: 'Talk with us!', | ||||
|   | ||||
| @@ -34,14 +34,15 @@ export const mr: LanguageTranslation = { | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             share: { | ||||
|             backup: { | ||||
|                 // TODO: Add translations | ||||
|                 share: 'Share', | ||||
|                 backup: 'Backup', | ||||
|                 export_diagram: 'Export Diagram', | ||||
|                 import_diagram: 'Import Diagram', | ||||
|                 restore_diagram: 'Restore Diagram', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'मदत', | ||||
|                 docs_website: 'दस्तऐवजीकरण', | ||||
|                 visit_website: 'ChartDB ला भेट द्या', | ||||
|                 join_discord: 'आमच्या डिस्कॉर्डमध्ये सामील व्हा', | ||||
|                 schedule_a_call: 'आमच्याशी बोला!', | ||||
|   | ||||
| @@ -34,13 +34,15 @@ export const ne: LanguageTranslation = { | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             share: { | ||||
|                 share: 'शेयर गर्नुहोस्', | ||||
|                 export_diagram: 'डायाग्राम निर्यात गर्नुहोस्', | ||||
|                 import_diagram: 'डायाग्राम आयात गर्नुहोस्', | ||||
|             // TODO: Translate | ||||
|             backup: { | ||||
|                 backup: 'Backup', | ||||
|                 export_diagram: 'Export Diagram', | ||||
|                 restore_diagram: 'Restore Diagram', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'मद्दत', | ||||
|                 docs_website: 'कागजात', | ||||
|                 visit_website: 'वेबसाइटमा जानुहोस्', | ||||
|                 join_discord: 'डिस्कोर्डमा सामिल हुनुहोस्', | ||||
|                 schedule_a_call: 'कल अनुसूची गर्नुहोस्', | ||||
|   | ||||
| @@ -35,13 +35,14 @@ export const pt_BR: LanguageTranslation = { | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             // TODO: Translate | ||||
|             share: { | ||||
|                 share: 'Share', | ||||
|                 export_diagram: 'Export Diagram', | ||||
|                 import_diagram: 'Import Diagram', | ||||
|             backup: { | ||||
|                 backup: 'Backup', | ||||
|                 export_diagram: 'Exportar Diagrama', | ||||
|                 restore_diagram: 'Restaurar Diagrama', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'Ajuda', | ||||
|                 docs_website: 'Documentação', | ||||
|                 visit_website: 'Visitar ChartDB', | ||||
|                 join_discord: 'Junte-se a nós no Discord', | ||||
|                 schedule_a_call: 'Fale Conosco!', | ||||
|   | ||||
| @@ -34,13 +34,15 @@ export const ru: LanguageTranslation = { | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             share: { | ||||
|                 share: 'Поделиться', | ||||
|                 export_diagram: 'Экспорт кода диаграммы', | ||||
|                 import_diagram: 'Импорт кода диаграммы', | ||||
|             // TODO: Translate | ||||
|             backup: { | ||||
|                 backup: 'Backup', | ||||
|                 export_diagram: 'Export Diagram', | ||||
|                 restore_diagram: 'Restore Diagram', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'Помощь', | ||||
|                 docs_website: 'Документация', | ||||
|                 visit_website: 'Перейти на сайт ChartDB', | ||||
|                 join_discord: 'Присоединиться к сообществу в Discord', | ||||
|                 schedule_a_call: 'Поговорите с нами!', | ||||
|   | ||||
| @@ -35,13 +35,14 @@ export const te: LanguageTranslation = { | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             // TODO: Translate | ||||
|             share: { | ||||
|                 share: 'Share', | ||||
|             backup: { | ||||
|                 backup: 'Backup', | ||||
|                 export_diagram: 'Export Diagram', | ||||
|                 import_diagram: 'Import Diagram', | ||||
|                 restore_diagram: 'Restore Diagram', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'సహాయం', | ||||
|                 docs_website: 'డాక్యుమెంటేషన్', | ||||
|                 visit_website: 'ChartDB సందర్శించండి', | ||||
|                 join_discord: 'డిస్కార్డ్లో మా నుంచి చేరండి', | ||||
|                 schedule_a_call: 'మాతో మాట్లాడండి!', | ||||
|   | ||||
| @@ -35,13 +35,14 @@ export const tr: LanguageTranslation = { | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             // TODO: Translate | ||||
|             share: { | ||||
|                 share: 'Share', | ||||
|             backup: { | ||||
|                 backup: 'Backup', | ||||
|                 export_diagram: 'Export Diagram', | ||||
|                 import_diagram: 'Import Diagram', | ||||
|                 restore_diagram: 'Restore Diagram', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'Yardım', | ||||
|                 docs_website: 'Belgeleme', | ||||
|                 visit_website: "ChartDB'yi Ziyaret Et", | ||||
|                 join_discord: "Discord'a Katıl", | ||||
|                 schedule_a_call: 'Bize Ulaş!', | ||||
|   | ||||
| @@ -33,13 +33,14 @@ export const uk: LanguageTranslation = { | ||||
|                 show_minimap: 'Показати мінімапу', | ||||
|                 hide_minimap: 'Приховати мінімапу', | ||||
|             }, | ||||
|             share: { | ||||
|                 share: 'Поширити', | ||||
|             backup: { | ||||
|                 backup: 'Резервне копіювання', | ||||
|                 export_diagram: 'Експорт діаграми', | ||||
|                 import_diagram: 'Імпорт діаграми', | ||||
|                 restore_diagram: 'Відновити діаграму', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'Довідка', | ||||
|                 docs_website: 'Документація', | ||||
|                 visit_website: 'Сайт ChartDB', | ||||
|                 join_discord: 'Приєднуйтесь до нас в Діскорд', | ||||
|                 schedule_a_call: 'Забронювати зустріч!', | ||||
|   | ||||
| @@ -34,13 +34,14 @@ export const vi: LanguageTranslation = { | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             share: { | ||||
|                 share: 'Chia sẻ', | ||||
|             backup: { | ||||
|                 backup: 'Hỗ trợ', | ||||
|                 export_diagram: 'Xuất sơ đồ', | ||||
|                 import_diagram: 'Nhập sơ đồ', | ||||
|                 restore_diagram: 'Khôi phục sơ đồ', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: 'Trợ giúp', | ||||
|                 docs_website: 'Tài liệu', | ||||
|                 visit_website: 'Truy cập ChartDB', | ||||
|                 join_discord: 'Tham gia Discord', | ||||
|                 schedule_a_call: 'Trò chuyện cùng chúng tôi!', | ||||
|   | ||||
| @@ -34,13 +34,14 @@ export const zh_CN: LanguageTranslation = { | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             share: { | ||||
|                 share: '分享', | ||||
|             backup: { | ||||
|                 backup: '备份', | ||||
|                 export_diagram: '导出关系图', | ||||
|                 import_diagram: '导入关系图', | ||||
|                 restore_diagram: '还原图表', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: '帮助', | ||||
|                 docs_website: '文档', | ||||
|                 visit_website: '访问 ChartDB', | ||||
|                 join_discord: '在 Discord 上加入我们', | ||||
|                 schedule_a_call: '和我们交流!', | ||||
|   | ||||
| @@ -34,13 +34,14 @@ export const zh_TW: LanguageTranslation = { | ||||
|                 show_minimap: 'Show Mini Map', | ||||
|                 hide_minimap: 'Hide Mini Map', | ||||
|             }, | ||||
|             share: { | ||||
|                 share: '分享', | ||||
|             backup: { | ||||
|                 backup: '備份', | ||||
|                 export_diagram: '匯出圖表', | ||||
|                 import_diagram: '匯入圖表', | ||||
|                 restore_diagram: '恢復圖表', | ||||
|             }, | ||||
|             help: { | ||||
|                 help: '幫助', | ||||
|                 docs_website: '文件', | ||||
|                 visit_website: '訪問 ChartDB 網站', | ||||
|                 join_discord: '加入 Discord', | ||||
|                 schedule_a_call: '與我們聯絡!', | ||||
|   | ||||
							
								
								
									
										82
									
								
								src/lib/data/export-metadata/export-per-type/common.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/lib/data/export-metadata/export-per-type/common.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| import type { Diagram } from '@/lib/domain/diagram'; | ||||
| import type { DBTable } from '@/lib/domain/db-table'; | ||||
|  | ||||
| export function isFunction(value: string): boolean { | ||||
|     // Common SQL functions | ||||
|     const functionPatterns = [ | ||||
|         /^CURRENT_TIMESTAMP$/i, | ||||
|         /^NOW\(\)$/i, | ||||
|         /^GETDATE\(\)$/i, | ||||
|         /^CURRENT_DATE$/i, | ||||
|         /^CURRENT_TIME$/i, | ||||
|         /^UUID\(\)$/i, | ||||
|         /^NEWID\(\)$/i, | ||||
|         /^NEXT VALUE FOR/i, | ||||
|         /^IDENTITY\s*\(\d+,\s*\d+\)$/i, | ||||
|     ]; | ||||
|     return functionPatterns.some((pattern) => pattern.test(value.trim())); | ||||
| } | ||||
|  | ||||
| export function isKeyword(value: string): boolean { | ||||
|     // Common SQL keywords that can be used as default values | ||||
|     const keywords = [ | ||||
|         'NULL', | ||||
|         'TRUE', | ||||
|         'FALSE', | ||||
|         'CURRENT_TIMESTAMP', | ||||
|         'CURRENT_DATE', | ||||
|         'CURRENT_TIME', | ||||
|         'CURRENT_USER', | ||||
|         'SESSION_USER', | ||||
|         'SYSTEM_USER', | ||||
|     ]; | ||||
|     return keywords.includes(value.trim().toUpperCase()); | ||||
| } | ||||
|  | ||||
| export function strHasQuotes(value: string): boolean { | ||||
|     return /^['"].*['"]$/.test(value.trim()); | ||||
| } | ||||
|  | ||||
| export function exportFieldComment(comment: string): string { | ||||
|     if (!comment) { | ||||
|         return ''; | ||||
|     } | ||||
|  | ||||
|     return comment | ||||
|         .split('\n') | ||||
|         .map((commentLine) => `    -- ${commentLine}\n`) | ||||
|         .join(''); | ||||
| } | ||||
|  | ||||
| export function getInlineFK(table: DBTable, diagram: Diagram): string { | ||||
|     if (!diagram.relationships) { | ||||
|         return ''; | ||||
|     } | ||||
|  | ||||
|     const fks = diagram.relationships | ||||
|         .filter((r) => r.sourceTableId === table.id) | ||||
|         .map((r) => { | ||||
|             const targetTable = diagram.tables?.find( | ||||
|                 (t) => t.id === r.targetTableId | ||||
|             ); | ||||
|             const sourceField = table.fields.find( | ||||
|                 (f) => f.id === r.sourceFieldId | ||||
|             ); | ||||
|             const targetField = targetTable?.fields.find( | ||||
|                 (f) => f.id === r.targetFieldId | ||||
|             ); | ||||
|  | ||||
|             if (!targetTable || !sourceField || !targetField) { | ||||
|                 return ''; | ||||
|             } | ||||
|  | ||||
|             const targetTableName = targetTable.schema | ||||
|                 ? `"${targetTable.schema}"."${targetTable.name}"` | ||||
|                 : `"${targetTable.name}"`; | ||||
|  | ||||
|             return `    FOREIGN KEY ("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}")`; | ||||
|         }) | ||||
|         .filter(Boolean); | ||||
|  | ||||
|     return fks.join(',\n'); | ||||
| } | ||||
							
								
								
									
										247
									
								
								src/lib/data/export-metadata/export-per-type/mssql.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								src/lib/data/export-metadata/export-per-type/mssql.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,247 @@ | ||||
| import { | ||||
|     exportFieldComment, | ||||
|     isFunction, | ||||
|     isKeyword, | ||||
|     strHasQuotes, | ||||
| } from './common'; | ||||
| import type { Diagram } from '@/lib/domain/diagram'; | ||||
| import type { DBTable } from '@/lib/domain/db-table'; | ||||
| import type { DBField } from '@/lib/domain/db-field'; | ||||
| import type { DBRelationship } from '@/lib/domain/db-relationship'; | ||||
|  | ||||
| function parseMSSQLDefault(field: DBField): string { | ||||
|     if (!field.default) { | ||||
|         return ''; | ||||
|     } | ||||
|  | ||||
|     let defaultValue = field.default.trim(); | ||||
|  | ||||
|     // Remove type casting for SQL Server | ||||
|     defaultValue = defaultValue.split('::')[0]; | ||||
|  | ||||
|     // Handle nextval sequences for SQL Server | ||||
|     if (defaultValue.includes('nextval')) { | ||||
|         return 'IDENTITY(1,1)'; | ||||
|     } | ||||
|  | ||||
|     // Special handling for SQL Server DEFAULT values | ||||
|     if (defaultValue.match(/^\(\(.*\)\)$/)) { | ||||
|         // Handle ((0)), ((0.00)) style defaults | ||||
|         return defaultValue.replace(/^\(\(|\)\)$/g, ''); | ||||
|     } else if (defaultValue.match(/^\(N'.*'\)$/)) { | ||||
|         // Handle (N'value') style defaults | ||||
|         const innerValue = defaultValue.replace(/^\(N'|'\)$/g, ''); | ||||
|         return `N'${innerValue}'`; | ||||
|     } else if (defaultValue.match(/^\(NULL\)$/i)) { | ||||
|         // Handle (NULL) defaults | ||||
|         return 'NULL'; | ||||
|     } else if (defaultValue.match(/^\(getdate\(\)\)$/i)) { | ||||
|         // Handle (getdate()) defaults | ||||
|         return 'getdate()'; | ||||
|     } else if (defaultValue.match(/^\('?\*'?\)$/i) || defaultValue === '*') { | ||||
|         // Handle ('*') or (*) or * defaults - common for "all" values | ||||
|         return "N'*'"; | ||||
|     } else if (defaultValue.match(/^\((['"])(.*)\1\)$/)) { | ||||
|         // Handle ('value') or ("value") style defaults | ||||
|         const matches = defaultValue.match(/^\((['"])(.*)\1\)$/); | ||||
|         return matches ? `N'${matches[2]}'` : defaultValue; | ||||
|     } | ||||
|  | ||||
|     // Handle special characters that could be interpreted as operators | ||||
|     const sqlServerSpecialChars = /[*+\-/%&|^!=<>~]/; | ||||
|     if (sqlServerSpecialChars.test(defaultValue)) { | ||||
|         // If the value contains special characters and isn't already properly quoted | ||||
|         if ( | ||||
|             !strHasQuotes(defaultValue) && | ||||
|             !isFunction(defaultValue) && | ||||
|             !isKeyword(defaultValue) | ||||
|         ) { | ||||
|             return `N'${defaultValue.replace(/'/g, "''")}'`; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if ( | ||||
|         strHasQuotes(defaultValue) || | ||||
|         isFunction(defaultValue) || | ||||
|         isKeyword(defaultValue) || | ||||
|         /^-?\d+(\.\d+)?$/.test(defaultValue) | ||||
|     ) { | ||||
|         return defaultValue; | ||||
|     } | ||||
|  | ||||
|     return `'${defaultValue}'`; | ||||
| } | ||||
|  | ||||
| export function exportMSSQL(diagram: Diagram): string { | ||||
|     if (!diagram.tables || !diagram.relationships) { | ||||
|         return ''; | ||||
|     } | ||||
|  | ||||
|     const tables = diagram.tables; | ||||
|     const relationships = diagram.relationships; | ||||
|  | ||||
|     // Create CREATE SCHEMA statements for all schemas | ||||
|     let sqlScript = ''; | ||||
|     const schemas = new Set<string>(); | ||||
|  | ||||
|     tables.forEach((table) => { | ||||
|         if (table.schema) { | ||||
|             schemas.add(table.schema); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // 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 ''; | ||||
|             } | ||||
|  | ||||
|             const tableName = table.schema | ||||
|                 ? `[${table.schema}].[${table.name}]` | ||||
|                 : `[${table.name}]`; | ||||
|  | ||||
|             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; | ||||
|  | ||||
|                     // 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})`; | ||||
|                         } | ||||
|                     } else if (field.precision) { | ||||
|                         if ( | ||||
|                             typeName.toLowerCase() === 'decimal' || | ||||
|                             typeName.toLowerCase() === 'numeric' | ||||
|                         ) { | ||||
|                             typeWithSize = `${typeName}(${field.precision})`; | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     const notNull = field.nullable ? '' : ' NOT NULL'; | ||||
|  | ||||
|                     // Check if identity column | ||||
|                     const identity = field.default | ||||
|                         ?.toLowerCase() | ||||
|                         .includes('identity') | ||||
|                         ? ' IDENTITY(1,1)' | ||||
|                         : ''; | ||||
|  | ||||
|                     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)}` | ||||
|                             : ''; | ||||
|  | ||||
|                     // 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}]` : ''; | ||||
|                         }) | ||||
|                         .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` | ||||
|                         : ''; | ||||
|                 }) | ||||
|                 .join('')}`; | ||||
|         }) | ||||
|         .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 ( | ||||
|                 !sourceTable || | ||||
|                 !targetTable || | ||||
|                 sourceTable.isView || | ||||
|                 targetTable.isView | ||||
|             ) { | ||||
|                 return ''; | ||||
|             } | ||||
|  | ||||
|             const sourceField = sourceTable.fields.find( | ||||
|                 (f) => f.id === r.sourceFieldId | ||||
|             ); | ||||
|             const targetField = targetTable.fields.find( | ||||
|                 (f) => f.id === r.targetFieldId | ||||
|             ); | ||||
|  | ||||
|             if (!sourceField || !targetField) { | ||||
|                 return ''; | ||||
|             } | ||||
|  | ||||
|             const sourceTableName = sourceTable.schema | ||||
|                 ? `[${sourceTable.schema}].[${sourceTable.name}]` | ||||
|                 : `[${sourceTable.name}]`; | ||||
|             const targetTableName = targetTable.schema | ||||
|                 ? `[${targetTable.schema}].[${targetTable.name}]` | ||||
|                 : `[${targetTable.name}]`; | ||||
|  | ||||
|             return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT [${r.name}] FOREIGN KEY([${sourceField.name}]) REFERENCES ${targetTableName}([${targetField.name}]);\n`; | ||||
|         }) | ||||
|         .filter(Boolean) // Remove empty strings | ||||
|         .join('\n')}`; | ||||
|  | ||||
|     return sqlScript; | ||||
| } | ||||
| @@ -1,9 +1,10 @@ | ||||
| import type { Diagram } from '../../domain/diagram'; | ||||
| import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env'; | ||||
| import type { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import { DatabaseType } from '@/lib/domain/database-type'; | ||||
| import type { DBTable } from '@/lib/domain/db-table'; | ||||
| import type { DataType } from '../data-types/data-types'; | ||||
| import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache'; | ||||
| import { exportMSSQL } from './export-per-type/mssql'; | ||||
|  | ||||
| export const exportBaseSQL = (diagram: Diagram): string => { | ||||
|     const { tables, relationships } = diagram; | ||||
| @@ -12,6 +13,10 @@ export const exportBaseSQL = (diagram: Diagram): string => { | ||||
|         return ''; | ||||
|     } | ||||
|  | ||||
|     if (diagram.databaseType === DatabaseType.SQL_SERVER) { | ||||
|         return exportMSSQL(diagram); | ||||
|     } | ||||
|  | ||||
|     // Filter out the tables that are views | ||||
|     const nonViewTables = tables.filter((table) => !table.isView); | ||||
|  | ||||
| @@ -226,6 +231,10 @@ export const exportSQL = async ( | ||||
|     } | ||||
| ): Promise<string> => { | ||||
|     const sqlScript = exportBaseSQL(diagram); | ||||
|     if (databaseType === DatabaseType.SQL_SERVER) { | ||||
|         return sqlScript; | ||||
|     } | ||||
|  | ||||
|     const cacheKey = await generateCacheKey(databaseType, sqlScript); | ||||
|  | ||||
|     const cachedResult = getFromCache(cacheKey); | ||||
| @@ -243,13 +252,16 @@ export const exportSQL = async ( | ||||
|  | ||||
|     const apiKey = window?.env?.OPENAI_API_KEY ?? OPENAI_API_KEY; | ||||
|     const baseUrl = window?.env?.OPENAI_API_ENDPOINT ?? OPENAI_API_ENDPOINT; | ||||
|     const modelName = window?.env?.LLM_MODEL_NAME || 'gpt-4o-mini-2024-07-18'; | ||||
|     const modelName = | ||||
|         window?.env?.LLM_MODEL_NAME ?? | ||||
|         LLM_MODEL_NAME ?? | ||||
|         'gpt-4o-mini-2024-07-18'; | ||||
|  | ||||
|     let config: { apiKey: string; baseUrl?: string }; | ||||
|  | ||||
|     if (useCustomEndpoint) { | ||||
|         config = { | ||||
|             apiKey: 'sk-xxx', // minimal valid API key format | ||||
|             apiKey: apiKey, | ||||
|             baseUrl: baseUrl, | ||||
|         }; | ||||
|     } else { | ||||
|   | ||||
| @@ -117,7 +117,7 @@ indexes AS ( | ||||
|     JOIN sys.schemas s ON t.schema_id = s.schema_id | ||||
|     JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id | ||||
|     JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id | ||||
|     WHERE s.name LIKE '%' AND i.name IS NOT NULL | ||||
|     WHERE s.name LIKE '%' AND i.name IS NOT NULL AND ic.is_included_column = 0 | ||||
| ), | ||||
| tbls AS ( | ||||
|     SELECT | ||||
| @@ -324,6 +324,7 @@ indexes AS ( | ||||
|                 JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id | ||||
|                 WHERE s.name LIKE '%' | ||||
|                         AND i.name IS NOT NULL | ||||
|                         AND ic.is_included_column = 0 | ||||
|                 FOR XML PATH('') | ||||
|             ), 1, 1, ''), '') | ||||
|         + N']' AS all_indexes_json | ||||
|   | ||||
| @@ -1,22 +1,12 @@ | ||||
| import React, { | ||||
|     Suspense, | ||||
|     useCallback, | ||||
|     useEffect, | ||||
|     useRef, | ||||
|     useState, | ||||
| } from 'react'; | ||||
| import React, { Suspense, useCallback, useEffect, useRef } from 'react'; | ||||
| import { TopNavbar } from './top-navbar/top-navbar'; | ||||
| import { useNavigate, useParams } from 'react-router-dom'; | ||||
| import { useConfig } from '@/hooks/use-config'; | ||||
| import { useParams } from 'react-router-dom'; | ||||
| import { useChartDB } from '@/hooks/use-chartdb'; | ||||
| import { useDialog } from '@/hooks/use-dialog'; | ||||
| import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack'; | ||||
| import { Toaster } from '@/components/toast/toaster'; | ||||
| import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner'; | ||||
| import { useBreakpoint } from '@/hooks/use-breakpoint'; | ||||
| import { useLayout } from '@/hooks/use-layout'; | ||||
| import { useToast } from '@/components/toast/use-toast'; | ||||
| import type { Diagram } from '@/lib/domain/diagram'; | ||||
| import { ToastAction } from '@/components/toast/toast'; | ||||
| import { useLocalConfig } from '@/hooks/use-local-config'; | ||||
| import { useTranslation } from 'react-i18next'; | ||||
| @@ -35,10 +25,10 @@ import { DialogProvider } from '@/context/dialog-context/dialog-provider'; | ||||
| import { KeyboardShortcutsProvider } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts-provider'; | ||||
| import { Spinner } from '@/components/spinner/spinner'; | ||||
| import { Helmet } from 'react-helmet-async'; | ||||
| import { useStorage } from '@/hooks/use-storage'; | ||||
| import { AlertProvider } from '@/context/alert-context/alert-provider'; | ||||
| import { CanvasProvider } from '@/context/canvas-context/canvas-provider'; | ||||
| import { HIDE_BUCKLE_DOT_DEV } from '@/lib/env'; | ||||
| import { useDiagramLoader } from './use-diagram-loader'; | ||||
|  | ||||
| const OPEN_STAR_US_AFTER_SECONDS = 30; | ||||
| const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1; | ||||
| @@ -56,23 +46,12 @@ export const EditorMobileLayoutLazy = React.lazy( | ||||
| ); | ||||
|  | ||||
| const EditorPageComponent: React.FC = () => { | ||||
|     const { | ||||
|         loadDiagram, | ||||
|         diagramName, | ||||
|         currentDiagram, | ||||
|         schemas, | ||||
|         filteredSchemas, | ||||
|     } = useChartDB(); | ||||
|     const { diagramName, currentDiagram, schemas, filteredSchemas } = | ||||
|         useChartDB(); | ||||
|     const { openSelectSchema, showSidePanel } = useLayout(); | ||||
|     const { resetRedoStack, resetUndoStack } = useRedoUndoStack(); | ||||
|     const { showLoader, hideLoader } = useFullScreenLoader(); | ||||
|     const { openCreateDiagramDialog, openStarUsDialog, openBuckleDialog } = | ||||
|         useDialog(); | ||||
|     const { openStarUsDialog, openBuckleDialog } = useDialog(); | ||||
|     const { diagramId } = useParams<{ diagramId: string }>(); | ||||
|     const { config, updateConfig } = useConfig(); | ||||
|     const navigate = useNavigate(); | ||||
|     const { isMd: isDesktop } = useBreakpoint('md'); | ||||
|     const [initialDiagram, setInitialDiagram] = useState<Diagram | undefined>(); | ||||
|     const { | ||||
|         hideMultiSchemaNotification, | ||||
|         setHideMultiSchemaNotification, | ||||
| @@ -85,73 +64,7 @@ const EditorPageComponent: React.FC = () => { | ||||
|     } = useLocalConfig(); | ||||
|     const { toast } = useToast(); | ||||
|     const { t } = useTranslation(); | ||||
|     const { listDiagrams } = useStorage(); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (!config) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (currentDiagram?.id === diagramId) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const loadDefaultDiagram = async () => { | ||||
|             if (diagramId) { | ||||
|                 setInitialDiagram(undefined); | ||||
|                 showLoader(); | ||||
|                 resetRedoStack(); | ||||
|                 resetUndoStack(); | ||||
|                 const diagram = await loadDiagram(diagramId); | ||||
|                 if (!diagram) { | ||||
|                     if (currentDiagram?.id) { | ||||
|                         await updateConfig({ | ||||
|                             defaultDiagramId: currentDiagram.id, | ||||
|                         }); | ||||
|                         navigate(`/diagrams/${currentDiagram.id}`); | ||||
|                     } else { | ||||
|                         navigate('/'); | ||||
|                     } | ||||
|                 } | ||||
|                 setInitialDiagram(diagram); | ||||
|                 hideLoader(); | ||||
|             } else if (!diagramId && config.defaultDiagramId) { | ||||
|                 const diagram = await loadDiagram(config.defaultDiagramId); | ||||
|                 if (!diagram) { | ||||
|                     await updateConfig({ | ||||
|                         defaultDiagramId: '', | ||||
|                     }); | ||||
|                     navigate('/'); | ||||
|                 } else { | ||||
|                     navigate(`/diagrams/${config.defaultDiagramId}`); | ||||
|                 } | ||||
|             } else { | ||||
|                 const diagrams = await listDiagrams(); | ||||
|  | ||||
|                 if (diagrams.length > 0) { | ||||
|                     const defaultDiagramId = diagrams[0].id; | ||||
|                     await updateConfig({ defaultDiagramId }); | ||||
|                     navigate(`/diagrams/${defaultDiagramId}`); | ||||
|                 } else { | ||||
|                     openCreateDiagramDialog(); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|         loadDefaultDiagram(); | ||||
|     }, [ | ||||
|         diagramId, | ||||
|         openCreateDiagramDialog, | ||||
|         config, | ||||
|         navigate, | ||||
|         listDiagrams, | ||||
|         loadDiagram, | ||||
|         resetRedoStack, | ||||
|         resetUndoStack, | ||||
|         hideLoader, | ||||
|         showLoader, | ||||
|         currentDiagram?.id, | ||||
|         updateConfig, | ||||
|     ]); | ||||
|     const { initialDiagram } = useDiagramLoader(); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (HIDE_BUCKLE_DOT_DEV) { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import React from 'react'; | ||||
| import React, { useCallback } from 'react'; | ||||
| import { Plus, FileType2, FileKey2, MessageCircleMore } from 'lucide-react'; | ||||
| import { Button } from '@/components/button/button'; | ||||
| import { | ||||
| @@ -70,17 +70,29 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({ | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const createIndexHandler = () => { | ||||
|         setSelectedItems((prev) => { | ||||
|             if (prev.includes('indexes')) { | ||||
|                 return prev; | ||||
|             } | ||||
|     const createIndexHandler = useCallback( | ||||
|         (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { | ||||
|             e.stopPropagation(); | ||||
|             setSelectedItems((prev) => { | ||||
|                 if (prev.includes('indexes')) { | ||||
|                     return prev; | ||||
|                 } | ||||
|  | ||||
|             return [...prev, 'indexes']; | ||||
|         }); | ||||
|                 return [...prev, 'indexes']; | ||||
|             }); | ||||
|  | ||||
|         createIndex(table.id); | ||||
|     }; | ||||
|             createIndex(table.id); | ||||
|         }, | ||||
|         [createIndex, table.id, setSelectedItems] | ||||
|     ); | ||||
|  | ||||
|     const createFieldHandler = useCallback( | ||||
|         (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { | ||||
|             e.stopPropagation(); | ||||
|             createField(table.id); | ||||
|         }, | ||||
|         [createField, table.id] | ||||
|     ); | ||||
|  | ||||
|     return ( | ||||
|         <div | ||||
| @@ -113,10 +125,7 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({ | ||||
|                                     <Button | ||||
|                                         variant="ghost" | ||||
|                                         className="size-4 p-0 text-xs hover:bg-primary-foreground" | ||||
|                                         onClick={(e) => { | ||||
|                                             e.stopPropagation(); | ||||
|                                             createField(table.id); | ||||
|                                         }} | ||||
|                                         onClick={createFieldHandler} | ||||
|                                     > | ||||
|                                         <Plus className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" /> | ||||
|                                     </Button> | ||||
| @@ -153,6 +162,18 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({ | ||||
|                                     /> | ||||
|                                 ))} | ||||
|                             </SortableContext> | ||||
|                             <div className="flex justify-start p-1"> | ||||
|                                 <Button | ||||
|                                     variant="ghost" | ||||
|                                     className="flex h-7 items-center gap-1 px-2 text-xs" | ||||
|                                     onClick={createFieldHandler} | ||||
|                                 > | ||||
|                                     <Plus className="size-4 text-muted-foreground" /> | ||||
|                                     {t( | ||||
|                                         'side_panel.tables_section.table.add_field' | ||||
|                                     )} | ||||
|                                 </Button> | ||||
|                             </div> | ||||
|                         </DndContext> | ||||
|                     </AccordionContent> | ||||
|                 </AccordionItem> | ||||
| @@ -173,10 +194,7 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({ | ||||
|                                     <Button | ||||
|                                         variant="ghost" | ||||
|                                         className="size-4 p-0 text-xs hover:bg-primary-foreground" | ||||
|                                         onClick={(e) => { | ||||
|                                             e.stopPropagation(); | ||||
|                                             createIndexHandler(); | ||||
|                                         }} | ||||
|                                         onClick={createIndexHandler} | ||||
|                                     > | ||||
|                                         <Plus className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" /> | ||||
|                                     </Button> | ||||
| @@ -198,6 +216,16 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({ | ||||
|                                 fields={table.fields} | ||||
|                             /> | ||||
|                         ))} | ||||
|                         <div className="flex justify-start p-1"> | ||||
|                             <Button | ||||
|                                 variant="ghost" | ||||
|                                 className="flex h-7 items-center gap-1 px-2 text-xs" | ||||
|                                 onClick={createIndexHandler} | ||||
|                             > | ||||
|                                 <Plus className="size-4 text-muted-foreground" /> | ||||
|                                 {t('side_panel.tables_section.table.add_index')} | ||||
|                             </Button> | ||||
|                         </div> | ||||
|                     </AccordionContent> | ||||
|                 </AccordionItem> | ||||
|  | ||||
| @@ -248,7 +276,7 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({ | ||||
|                     <Button | ||||
|                         variant="outline" | ||||
|                         className="h-8 p-2 text-xs" | ||||
|                         onClick={() => createField(table.id)} | ||||
|                         onClick={createFieldHandler} | ||||
|                     > | ||||
|                         <FileType2 className="h-4" /> | ||||
|                         {t('side_panel.tables_section.table.add_field')} | ||||
|   | ||||
| @@ -102,6 +102,10 @@ export const Menu: React.FC<MenuProps> = () => { | ||||
|         window.location.href = 'https://chartdb.io'; | ||||
|     }, []); | ||||
|  | ||||
|     const openChartDBDocs = useCallback(() => { | ||||
|         window.open('https://docs.chartdb.io', '_blank'); | ||||
|     }, []); | ||||
|  | ||||
|     const openJoinDiscord = useCallback(() => { | ||||
|         window.open('https://discord.gg/QeFwyWSKwC', '_blank'); | ||||
|     }, []); | ||||
| @@ -225,6 +229,9 @@ export const Menu: React.FC<MenuProps> = () => { | ||||
|                             {t('menu.file.import')} | ||||
|                         </MenubarSubTrigger> | ||||
|                         <MenubarSubContent> | ||||
|                             <MenubarItem onClick={openImportDiagramDialog}> | ||||
|                                 .json | ||||
|                             </MenubarItem> | ||||
|                             <MenubarItem onClick={() => openImportDBMLDialog()}> | ||||
|                                 .dbml | ||||
|                             </MenubarItem> | ||||
| @@ -341,6 +348,10 @@ export const Menu: React.FC<MenuProps> = () => { | ||||
|                             <MenubarItem onClick={exportPNG}>PNG</MenubarItem> | ||||
|                             <MenubarItem onClick={exportJPG}>JPG</MenubarItem> | ||||
|                             <MenubarItem onClick={exportSVG}>SVG</MenubarItem> | ||||
|                             <MenubarSeparator /> | ||||
|                             <MenubarItem onClick={openExportDiagramDialog}> | ||||
|                                 JSON | ||||
|                             </MenubarItem> | ||||
|                         </MenubarSubContent> | ||||
|                     </MenubarSub> | ||||
|                     <MenubarSeparator /> | ||||
| @@ -487,13 +498,13 @@ export const Menu: React.FC<MenuProps> = () => { | ||||
|             </MenubarMenu> | ||||
|  | ||||
|             <MenubarMenu> | ||||
|                 <MenubarTrigger>{t('menu.share.share')}</MenubarTrigger> | ||||
|                 <MenubarTrigger>{t('menu.backup.backup')}</MenubarTrigger> | ||||
|                 <MenubarContent> | ||||
|                     <MenubarItem onClick={openExportDiagramDialog}> | ||||
|                         {t('menu.share.export_diagram')} | ||||
|                         {t('menu.backup.export_diagram')} | ||||
|                     </MenubarItem> | ||||
|                     <MenubarItem onClick={openImportDiagramDialog}> | ||||
|                         {t('menu.share.import_diagram')} | ||||
|                         {t('menu.backup.restore_diagram')} | ||||
|                     </MenubarItem> | ||||
|                 </MenubarContent> | ||||
|             </MenubarMenu> | ||||
| @@ -501,6 +512,9 @@ export const Menu: React.FC<MenuProps> = () => { | ||||
|             <MenubarMenu> | ||||
|                 <MenubarTrigger>{t('menu.help.help')}</MenubarTrigger> | ||||
|                 <MenubarContent> | ||||
|                     <MenubarItem onClick={openChartDBDocs}> | ||||
|                         {t('menu.help.docs_website')} | ||||
|                     </MenubarItem> | ||||
|                     <MenubarItem onClick={openChartDBIO}> | ||||
|                         {t('menu.help.visit_website')} | ||||
|                     </MenubarItem> | ||||
|   | ||||
							
								
								
									
										92
									
								
								src/pages/editor-page/use-diagram-loader.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/pages/editor-page/use-diagram-loader.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| import { useChartDB } from '@/hooks/use-chartdb'; | ||||
| import { useConfig } from '@/hooks/use-config'; | ||||
| import { useDialog } from '@/hooks/use-dialog'; | ||||
| import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner'; | ||||
| import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack'; | ||||
| import { useStorage } from '@/hooks/use-storage'; | ||||
| import type { Diagram } from '@/lib/domain/diagram'; | ||||
| import { useEffect, useRef, useState } from 'react'; | ||||
| import { useNavigate, useParams } from 'react-router-dom'; | ||||
|  | ||||
| export const useDiagramLoader = () => { | ||||
|     const [initialDiagram, setInitialDiagram] = useState<Diagram | undefined>(); | ||||
|     const { diagramId } = useParams<{ diagramId: string }>(); | ||||
|     const { config } = useConfig(); | ||||
|     const { loadDiagram, currentDiagram } = useChartDB(); | ||||
|     const { resetRedoStack, resetUndoStack } = useRedoUndoStack(); | ||||
|     const { showLoader, hideLoader } = useFullScreenLoader(); | ||||
|     const { openCreateDiagramDialog, openOpenDiagramDialog } = useDialog(); | ||||
|     const navigate = useNavigate(); | ||||
|     const { listDiagrams } = useStorage(); | ||||
|  | ||||
|     const currentDiagramLoadingRef = useRef<string | undefined>(undefined); | ||||
|  | ||||
|     useEffect(() => { | ||||
|         if (!config) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (currentDiagram?.id === diagramId) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const loadDefaultDiagram = async () => { | ||||
|             if (diagramId) { | ||||
|                 setInitialDiagram(undefined); | ||||
|                 showLoader(); | ||||
|                 resetRedoStack(); | ||||
|                 resetUndoStack(); | ||||
|                 const diagram = await loadDiagram(diagramId); | ||||
|                 if (!diagram) { | ||||
|                     openOpenDiagramDialog({ canClose: false }); | ||||
|                     hideLoader(); | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 setInitialDiagram(diagram); | ||||
|                 hideLoader(); | ||||
|  | ||||
|                 return; | ||||
|             } else if (!diagramId && config.defaultDiagramId) { | ||||
|                 const diagram = await loadDiagram(config.defaultDiagramId); | ||||
|                 if (diagram) { | ||||
|                     navigate(`/diagrams/${config.defaultDiagramId}`); | ||||
|  | ||||
|                     return; | ||||
|                 } | ||||
|             } | ||||
|             const diagrams = await listDiagrams(); | ||||
|  | ||||
|             if (diagrams.length > 0) { | ||||
|                 openOpenDiagramDialog({ canClose: false }); | ||||
|             } else { | ||||
|                 openCreateDiagramDialog(); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         if ( | ||||
|             currentDiagramLoadingRef.current === (diagramId ?? '') && | ||||
|             currentDiagramLoadingRef.current !== undefined | ||||
|         ) { | ||||
|             return; | ||||
|         } | ||||
|         currentDiagramLoadingRef.current = diagramId ?? ''; | ||||
|  | ||||
|         loadDefaultDiagram(); | ||||
|     }, [ | ||||
|         diagramId, | ||||
|         openCreateDiagramDialog, | ||||
|         config, | ||||
|         navigate, | ||||
|         listDiagrams, | ||||
|         loadDiagram, | ||||
|         resetRedoStack, | ||||
|         resetUndoStack, | ||||
|         hideLoader, | ||||
|         showLoader, | ||||
|         currentDiagram?.id, | ||||
|         openOpenDiagramDialog, | ||||
|     ]); | ||||
|  | ||||
|     return { initialDiagram }; | ||||
| }; | ||||
		Reference in New Issue
	
	Block a user