Compare commits

..

1 Commits

Author SHA1 Message Date
johnnyfish
595e3db0b3 fix(import-db): handle direct query copy-paste instead of fetched JSON 2025-02-17 20:28:15 +02:00
41 changed files with 288 additions and 861 deletions

View File

@@ -1,24 +1,5 @@
# 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)

View File

@@ -1,20 +1,17 @@
{
"$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"
}
}
"$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"
}
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "chartdb",
"version": "1.8.1",
"version": "1.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chartdb",
"version": "1.8.1",
"version": "1.8.0",
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dbml/core": "^3.9.5",

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.8.1",
"version": "1.8.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -8,7 +8,6 @@ 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
@@ -16,9 +15,7 @@ export interface DialogContext {
closeCreateDiagramDialog: () => void;
// Open diagram dialog
openOpenDiagramDialog: (
params?: Omit<OpenDiagramDialogProps, 'dialog'>
) => void;
openOpenDiagramDialog: () => void;
closeOpenDiagramDialog: () => void;
// Export SQL dialog

View File

@@ -2,7 +2,6 @@ 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';
@@ -28,17 +27,6 @@ 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);
@@ -132,7 +120,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
value={{
openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
openOpenDiagramDialog: openOpenDiagramDialogHandler,
openOpenDiagramDialog: () => setOpenOpenDiagramDialog(true),
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
openExportSQLDialog: openExportSQLDialogHandler,
closeExportSQLDialog: () => setOpenExportSQLDialog(false),
@@ -166,10 +154,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
>
{children}
<CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} />
<OpenDiagramDialog
dialog={{ open: openOpenDiagramDialog }}
{...openDiagramDialogParams}
/>
<OpenDiagramDialog dialog={{ open: openOpenDiagramDialog }} />
<ExportSQLDialog
dialog={{ open: openExportSQLDialog }}
{...exportSQLDialogParams}

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useEffect, useState } from 'react';
import React, { useCallback, useMemo } 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,8 +6,6 @@ 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,
@@ -16,24 +14,6 @@ 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) => {
@@ -148,22 +128,16 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
'http://www.w3.org/2000/svg',
'rect'
);
const bgPadding = 2000;
backgroundRect.setAttribute(
'x',
String(-viewport.x - bgPadding)
);
backgroundRect.setAttribute(
'y',
String(-viewport.y - bgPadding)
);
const padding = 2000;
backgroundRect.setAttribute('x', String(-viewport.x - padding));
backgroundRect.setAttribute('y', String(-viewport.y - padding));
backgroundRect.setAttribute(
'width',
String(reactFlowBounds.width + 2 * bgPadding)
String(reactFlowBounds.width + 2 * padding)
);
backgroundRect.setAttribute(
'height',
String(reactFlowBounds.height + 2 * bgPadding)
String(reactFlowBounds.height + 2 * padding)
);
backgroundRect.setAttribute('fill', 'url(#background-pattern)');
tempSvg.appendChild(backgroundRect);
@@ -174,110 +148,28 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
);
try {
// 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;
}
// 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);
};
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,
});
downloadImage(dataUrl, type);
} finally {
viewportElement.removeChild(tempSvg);
hideLoader();
@@ -292,7 +184,6 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
setNodes,
showLoader,
effectiveTheme,
logoBase64,
]
);

View File

