mirror of
https://github.com/chartdb/chartdb.git
synced 2025-10-28 18:43:53 +00:00
Compare commits
1 Commits
v1.8.1
...
jf/wrong_i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
595e3db0b3 |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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
4
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chartdb",
|
||||
"private": true,
|
||||
"version": "1.8.1",
|
||||
"version": "1.8.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
||||
useHotkeys(
|
||||
keyboardShortcutsForOS[KeyboardShortcutAction.OPEN_DIAGRAM]
|
||||
.keyCombination,
|
||||
() => openOpenDiagramDialog(),
|
||||
openOpenDiagramDialog,
|
||||
{
|
||||
preventDefault: true,
|
||||
},
|
||||
|
||||
@@ -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'
|
||||
)}
|
||||
|
||||
@@ -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(
|
||||
() =>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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: '!تحدث معنا',
|
||||
|
||||
@@ -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: 'আমাদের সাথে কথা বলুন!',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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!',
|
||||
|
||||
@@ -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!',
|
||||
|
||||
@@ -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 !',
|
||||
|
||||
@@ -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: 'અમારી સાથે વાત કરો!',
|
||||
|
||||
@@ -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: 'हमसे बात करें!',
|
||||
|
||||
@@ -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!',
|
||||
|
||||
@@ -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: '話しかけてください!',
|
||||
|
||||
@@ -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!',
|
||||
|
||||
@@ -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: 'आमच्याशी बोला!',
|
||||
|
||||
@@ -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: 'कल अनुसूची गर्नुहोस्',
|
||||
|
||||
@@ -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!',
|
||||
|
||||
@@ -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: 'Поговорите с нами!',
|
||||
|
||||
@@ -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: 'మాతో మాట్లాడండి!',
|
||||
|
||||
@@ -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ş!',
|
||||
|
||||
@@ -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: 'Забронювати зустріч!',
|
||||
|
||||
@@ -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!',
|
||||
|
||||
@@ -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: '和我们交流!',
|
||||
|
||||
@@ -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: '與我們聯絡!',
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
Reference in New Issue
Block a user