@@ -39,7 +39,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
useHotkeys(
keyboardShortcutsForOS[KeyboardShortcutAction.OPEN_DIAGRAM]
.keyCombination,
() => openOpenDiagramDialog(),
openOpenDiagramDialog,
{
preventDefault: true,
},

View File

@@ -87,6 +87,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false);
const helpButtonRef = React.useRef<HTMLButtonElement>(null);
useEffect(() => {
const loadScripts = async () => {
const { importMetadataScripts } = await import(
@@ -134,6 +136,11 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
if (inputValue.length === 65535) {
setShowSSMSInfoDialog(true);
}
// Show instructions when input contains "WITH fk_info as"
if (inputValue.toLowerCase().includes('with fk_info as')) {
helpButtonRef.current?.click();
}
},
[setScriptResult]
);
@@ -398,7 +405,11 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
)}
{isDesktop ? (
<ZoomableImage src="/load-new-db-instructions.gif">
<Button type="button" variant="link">
<Button
type="button"
variant="link"
ref={helpButtonRef}
>
{t(
'new_diagram_dialog.import_database.instructions_link'
)}
@@ -450,7 +461,11 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
{!isDesktop ? (
<ZoomableImage src="/load-new-db-instructions.gif">
<Button type="button" variant="link">
<Button
type="button"
variant="link"
ref={helpButtonRef}
>
{t(
'new_diagram_dialog.import_database.instructions_link'
)}

View File

@@ -15,10 +15,11 @@ 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 {}
@@ -26,27 +27,44 @@ export const ExportDiagramDialog: React.FC<ExportDiagramDialogProps> = ({
dialog,
}) => {
const { t } = useTranslation();
const { currentDiagram } = useChartDB();
const { diagramName, currentDiagram } = useChartDB();
const [isLoading, setIsLoading] = useState(false);
const { closeExportDiagramDialog } = useDialog();
const [error, setError] = useState(false);
useEffect(() => {
if (!dialog.open) return;
setIsLoading(false);
setError(false);
}, [dialog.open]);
const { exportDiagram, isExporting: isLoading } = useExportDiagram();
const downloadOutput = useCallback(
(dataUrl: string) => {
const a = document.createElement('a');
a.setAttribute('download', `ChartDB(${diagramName}).json`);
a.setAttribute('href', dataUrl);
a.click();
},
[diagramName]
);
const handleExport = useCallback(async () => {
setIsLoading(true);
await waitFor(1000);
try {
await exportDiagram({ diagram: currentDiagram });
const json = diagramToJSONOutput(currentDiagram);
const blob = new Blob([json], { type: 'application/json' });
const dataUrl = URL.createObjectURL(blob);
downloadOutput(dataUrl);
setIsLoading(false);
closeExportDiagramDialog();
} catch (e) {
setError(true);
setIsLoading(false);
throw e;
}
}, [exportDiagram, currentDiagram, closeExportDiagramDialog]);
}, [downloadOutput, currentDiagram, closeExportDiagramDialog]);
const outputTypeOptions: SelectBoxOption[] = useMemo(
() =>

View File

@@ -28,13 +28,10 @@ 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 {
canClose?: boolean;
}
export interface OpenDiagramDialogProps extends BaseDialogProps {}
export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
dialog,
canClose = true,
}) => {
const { closeOpenDiagramDialog } = useDialog();
const { t } = useTranslation();
@@ -125,14 +122,14 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
<Dialog
{...dialog}
onOpenChange={(open) => {
if (!open && canClose) {
if (!open) {
closeOpenDiagramDialog();
}
}}
>
<DialogContent
className="flex h-[30rem] max-h-screen flex-col overflow-y-auto md:min-w-[80vw] xl:min-w-[55vw]"
showClose={canClose}
showClose
>
<DialogHeader>
<DialogTitle>{t('open_diagram_dialog.title')}</DialogTitle>
@@ -229,15 +226,11 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
</DialogInternalContent>
<DialogFooter className="flex !justify-between gap-2">
{canClose ? (
<DialogClose asChild>
<Button type="button" variant="secondary">
{t('open_diagram_dialog.cancel')}
</Button>
</DialogClose>
) : (
<div />
)}
<DialogClose asChild>
<Button type="button" variant="secondary">
{t('open_diagram_dialog.cancel')}
</Button>
</DialogClose>
<DialogClose asChild>
<Button
type="submit"

View File

@@ -1,40 +0,0 @@
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,
};
};

View File

@@ -34,14 +34,13 @@ export const ar: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
backup: {
backup: 'النسخ الاحتياطي',
share: {
share: 'مشاركة',
export_diagram: 'تصدير المخطط',
restore_diagram: 'استعادة المخطط',
import_diagram: 'استيراد المخطط',
},
help: {
help: 'مساعدة',
docs_website: 'الوثائق',
visit_website: 'ChartDB قم بزيارة',
join_discord: 'Discord انضم إلينا على',
schedule_a_call: '!تحدث معنا',

View File

@@ -35,14 +35,13 @@ export const bn: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
backup: {
backup: 'ব্যাকআপ',
share: {
share: 'শেয়ার করুন',
export_diagram: 'ডায়াগ্রাম রপ্তানি করুন',
restore_diagram: 'ডায়াগ্রাম পুনরুদ্ধার করুন',
import_diagram: 'ডায়াগ্রাম আমদানি করুন',
},
help: {
help: 'সাহায্য',
docs_website: 'ডকুমেন্টেশন',
visit_website: 'ChartDB ওয়েবসাইটে যান',
join_discord: 'আমাদের Discord-এ যোগ দিন',
schedule_a_call: 'আমাদের সাথে কথা বলুন!',

View File

@@ -35,14 +35,13 @@ export const de: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
// TODO: Translate
backup: {
backup: 'Backup',
share: {
share: 'Share',
export_diagram: 'Export Diagram',
restore_diagram: 'Restore Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'Hilfe',
docs_website: 'Dokumentation',
visit_website: 'ChartDB Webseite',
join_discord: 'Auf Discord beitreten',
schedule_a_call: 'Gespräch vereinbaren',

View File

@@ -33,14 +33,13 @@ export const en = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
backup: {
backup: 'Backup',
share: {
share: 'Share',
export_diagram: 'Export Diagram',
restore_diagram: 'Restore Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'Help',
docs_website: 'Docs',
visit_website: 'Visit ChartDB',
join_discord: 'Join us on Discord',
schedule_a_call: 'Talk with us!',

View File

@@ -34,14 +34,14 @@ export const es: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
backup: {
backup: 'Respaldo',
export_diagram: 'Exportar Diagrama',
restore_diagram: 'Restaurar Diagrama',
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'Ayuda',
docs_website: 'Documentación',
visit_website: 'Visitar ChartDB',
join_discord: 'Únete a nosotros en Discord',
schedule_a_call: '¡Habla con nosotros!',

View File

@@ -33,14 +33,13 @@ export const fr: LanguageTranslation = {
show_minimap: 'Afficher la Mini Carte',
hide_minimap: 'Masquer la Mini Carte',
},
backup: {
backup: 'Sauvegarde',
share: {
share: 'Partage',
export_diagram: 'Exporter le diagramme',
restore_diagram: 'Restaurer le diagramme',
import_diagram: 'Importer un diagramme',
},
help: {
help: 'Aide',
docs_website: 'Documentation',
visit_website: 'Visitez ChartDB',
join_discord: 'Rejoignez-nous sur Discord',
schedule_a_call: 'Parlez avec nous !',

View File

@@ -35,14 +35,13 @@ export const gu: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
backup: {
backup: 'બેકઅપ',
share: {
share: 'શેર કરો',
export_diagram: 'ડાયાગ્રામ નિકાસ કરો',
restore_diagram: 'ડાયાગ્રામ પુનઃસ્થાપિત કરો',
import_diagram: 'ડાયાગ્રામ આયાત કરો',
},
help: {
help: 'મદદ',
docs_website: 'દસ્તાવેજીકરણ',
visit_website: 'ChartDB વેબસાઇટ પર જાઓ',
join_discord: 'અમારા Discordમાં જોડાઓ',
schedule_a_call: 'અમારી સાથે વાત કરો!',

View File

@@ -34,14 +34,14 @@ export const hi: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
backup: {
backup: 'बैकअप',
export_diagram: 'आरेख निर्यात करें',
restore_diagram: 'आरेख पुनर्स्थापित करें',
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'मदद',
docs_website: 'દસ્તાવેજીકરણ',
visit_website: 'ChartDB वेबसाइट पर जाएँ',
join_discord: 'हमसे Discord पर जुड़ें',
schedule_a_call: 'हमसे बात करें!',

View File

@@ -34,14 +34,13 @@ export const id_ID: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
backup: {
backup: 'Cadangan',
share: {
share: 'Bagikan',
export_diagram: 'Ekspor Diagram',
restore_diagram: 'Pulihkan Diagram',
import_diagram: 'Impor Diagram',
},
help: {
help: 'Bantuan',
docs_website: 'દસ્તાવેજીકરણ',
visit_website: 'Kunjungi ChartDB',
join_discord: 'Bergabunglah di Discord kami',
schedule_a_call: 'Berbicara dengan kami!',

View File

@@ -36,14 +36,13 @@ export const ja: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
// TODO: Translate
backup: {
backup: 'Backup',
share: {
share: 'Share',
export_diagram: 'Export Diagram',
restore_diagram: 'Restore Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'ヘルプ',
docs_website: 'ドキュメント',
visit_website: 'ChartDBにアクセス',
join_discord: 'Discordに参加',
schedule_a_call: '話しかけてください!',

View File

@@ -34,14 +34,13 @@ export const ko_KR: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
backup: {
backup: '백업',
share: {
share: '공유',
export_diagram: '다이어그램 내보내기',
restore_diagram: '다이어그램 복구',
import_diagram: '다이어그램 가져오기',
},
help: {
help: '도움말',
docs_website: '선적 서류 비치',
visit_website: 'ChartDB 사이트 방문',
join_discord: 'Discord 가입',
schedule_a_call: 'Talk with us!',

View File

@@ -34,15 +34,14 @@ export const mr: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
backup: {
share: {
// TODO: Add translations
backup: 'Backup',
share: 'Share',
export_diagram: 'Export Diagram',
restore_diagram: 'Restore Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'मदत',
docs_website: 'दस्तऐवजीकरण',
visit_website: 'ChartDB ला भेट द्या',
join_discord: 'आमच्या डिस्कॉर्डमध्ये सामील व्हा',
schedule_a_call: 'आमच्याशी बोला!',

View File

@@ -34,15 +34,13 @@ export const ne: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
// TODO: Translate
backup: {
backup: 'Backup',
export_diagram: 'Export Diagram',
restore_diagram: 'Restore Diagram',
share: {
share: 'शेयर गर्नुहोस्',
export_diagram: 'डायाग्राम निर्यात गर्नुहोस्',
import_diagram: 'डायाग्राम आयात गर्नुहोस्',
},
help: {
help: 'मद्दत',
docs_website: 'कागजात',
visit_website: 'वेबसाइटमा जानुहोस्',
join_discord: 'डिस्कोर्डमा सामिल हुनुहोस्',
schedule_a_call: 'कल अनुसूची गर्नुहोस्',

View File

@@ -35,14 +35,13 @@ export const pt_BR: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
// TODO: Translate
backup: {
backup: 'Backup',
export_diagram: 'Exportar Diagrama',
restore_diagram: 'Restaurar Diagrama',
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
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!',

View File

@@ -34,15 +34,13 @@ export const ru: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
// TODO: Translate
backup: {
backup: 'Backup',
export_diagram: 'Export Diagram',
restore_diagram: 'Restore Diagram',
share: {
share: 'Поделиться',
export_diagram: 'Экспорт кода диаграммы',
import_diagram: 'Импорт кода диаграммы',
},
help: {
help: 'Помощь',
docs_website: 'Документация',
visit_website: 'Перейти на сайт ChartDB',
join_discord: 'Присоединиться к сообществу в Discord',
schedule_a_call: 'Поговорите с нами!',

View File

@@ -35,14 +35,13 @@ export const te: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
// TODO: Translate
backup: {
backup: 'Backup',
share: {
share: 'Share',
export_diagram: 'Export Diagram',
restore_diagram: 'Restore Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'సహాయం',
docs_website: 'డాక్యుమెంటేషన్',
visit_website: 'ChartDB సందర్శించండి',
join_discord: 'డిస్కార్డ్‌లో మా నుంచి చేరండి',
schedule_a_call: 'మాతో మాట్లాడండి!',

View File

@@ -35,14 +35,13 @@ export const tr: LanguageTranslation = {
hide_minimap: 'Hide Mini Map',
},
// TODO: Translate
backup: {
backup: 'Backup',
share: {
share: 'Share',
export_diagram: 'Export Diagram',
restore_diagram: 'Restore Diagram',
import_diagram: 'Import 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ş!',

View File

@@ -33,14 +33,13 @@ export const uk: LanguageTranslation = {
show_minimap: 'Показати мінімапу',
hide_minimap: 'Приховати мінімапу',
},
backup: {
backup: 'Резервне копіювання',
share: {
share: 'Поширити',
export_diagram: 'Експорт діаграми',
restore_diagram: 'Відновити діаграму',
import_diagram: 'Імпорт діаграми',
},
help: {
help: 'Довідка',
docs_website: 'Документація',
visit_website: 'Сайт ChartDB',
join_discord: 'Приєднуйтесь до нас в Діскорд',
schedule_a_call: 'Забронювати зустріч!',

View File

@@ -34,14 +34,13 @@ export const vi: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
backup: {
backup: 'Hỗ trợ',
share: {
share: 'Chia sẻ',
export_diagram: 'Xuất sơ đồ',
restore_diagram: 'Khôi phục sơ đồ',
import_diagram: 'Nhập 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!',

View File

@@ -34,14 +34,13 @@ export const zh_CN: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
backup: {
backup: '备份',
share: {
share: '分享',
export_diagram: '导出关系图',
restore_diagram: '还原图表',
import_diagram: '导入关系图',
},
help: {
help: '帮助',
docs_website: '文档',
visit_website: '访问 ChartDB',
join_discord: '在 Discord 上加入我们',
schedule_a_call: '和我们交流!',

View File

@@ -34,14 +34,13 @@ export const zh_TW: LanguageTranslation = {
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
},
backup: {
backup: '備份',
share: {
share: '分享',
export_diagram: '匯出圖表',
restore_diagram: '恢復圖表',
import_diagram: '匯入圖表',
},
help: {
help: '幫助',
docs_website: '文件',
visit_website: '訪問 ChartDB 網站',
join_discord: '加入 Discord',
schedule_a_call: '與我們聯絡!',

View File

@@ -1,82 +0,0 @@
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');
}

View File

@@ -1,247 +0,0 @@
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;
}

View File

@@ -1,10 +1,9 @@
import type { Diagram } from '../../domain/diagram';
import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env';
import { DatabaseType } from '@/lib/domain/database-type';
import type { 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;
@@ -13,10 +12,6 @@ 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);
@@ -231,10 +226,6 @@ 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);
@@ -252,16 +243,13 @@ 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 ??
LLM_MODEL_NAME ??
'gpt-4o-mini-2024-07-18';
const modelName = window?.env?.LLM_MODEL_NAME || 'gpt-4o-mini-2024-07-18';
let config: { apiKey: string; baseUrl?: string };
if (useCustomEndpoint) {
config = {
apiKey: apiKey,
apiKey: 'sk-xxx', // minimal valid API key format
baseUrl: baseUrl,
};
} else {

View File

@@ -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 AND ic.is_included_column = 0
WHERE s.name LIKE '%' AND i.name IS NOT NULL
),
tbls AS (
SELECT
@@ -324,7 +324,6 @@ 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

View File

@@ -1,12 +1,22 @@
import React, { Suspense, useCallback, useEffect, useRef } from 'react';
import React, {
Suspense,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { TopNavbar } from './top-navbar/top-navbar';
import { useParams } from 'react-router-dom';
import { useNavigate, useParams } from 'react-router-dom';
import { useConfig } from '@/hooks/use-config';
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';
@@ -25,10 +35,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;
@@ -46,12 +56,23 @@ export const EditorMobileLayoutLazy = React.lazy(
);
const EditorPageComponent: React.FC = () => {
const { diagramName, currentDiagram, schemas, filteredSchemas } =
useChartDB();
const {
loadDiagram,
diagramName,
currentDiagram,
schemas,
filteredSchemas,
} = useChartDB();
const { openSelectSchema, showSidePanel } = useLayout();
const { openStarUsDialog, openBuckleDialog } = useDialog();
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
const { showLoader, hideLoader } = useFullScreenLoader();
const { openCreateDiagramDialog, 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,
@@ -64,7 +85,73 @@ const EditorPageComponent: React.FC = () => {
} = useLocalConfig();
const { toast } = useToast();
const { t } = useTranslation();
const { initialDiagram } = useDiagramLoader();
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,
]);
useEffect(() => {
if (HIDE_BUCKLE_DOT_DEV) {

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React from 'react';
import { Plus, FileType2, FileKey2, MessageCircleMore } from 'lucide-react';
import { Button } from '@/components/button/button';
import {
@@ -70,29 +70,17 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
}
};
const createIndexHandler = useCallback(
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
setSelectedItems((prev) => {
if (prev.includes('indexes')) {
return prev;
}
const createIndexHandler = () => {
setSelectedItems((prev) => {
if (prev.includes('indexes')) {
return prev;
}
return [...prev, 'indexes'];
});
return [...prev, 'indexes'];
});
createIndex(table.id);
},
[createIndex, table.id, setSelectedItems]
);
const createFieldHandler = useCallback(
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
e.stopPropagation();
createField(table.id);
},
[createField, table.id]
);
createIndex(table.id);
};
return (
<div
@@ -125,7 +113,10 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
<Button
variant="ghost"
className="size-4 p-0 text-xs hover:bg-primary-foreground"
onClick={createFieldHandler}
onClick={(e) => {
e.stopPropagation();
createField(table.id);
}}
>
<Plus className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</Button>
@@ -162,18 +153,6 @@ 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>
@@ -194,7 +173,10 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
<Button
variant="ghost"
className="size-4 p-0 text-xs hover:bg-primary-foreground"
onClick={createIndexHandler}
onClick={(e) => {
e.stopPropagation();
createIndexHandler();
}}
>
<Plus className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</Button>
@@ -216,16 +198,6 @@ 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>
@@ -276,7 +248,7 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
<Button
variant="outline"
className="h-8 p-2 text-xs"
onClick={createFieldHandler}
onClick={() => createField(table.id)}
>
<FileType2 className="h-4" />
{t('side_panel.tables_section.table.add_field')}

View File

@@ -102,10 +102,6 @@ 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');
}, []);
@@ -229,9 +225,6 @@ export const Menu: React.FC<MenuProps> = () => {
{t('menu.file.import')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem onClick={openImportDiagramDialog}>
.json
</MenubarItem>
<MenubarItem onClick={() => openImportDBMLDialog()}>
.dbml
</MenubarItem>
@@ -348,10 +341,6 @@ 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 />
@@ -498,13 +487,13 @@ export const Menu: React.FC<MenuProps> = () => {
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>{t('menu.backup.backup')}</MenubarTrigger>
<MenubarTrigger>{t('menu.share.share')}</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={openExportDiagramDialog}>
{t('menu.backup.export_diagram')}
{t('menu.share.export_diagram')}
</MenubarItem>
<MenubarItem onClick={openImportDiagramDialog}>
{t('menu.backup.restore_diagram')}
{t('menu.share.import_diagram')}
</MenubarItem>
</MenubarContent>
</MenubarMenu>
@@ -512,9 +501,6 @@ 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>

View File

@@ -1,92 +0,0 @@
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 };
};