Compare commits

...

31 Commits

Author SHA1 Message Date
Guy Ben-Aharon
467ff697c9 chore(main): release 1.8.1 (#581) 2025-03-02 13:15:36 +02:00
Sibi Krishnamoorthy
d6919f3033 fix(docker config): Environment Variable Handling and Configuration Logic (#605)
* fixed custom openai endpoint not working

* minor fix
2025-03-02 13:12:39 +02:00
Guy Ben-Aharon
56382a9fdc fix(sql_server_export): use sql server export (#600) 2025-02-26 22:05:40 +02:00
Jonathan Fishner
e06eb2a48e fix(import-mssql): fix import/export scripts to handle data correctly (#598)
* fix(import-mssql): fix import/export scripts to handle data correctly

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-26 21:38:34 +02:00
Guy Ben-Aharon
543b716c77 refactor: move export diagram to hook (#599) 2025-02-26 20:03:26 +02:00
Jonathan Fishner
b55d631146 fix(add-docs): add link to ChartDB documentation (#597) 2025-02-24 21:35:10 +02:00
Guy Ben-Aharon
ef118929ad fix: open create new diagram when there is no diagram (#594) 2025-02-23 21:19:40 +02:00
Guy Ben-Aharon
68f48190c9 fix(open diagram): in case there is no diagram, opens the dialog (#593)
* fix(open diagram): in case there is no diagram, opens the dialog

* fix

* fix
2025-02-23 19:57:01 +02:00
Guy Ben-Aharon
bba265ad43 refactor(editor): import default diagram (#592)
* refactor(editor): import default diagram

* refactor(editor): import default diagram
2025-02-23 15:13:57 +02:00
Guy Ben-Aharon
cbc4e85a14 fix: components config (#591)
* fix: fix components config

* fix: fix components config
2025-02-23 09:16:55 +02:00
Jonathan Fishner
26a0a5b550 fix(menu-backup): update export to be backup (#590)
* fix(menu-backup): update copy change export to be backup

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-23 09:14:27 +02:00
Jonathan Fishner
b935b7f251 fix(img-export): add ChartDB watermark to exported image (#588)
* fix(img-export): add ChartDB watermark to exported image

* change watermark size to be smaller

* fix back the export for SVG
2025-02-23 08:42:37 +02:00
Jonathan Fishner
a1c0cf102a fix(side-panel): simplify how to add field and index (#573)
* fix(side-panel): simplify how to add feild and index

* fix(side-panel): auto-open the index Attributes when adding index

* fix(side-pannel): add focus after adding feild/index

* add some fixes

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-19 14:46:41 +02:00
Anthony Mini
ab89bad6d5 fix(i18n): add [FR] translation (#579)
Co-authored-by: Jonathan Fishner <jonathanfishner11@gmail.com>
2025-02-16 20:08:18 +02:00
Jonathan Fishner
deb218423f fix(sqlite-import): import nuallable columns correctly + add json type (#571) 2025-02-16 11:36:02 +02:00
Jonathan Fishner
48342471ac fix(empty-state): show diff buttons on import-dbml when triggered by empty (#574)
* feat(empty-state): trigger import-dbml when clicked empty diagram

* fix(empty-state): show diff buttons on import-dbml when triggered by empty

* fix(empty-state): add missing translations for empty state flow

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-13 17:53:21 +02:00
Guy Ben-Aharon
47bb87a88f chore(main): release 1.8.0 (#558) 2025-02-13 15:49:53 +02:00
Guy Ben-Aharon
a96c2e1078 fix(docker): add option to hide popups (#580) 2025-02-13 15:47:02 +02:00
Guy Ben-Aharon
26d95eed25 fix(table actions): fix size of table actions (#578) 2025-02-11 16:22:54 +02:00
Jonathan Fishner
be65328f24 fix(mssql-import): improve script readability by adding edition comment (#572)
* fix(mssql-import): improve script readability by adding edition comment

* fix(mssql-import): fixing issues with import query
2025-02-10 11:00:51 +02:00
Jonathan Fishner
85fd14fa02 fix(export-sql): show create script for only filtered schemas (#570)
* fix(export-sql): show create script for only filtered schemas

* add dependencies filter

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-10 10:54:22 +02:00
Jonathan Fishner
9c485b3b01 fix(sqlserver-import): open ssms guide when max chars (#565)
* fix(sqlserver-import): open ssms guide when max chars (truncated chars) for input

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-10 10:37:32 +02:00
Jonathan Fishner
e993f1549c fix(canvas): add right-click option to create relationships (#568)
* feat(create-relationship): add right-click option to easy create relationships

* add missing translations

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-10 10:06:31 +02:00
Jonathan Fishner
0db67ea42a fix(import dbml): add import for indexes (#566) 2025-02-10 09:12:11 +02:00
Jonathan Fishner
b9e621bd68 fix(realtionships section): add the schema to source/target tables (#561) 2025-02-09 23:23:11 +02:00
Jonathan Fishner
93d59f8887 fix(import-query): improve the cleanup for messy json input (#562) 2025-02-07 14:03:38 +02:00
Jonathan Fishner
190e4f4ffa feat(dbml-import): add error highlighting for dbml imports (#556)
* feat(dbml-import): add error highlighting for dbml imports

* fix deprecated api

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-02-06 21:42:14 +02:00
Guy Ben-Aharon
dc404c9d7e fix(canvas): locate table from canvas (#560) 2025-02-06 21:39:55 +02:00
Guy Ben-Aharon
dd4324d64f fix(index unique): extract unique toggle for faster editing (#559) 2025-02-06 21:37:11 +02:00
Calum Siemer
1878083056 feat(docker image): add support for custom inference servers (#543)
- Add OPENAI_API_ENDPOINT configuration
- Add LLM_MODEL_NAME configuration
- Update documentation for custom server setup
- Add error handling for endpoint configuration
2025-02-06 20:16:15 +02:00
Andrii Holovin
7b6271962a fix(i18n): fix Ukrainian (#554) 2025-02-06 18:59:06 +02:00
63 changed files with 1984 additions and 775 deletions

View File

@@ -1,5 +1,48 @@
# Changelog # 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)
### Features
* **dbml-import:** add error highlighting for dbml imports ([#556](https://github.com/chartdb/chartdb/issues/556)) ([190e4f4](https://github.com/chartdb/chartdb/commit/190e4f4ffa834fa621f264dc608ca3f3b393a331))
* **docker image:** add support for custom inference servers ([#543](https://github.com/chartdb/chartdb/issues/543)) ([1878083](https://github.com/chartdb/chartdb/commit/1878083056ea4db7a05cdeeb38a4f7b9f5f95bd1))
### Bug Fixes
* **canvas:** add right-click option to create relationships ([#568](https://github.com/chartdb/chartdb/issues/568)) ([e993f15](https://github.com/chartdb/chartdb/commit/e993f1549c4c86bb9e7e36062db803ba6613b3b3))
* **canvas:** locate table from canvas ([#560](https://github.com/chartdb/chartdb/issues/560)) ([dc404c9](https://github.com/chartdb/chartdb/commit/dc404c9d7ee272c93aac69646bac859829a5234e))
* **docker:** add option to hide popups ([#580](https://github.com/chartdb/chartdb/issues/580)) ([a96c2e1](https://github.com/chartdb/chartdb/commit/a96c2e107838d2dc13b586923fd9dbe06598cdd8))
* **export-sql:** show create script for only filtered schemas ([#570](https://github.com/chartdb/chartdb/issues/570)) ([85fd14f](https://github.com/chartdb/chartdb/commit/85fd14fa02bb2879c36bba53369dbf2e7fa578d4))
* **i18n:** fix Ukrainian ([#554](https://github.com/chartdb/chartdb/issues/554)) ([7b62719](https://github.com/chartdb/chartdb/commit/7b6271962a99bfe5ffbd0176e714c76368ef5c41))
* **import dbml:** add import for indexes ([#566](https://github.com/chartdb/chartdb/issues/566)) ([0db67ea](https://github.com/chartdb/chartdb/commit/0db67ea42a5f9585ca1d246db7a7ff0239bec0ba))
* **import-query:** improve the cleanup for messy json input ([#562](https://github.com/chartdb/chartdb/issues/562)) ([93d59f8](https://github.com/chartdb/chartdb/commit/93d59f8887765098d040a3184aaee32112f67267))
* **index unique:** extract unique toggle for faster editing ([#559](https://github.com/chartdb/chartdb/issues/559)) ([dd4324d](https://github.com/chartdb/chartdb/commit/dd4324d64f7638ada5c022a2ab38bd8e6986af25))
* **mssql-import:** improve script readability by adding edition comment ([#572](https://github.com/chartdb/chartdb/issues/572)) ([be65328](https://github.com/chartdb/chartdb/commit/be65328f24b0361638b9e2edb39eaa9906e77f67))
* **realtionships section:** add the schema to source/target tables ([#561](https://github.com/chartdb/chartdb/issues/561)) ([b9e621b](https://github.com/chartdb/chartdb/commit/b9e621bd680730a0ffbf1054d735bfa418711cae))
* **sqlserver-import:** open ssms guide when max chars ([#565](https://github.com/chartdb/chartdb/issues/565)) ([9c485b3](https://github.com/chartdb/chartdb/commit/9c485b3b01a131bf551c7e95916b0c416f6aa0b5))
* **table actions:** fix size of table actions ([#578](https://github.com/chartdb/chartdb/issues/578)) ([26d95ee](https://github.com/chartdb/chartdb/commit/26d95eed25d86452d9168a9d93a301ba50d934e3))
## [1.7.0](https://github.com/chartdb/chartdb/compare/v1.6.1...v1.7.0) (2025-02-03) ## [1.7.0](https://github.com/chartdb/chartdb/compare/v1.6.1...v1.7.0) (2025-02-03)

View File

@@ -1,6 +1,9 @@
FROM node:22-alpine AS builder FROM node:22-alpine AS builder
ARG VITE_OPENAI_API_KEY ARG VITE_OPENAI_API_KEY
ARG VITE_OPENAI_API_ENDPOINT
ARG VITE_LLM_MODEL_NAME
ARG VITE_HIDE_BUCKLE_DOT_DEV
WORKDIR /usr/src/app WORKDIR /usr/src/app
@@ -10,9 +13,13 @@ RUN npm ci
COPY . . COPY . .
RUN echo "VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}" > .env && \
echo "VITE_OPENAI_API_ENDPOINT=${VITE_OPENAI_API_ENDPOINT}" >> .env && \
echo "VITE_LLM_MODEL_NAME=${VITE_LLM_MODEL_NAME}" >> .env && \
echo "VITE_HIDE_BUCKLE_DOT_DEV=${VITE_HIDE_BUCKLE_DOT_DEV}" >> .env
RUN npm run build RUN npm run build
# Use a lightweight web server to serve the production build
FROM nginx:stable-alpine AS production FROM nginx:stable-alpine AS production
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
@@ -20,7 +27,6 @@ COPY ./default.conf.template /etc/nginx/conf.d/default.conf.template
COPY entrypoint.sh /entrypoint.sh COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh
# Expose the default port for the Nginx web server
EXPOSE 80 EXPOSE 80
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -107,8 +107,33 @@ docker build -t chartdb .
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 chartdb docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 chartdb
``` ```
#### Using Custom Inference Server
```bash
# Build
docker build \
--build-arg VITE_OPENAI_API_ENDPOINT=<YOUR_ENDPOINT> \
--build-arg VITE_LLM_MODEL_NAME=<YOUR_MODEL_NAME> \
-t chartdb .
# Run
docker run \
-e OPENAI_API_ENDPOINT=<YOUR_ENDPOINT> \
-e LLM_MODEL_NAME=<YOUR_MODEL_NAME> \
-p 8080:80 chartdb
```
> **Note:** You must configure either Option 1 (OpenAI API key) OR Option 2 (Custom endpoint and model name) for AI capabilities to work. Do not mix the two options.
Open your browser and navigate to `http://localhost:8080`. Open your browser and navigate to `http://localhost:8080`.
Example configuration for a local vLLM server:
```bash
VITE_OPENAI_API_ENDPOINT=http://localhost:8000/v1
VITE_LLM_MODEL_NAME=Qwen/Qwen2.5-32B-Instruct-AWQ
```
## Try it on our website ## Try it on our website
1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2) 1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2)

View File

@@ -12,6 +12,9 @@
}, },
"aliases": { "aliases": {
"components": "src/components", "components": "src/components",
"utils": "@/lib/utils" "utils": "src/lib/utils",
"ui": "src/components/ui",
"lib": "src/lib",
"hooks": "src/hooks"
} }
} }

View File

@@ -10,7 +10,12 @@ server {
location /config.js { location /config.js {
default_type application/javascript; default_type application/javascript;
return 200 "window.env = { OPENAI_API_KEY: \"$OPENAI_API_KEY\" };"; return 200 "window.env = {
OPENAI_API_KEY: \"$OPENAI_API_KEY\",
OPENAI_API_ENDPOINT: \"$OPENAI_API_ENDPOINT\",
LLM_MODEL_NAME: \"$LLM_MODEL_NAME\",
HIDE_BUCKLE_DOT_DEV: \"$HIDE_BUCKLE_DOT_DEV\"
};";
} }
error_page 500 502 503 504 /50x.html; error_page 500 502 503 504 /50x.html;

View File

@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# Replace placeholders in nginx.conf # Replace placeholders in nginx.conf
envsubst '${OPENAI_API_KEY}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf envsubst '${OPENAI_API_KEY} ${OPENAI_API_ENDPOINT} ${LLM_MODEL_NAME} ${HIDE_BUCKLE_DOT_DEV}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
# Start Nginx # Start Nginx
nginx -g "daemon off;" nginx -g "daemon off;"

4
package-lock.json generated
View File

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

View File

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

View File

@@ -6,6 +6,9 @@ import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sq
import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog'; import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog';
import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog'; import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/import-diagram-dialog'; 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 { export interface DialogContext {
// Create diagram dialog // Create diagram dialog
@@ -13,7 +16,9 @@ export interface DialogContext {
closeCreateDiagramDialog: () => void; closeCreateDiagramDialog: () => void;
// Open diagram dialog // Open diagram dialog
openOpenDiagramDialog: () => void; openOpenDiagramDialog: (
params?: Omit<OpenDiagramDialogProps, 'dialog'>
) => void;
closeOpenDiagramDialog: () => void; closeOpenDiagramDialog: () => void;
// Export SQL dialog // Export SQL dialog
@@ -21,7 +26,9 @@ export interface DialogContext {
closeExportSQLDialog: () => void; closeExportSQLDialog: () => void;
// Create relationship dialog // Create relationship dialog
openCreateRelationshipDialog: () => void; openCreateRelationshipDialog: (
params?: Omit<CreateRelationshipDialogProps, 'dialog'>
) => void;
closeCreateRelationshipDialog: () => void; closeCreateRelationshipDialog: () => void;
// Import database dialog // Import database dialog
@@ -63,7 +70,9 @@ export interface DialogContext {
closeImportDiagramDialog: () => void; closeImportDiagramDialog: () => void;
// Import DBML dialog // Import DBML dialog
openImportDBMLDialog: () => void; openImportDBMLDialog: (
params?: Omit<ImportDBMLDialogProps, 'dialog'>
) => void;
closeImportDBMLDialog: () => void; closeImportDBMLDialog: () => void;
} }

View File

@@ -2,10 +2,12 @@ import React, { useCallback, useState } from 'react';
import type { DialogContext } from './dialog-context'; import type { DialogContext } from './dialog-context';
import { dialogContext } from './dialog-context'; import { dialogContext } from './dialog-context';
import { CreateDiagramDialog } from '@/dialogs/create-diagram-dialog/create-diagram-dialog'; 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 { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog'; import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog'; import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog';
import { DatabaseType } from '@/lib/domain/database-type'; import { DatabaseType } from '@/lib/domain/database-type';
import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog'; import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog'; import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
import { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog'; import { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog';
@@ -18,6 +20,7 @@ import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-di
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog'; import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog'; import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
import { BuckleDialog } from '@/dialogs/buckle-dialog/buckle-dialog'; import { BuckleDialog } from '@/dialogs/buckle-dialog/buckle-dialog';
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog'; import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
export const DialogProvider: React.FC<React.PropsWithChildren> = ({ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
@@ -25,9 +28,31 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
}) => { }) => {
const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false); const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false);
const [openOpenDiagramDialog, setOpenOpenDiagramDialog] = 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] = const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] =
useState(false); useState(false);
const [createRelationshipDialogParams, setCreateRelationshipDialogParams] =
useState<Omit<CreateRelationshipDialogProps, 'dialog'>>();
const openCreateRelationshipDialogHandler: DialogContext['openCreateRelationshipDialog'] =
useCallback(
(params) => {
setCreateRelationshipDialogParams(params);
setOpenCreateRelationshipDialog(true);
},
[setOpenCreateRelationshipDialog]
);
const [openStarUsDialog, setOpenStarUsDialog] = useState(false); const [openStarUsDialog, setOpenStarUsDialog] = useState(false);
const [openBuckleDialog, setOpenBuckleDialog] = useState(false); const [openBuckleDialog, setOpenBuckleDialog] = useState(false);
@@ -99,18 +124,20 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
// Import DBML dialog // Import DBML dialog
const [openImportDBMLDialog, setOpenImportDBMLDialog] = useState(false); const [openImportDBMLDialog, setOpenImportDBMLDialog] = useState(false);
const [importDBMLDialogParams, setImportDBMLDialogParams] =
useState<Omit<ImportDBMLDialogProps, 'dialog'>>();
return ( return (
<dialogContext.Provider <dialogContext.Provider
value={{ value={{
openCreateDiagramDialog: () => setOpenNewDiagramDialog(true), openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false), closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
openOpenDiagramDialog: () => setOpenOpenDiagramDialog(true), openOpenDiagramDialog: openOpenDiagramDialogHandler,
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false), closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
openExportSQLDialog: openExportSQLDialogHandler, openExportSQLDialog: openExportSQLDialogHandler,
closeExportSQLDialog: () => setOpenExportSQLDialog(false), closeExportSQLDialog: () => setOpenExportSQLDialog(false),
openCreateRelationshipDialog: () => openCreateRelationshipDialog:
setOpenCreateRelationshipDialog(true), openCreateRelationshipDialogHandler,
closeCreateRelationshipDialog: () => closeCreateRelationshipDialog: () =>
setOpenCreateRelationshipDialog(false), setOpenCreateRelationshipDialog(false),
openImportDatabaseDialog: openImportDatabaseDialogHandler, openImportDatabaseDialog: openImportDatabaseDialogHandler,
@@ -130,19 +157,26 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
openImportDiagramDialog: () => setOpenImportDiagramDialog(true), openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
closeImportDiagramDialog: () => closeImportDiagramDialog: () =>
setOpenImportDiagramDialog(false), setOpenImportDiagramDialog(false),
openImportDBMLDialog: () => setOpenImportDBMLDialog(true), openImportDBMLDialog: (params) => {
setImportDBMLDialogParams(params);
setOpenImportDBMLDialog(true);
},
closeImportDBMLDialog: () => setOpenImportDBMLDialog(false), closeImportDBMLDialog: () => setOpenImportDBMLDialog(false),
}} }}
> >
{children} {children}
<CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} /> <CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} />
<OpenDiagramDialog dialog={{ open: openOpenDiagramDialog }} /> <OpenDiagramDialog
dialog={{ open: openOpenDiagramDialog }}
{...openDiagramDialogParams}
/>
<ExportSQLDialog <ExportSQLDialog
dialog={{ open: openExportSQLDialog }} dialog={{ open: openExportSQLDialog }}
{...exportSQLDialogParams} {...exportSQLDialogParams}
/> />
<CreateRelationshipDialog <CreateRelationshipDialog
dialog={{ open: openCreateRelationshipDialog }} dialog={{ open: openCreateRelationshipDialog }}
{...createRelationshipDialogParams}
/> />
<ImportDatabaseDialog <ImportDatabaseDialog
dialog={{ open: openImportDatabaseDialog }} dialog={{ open: openImportDatabaseDialog }}
@@ -160,7 +194,10 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} /> <ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} /> <ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
<BuckleDialog dialog={{ open: openBuckleDialog }} /> <BuckleDialog dialog={{ open: openBuckleDialog }} />
<ImportDBMLDialog dialog={{ open: openImportDBMLDialog }} /> <ImportDBMLDialog
dialog={{ open: openImportDBMLDialog }}
{...importDBMLDialogParams}
/>
</dialogContext.Provider> </dialogContext.Provider>
); );
}; };

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo, useEffect, useState } from 'react';
import type { ExportImageContext, ImageType } from './export-image-context'; import type { ExportImageContext, ImageType } from './export-image-context';
import { exportImageContext } from './export-image-context'; import { exportImageContext } from './export-image-context';
import { toJpeg, toPng, toSvg } from 'html-to-image'; import { toJpeg, toPng, toSvg } from 'html-to-image';
@@ -6,6 +6,8 @@ import { useReactFlow } from '@xyflow/react';
import { useChartDB } from '@/hooks/use-chartdb'; import { useChartDB } from '@/hooks/use-chartdb';
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner'; import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
import { useTheme } from '@/hooks/use-theme'; 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> = ({ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
children, children,
@@ -14,6 +16,24 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
const { setNodes, getViewport } = useReactFlow(); const { setNodes, getViewport } = useReactFlow();
const { effectiveTheme } = useTheme(); const { effectiveTheme } = useTheme();
const { diagramName } = useChartDB(); 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( const downloadImage = useCallback(
(dataUrl: string, type: ImageType) => { (dataUrl: string, type: ImageType) => {
@@ -128,16 +148,22 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
'http://www.w3.org/2000/svg', 'http://www.w3.org/2000/svg',
'rect' 'rect'
); );
const padding = 2000; const bgPadding = 2000;
backgroundRect.setAttribute('x', String(-viewport.x - padding)); backgroundRect.setAttribute(
backgroundRect.setAttribute('y', String(-viewport.y - padding)); 'x',
String(-viewport.x - bgPadding)
);
backgroundRect.setAttribute(
'y',
String(-viewport.y - bgPadding)
);
backgroundRect.setAttribute( backgroundRect.setAttribute(
'width', 'width',
String(reactFlowBounds.width + 2 * padding) String(reactFlowBounds.width + 2 * bgPadding)
); );
backgroundRect.setAttribute( backgroundRect.setAttribute(
'height', 'height',
String(reactFlowBounds.height + 2 * padding) String(reactFlowBounds.height + 2 * bgPadding)
); );
backgroundRect.setAttribute('fill', 'url(#background-pattern)'); backgroundRect.setAttribute('fill', 'url(#background-pattern)');
tempSvg.appendChild(backgroundRect); tempSvg.appendChild(backgroundRect);
@@ -148,15 +174,9 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
); );
try { try {
// Handle SVG export differently
if (type === 'svg') {
const dataUrl = await imageCreateFn(viewportElement, { const dataUrl = await imageCreateFn(viewportElement, {
...(type === 'jpeg' || type === 'png'
? {
backgroundColor:
effectiveTheme === 'light'
? '#ffffff'
: '#141414',
}
: {}),
width: reactFlowBounds.width, width: reactFlowBounds.width,
height: reactFlowBounds.height, height: reactFlowBounds.height,
style: { style: {
@@ -168,8 +188,96 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
pixelRatio: scale, pixelRatio: scale,
skipFonts: true, skipFonts: true,
}); });
downloadImage(dataUrl, type); 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);
};
});
} finally { } finally {
viewportElement.removeChild(tempSvg); viewportElement.removeChild(tempSvg);
hideLoader(); hideLoader();
@@ -184,6 +292,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
setNodes, setNodes,
showLoader, showLoader,
effectiveTheme, effectiveTheme,
logoBase64,
] ]
); );

View File

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

View File

@@ -85,6 +85,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
const [showCheckJsonButton, setShowCheckJsonButton] = useState(false); const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
const [isCheckingJson, setIsCheckingJson] = useState(false); const [isCheckingJson, setIsCheckingJson] = useState(false);
const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false);
useEffect(() => { useEffect(() => {
const loadScripts = async () => { const loadScripts = async () => {
const { importMetadataScripts } = await import( const { importMetadataScripts } = await import(
@@ -127,6 +129,11 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
(e: React.ChangeEvent<HTMLTextAreaElement>) => { (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const inputValue = e.target.value; const inputValue = e.target.value;
setScriptResult(inputValue); setScriptResult(inputValue);
// Automatically open SSMS info when input length is exactly 65535
if (inputValue.length === 65535) {
setShowSSMSInfoDialog(true);
}
}, },
[setScriptResult] [setScriptResult]
); );
@@ -245,7 +252,10 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
{t('new_diagram_dialog.import_database.step_1')} {t('new_diagram_dialog.import_database.step_1')}
</div> </div>
{databaseType === DatabaseType.SQL_SERVER && ( {databaseType === DatabaseType.SQL_SERVER && (
<SSMSInfo /> <SSMSInfo
open={showSSMSInfoDialog}
setOpen={setShowSSMSInfoDialog}
/>
)} )}
</div> </div>
{databaseTypeToClientsMap[databaseType].length > 0 ? ( {databaseTypeToClientsMap[databaseType].length > 0 ? (
@@ -369,6 +379,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
showCheckJsonButton, showCheckJsonButton,
isCheckingJson, isCheckingJson,
handleCheckJson, handleCheckJson,
showSSMSInfoDialog,
setShowSSMSInfoDialog,
]); ]);
const renderFooter = useCallback(() => { const renderFooter = useCallback(() => {

View File

@@ -4,32 +4,55 @@ import {
HoverCardTrigger, HoverCardTrigger,
} from '@/components/hover-card/hover-card'; } from '@/components/hover-card/hover-card';
import { Label } from '@/components/label/label'; import { Label } from '@/components/label/label';
import { Info } from 'lucide-react'; import { Info, X } from 'lucide-react';
import React from 'react'; import React, { useCallback, useEffect, useMemo } from 'react';
import SSMSInstructions from '@/assets/ssms-instructions.png'; import SSMSInstructions from '@/assets/ssms-instructions.png';
import { ZoomableImage } from '@/components/zoomable-image/zoomable-image'; import { ZoomableImage } from '@/components/zoomable-image/zoomable-image';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
export interface SSMSInfoProps {} export interface SSMSInfoProps {
open?: boolean;
setOpen?: (open: boolean) => void;
}
export const SSMSInfo = React.forwardRef< export const SSMSInfo = React.forwardRef<
React.ElementRef<typeof HoverCardTrigger>, React.ElementRef<typeof HoverCardTrigger>,
SSMSInfoProps SSMSInfoProps
>((props, ref) => { >(({ open: controlledOpen, setOpen: setControlledOpen }, ref) => {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => {
if (controlledOpen) {
setOpen(true);
}
}, [controlledOpen]);
const closeHandler = useCallback(() => {
setOpen(false);
setControlledOpen?.(false);
}, [setControlledOpen]);
const isOpen = useMemo(
() => open || controlledOpen,
[open, controlledOpen]
);
return ( return (
<HoverCard <HoverCard
open={open} open={isOpen}
onOpenChange={(isOpen) => { onOpenChange={(isOpen) => {
if (controlledOpen) {
return;
}
setOpen(isOpen); setOpen(isOpen);
}} }}
> >
<HoverCardTrigger ref={ref} {...props} asChild> <HoverCardTrigger ref={ref} asChild>
<div <div
className="flex flex-row items-center gap-1 text-pink-600" className="flex flex-row items-center gap-1 text-pink-600"
onClick={() => { onClick={() => {
setOpen(!open); setOpen?.(!open);
}} }}
> >
<Info size={14} /> <Info size={14} />
@@ -41,13 +64,21 @@ export const SSMSInfo = React.forwardRef<
</div> </div>
</HoverCardTrigger> </HoverCardTrigger>
<HoverCardContent className="w-80"> <HoverCardContent className="w-80">
<div className="flex"> <div className="flex flex-col">
<div className="space-y-1"> <div className="flex items-start justify-between">
<h4 className="text-sm font-semibold"> <h4 className="text-sm font-semibold">
{t( {t(
'new_diagram_dialog.import_database.ssms_instructions.title' 'new_diagram_dialog.import_database.ssms_instructions.title'
)} )}
</h4> </h4>
<button
onClick={closeHandler}
className="text-muted-foreground hover:text-foreground"
>
<X size={16} />
</button>
</div>
<div className="space-y-1">
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
<span className="font-semibold">1. </span> <span className="font-semibold">1. </span>
{t( {t(

View File

@@ -28,7 +28,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
const [databaseType, setDatabaseType] = useState<DatabaseType>( const [databaseType, setDatabaseType] = useState<DatabaseType>(
DatabaseType.GENERIC DatabaseType.GENERIC
); );
const { closeCreateDiagramDialog } = useDialog(); const { closeCreateDiagramDialog, openImportDBMLDialog } = useDialog();
const { updateConfig } = useConfig(); const { updateConfig } = useConfig();
const [scriptResult, setScriptResult] = useState(''); const [scriptResult, setScriptResult] = useState('');
const [databaseEdition, setDatabaseEdition] = useState< const [databaseEdition, setDatabaseEdition] = useState<
@@ -104,6 +104,10 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
await updateConfig({ defaultDiagramId: diagram.id }); await updateConfig({ defaultDiagramId: diagram.id });
closeCreateDiagramDialog(); closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`); navigate(`/diagrams/${diagram.id}`);
setTimeout(
() => openImportDBMLDialog({ withCreateEmptyDiagram: true }),
700
);
}, [ }, [
databaseType, databaseType,
addDiagram, addDiagram,
@@ -112,6 +116,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
navigate, navigate,
updateConfig, updateConfig,
diagramNumber, diagramNumber,
openImportDBMLDialog,
]); ]);
return ( return (

View File

@@ -22,13 +22,17 @@ import { areFieldTypesCompatible } from '@/lib/data/data-types/data-types';
const ErrorMessageRelationshipFieldsNotSameType = const ErrorMessageRelationshipFieldsNotSameType =
'Relationships can only be created between fields of the same type'; 'Relationships can only be created between fields of the same type';
export interface CreateRelationshipDialogProps extends BaseDialogProps {} export interface CreateRelationshipDialogProps extends BaseDialogProps {
sourceTableId?: string;
}
export const CreateRelationshipDialog: React.FC< export const CreateRelationshipDialog: React.FC<
CreateRelationshipDialogProps CreateRelationshipDialogProps
> = ({ dialog }) => { > = ({ dialog, sourceTableId: preSelectedSourceTableId }) => {
const { closeCreateRelationshipDialog } = useDialog(); const { closeCreateRelationshipDialog } = useDialog();
const [primaryTableId, setPrimaryTableId] = useState<string | undefined>(); const [primaryTableId, setPrimaryTableId] = useState<string | undefined>(
preSelectedSourceTableId
);
const [primaryFieldId, setPrimaryFieldId] = useState<string | undefined>(); const [primaryFieldId, setPrimaryFieldId] = useState<string | undefined>();
const [referencedTableId, setReferencedTableId] = useState< const [referencedTableId, setReferencedTableId] = useState<
string | undefined string | undefined
@@ -43,6 +47,9 @@ export const CreateRelationshipDialog: React.FC<
const [canCreateRelationship, setCanCreateRelationship] = useState(false); const [canCreateRelationship, setCanCreateRelationship] = useState(false);
const { fitView, setEdges } = useReactFlow(); const { fitView, setEdges } = useReactFlow();
const { databaseType } = useChartDB(); const { databaseType } = useChartDB();
const [primaryFieldSelectOpen, setPrimaryFieldSelectOpen] = useState(false);
const [referencedTableSelectOpen, setReferencedTableSelectOpen] =
useState(false);
const tableOptions = useMemo(() => { const tableOptions = useMemo(() => {
return tables.map( return tables.map(
@@ -89,8 +96,23 @@ export const CreateRelationshipDialog: React.FC<
setReferencedTableId(undefined); setReferencedTableId(undefined);
setReferencedFieldId(undefined); setReferencedFieldId(undefined);
setErrorMessage(''); setErrorMessage('');
setPrimaryFieldSelectOpen(false);
setReferencedTableSelectOpen(false);
}, [dialog.open]); }, [dialog.open]);
useEffect(() => {
if (preSelectedSourceTableId) {
const table = getTable(preSelectedSourceTableId);
if (table) {
setPrimaryTableId(preSelectedSourceTableId);
}
setTimeout(() => {
setPrimaryFieldSelectOpen(true);
}, 100);
}
}, [preSelectedSourceTableId, getTable]);
useEffect(() => { useEffect(() => {
setCanCreateRelationship(false); setCanCreateRelationship(false);
setErrorMessage(''); setErrorMessage('');
@@ -223,8 +245,14 @@ export const CreateRelationshipDialog: React.FC<
)} )}
value={primaryTableId} value={primaryTableId}
onChange={(value) => { onChange={(value) => {
setPrimaryTableId(value as string); const newTableId = value as string;
setPrimaryTableId(newTableId);
if (
newTableId !==
preSelectedSourceTableId
) {
setPrimaryFieldId(undefined); setPrimaryFieldId(undefined);
}
}} }}
emptyPlaceholder={t( emptyPlaceholder={t(
'create_relationship_dialog.no_tables_found' 'create_relationship_dialog.no_tables_found'
@@ -253,6 +281,8 @@ export const CreateRelationshipDialog: React.FC<
'create_relationship_dialog.primary_field_placeholder' 'create_relationship_dialog.primary_field_placeholder'
)} )}
value={primaryFieldId} value={primaryFieldId}
open={primaryFieldSelectOpen}
onOpenChange={setPrimaryFieldSelectOpen}
onChange={(value) => onChange={(value) =>
setPrimaryFieldId(value as string) setPrimaryFieldId(value as string)
} }
@@ -283,6 +313,8 @@ export const CreateRelationshipDialog: React.FC<
'create_relationship_dialog.referenced_table_placeholder' 'create_relationship_dialog.referenced_table_placeholder'
)} )}
value={referencedTableId} value={referencedTableId}
open={referencedTableSelectOpen}
onOpenChange={setReferencedTableSelectOpen}
onChange={(value) => { onChange={(value) => {
setReferencedTableId(value as string); setReferencedTableId(value as string);
setReferencedFieldId(undefined); setReferencedFieldId(undefined);

View File

@@ -15,11 +15,10 @@ import { SelectBox } from '@/components/select-box/select-box';
import type { BaseDialogProps } from '../common/base-dialog-props'; import type { BaseDialogProps } from '../common/base-dialog-props';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useChartDB } from '@/hooks/use-chartdb'; import { useChartDB } from '@/hooks/use-chartdb';
import { diagramToJSONOutput } from '@/lib/export-import-utils';
import { Spinner } from '@/components/spinner/spinner'; import { Spinner } from '@/components/spinner/spinner';
import { waitFor } from '@/lib/utils';
import { AlertCircle } from 'lucide-react'; import { AlertCircle } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert'; import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert';
import { useExportDiagram } from '@/hooks/use-export-diagram';
export interface ExportDiagramDialogProps extends BaseDialogProps {} export interface ExportDiagramDialogProps extends BaseDialogProps {}
@@ -27,44 +26,27 @@ export const ExportDiagramDialog: React.FC<ExportDiagramDialogProps> = ({
dialog, dialog,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { diagramName, currentDiagram } = useChartDB(); const { currentDiagram } = useChartDB();
const [isLoading, setIsLoading] = useState(false);
const { closeExportDiagramDialog } = useDialog(); const { closeExportDiagramDialog } = useDialog();
const [error, setError] = useState(false); const [error, setError] = useState(false);
useEffect(() => { useEffect(() => {
if (!dialog.open) return; if (!dialog.open) return;
setIsLoading(false);
setError(false); setError(false);
}, [dialog.open]); }, [dialog.open]);
const downloadOutput = useCallback( const { exportDiagram, isExporting: isLoading } = useExportDiagram();
(dataUrl: string) => {
const a = document.createElement('a');
a.setAttribute('download', `ChartDB(${diagramName}).json`);
a.setAttribute('href', dataUrl);
a.click();
},
[diagramName]
);
const handleExport = useCallback(async () => { const handleExport = useCallback(async () => {
setIsLoading(true);
await waitFor(1000);
try { try {
const json = diagramToJSONOutput(currentDiagram); await exportDiagram({ diagram: currentDiagram });
const blob = new Blob([json], { type: 'application/json' });
const dataUrl = URL.createObjectURL(blob);
downloadOutput(dataUrl);
setIsLoading(false);
closeExportDiagramDialog(); closeExportDiagramDialog();
} catch (e) { } catch (e) {
setError(true); setError(true);
setIsLoading(false);
throw e; throw e;
} }
}, [downloadOutput, currentDiagram, closeExportDiagramDialog]); }, [exportDiagram, currentDiagram, closeExportDiagramDialog]);
const outputTypeOptions: SelectBoxOption[] = useMemo( const outputTypeOptions: SelectBoxOption[] = useMemo(
() => () =>

View File

@@ -20,10 +20,12 @@ import {
} from '@/lib/data/export-metadata/export-sql-script'; } from '@/lib/data/export-metadata/export-sql-script';
import { databaseTypeToLabelMap } from '@/lib/databases'; import { databaseTypeToLabelMap } from '@/lib/databases';
import { DatabaseType } from '@/lib/domain/database-type'; import { DatabaseType } from '@/lib/domain/database-type';
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
import { Annoyed, Sparkles } from 'lucide-react'; import { Annoyed, Sparkles } from 'lucide-react';
import React, { useCallback, useEffect, useRef } from 'react'; import React, { useCallback, useEffect, useRef } from 'react';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import type { BaseDialogProps } from '../common/base-dialog-props'; import type { BaseDialogProps } from '../common/base-dialog-props';
import type { Diagram } from '@/lib/domain/diagram';
export interface ExportSQLDialogProps extends BaseDialogProps { export interface ExportSQLDialogProps extends BaseDialogProps {
targetDatabaseType: DatabaseType; targetDatabaseType: DatabaseType;
@@ -34,7 +36,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
targetDatabaseType, targetDatabaseType,
}) => { }) => {
const { closeExportSQLDialog } = useDialog(); const { closeExportSQLDialog } = useDialog();
const { currentDiagram } = useChartDB(); const { currentDiagram, filteredSchemas } = useChartDB();
const { t } = useTranslation(); const { t } = useTranslation();
const [script, setScript] = React.useState<string>(); const [script, setScript] = React.useState<string>();
const [error, setError] = React.useState<boolean>(false); const [error, setError] = React.useState<boolean>(false);
@@ -43,17 +45,58 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
const abortControllerRef = useRef<AbortController | null>(null); const abortControllerRef = useRef<AbortController | null>(null);
const exportSQLScript = useCallback(async () => { const exportSQLScript = useCallback(async () => {
const filteredDiagram: Diagram = {
...currentDiagram,
tables: currentDiagram.tables?.filter((table) =>
shouldShowTablesBySchemaFilter(table, filteredSchemas)
),
relationships: currentDiagram.relationships?.filter((rel) => {
const sourceTable = currentDiagram.tables?.find(
(t) => t.id === rel.sourceTableId
);
const targetTable = currentDiagram.tables?.find(
(t) => t.id === rel.targetTableId
);
return (
sourceTable &&
targetTable &&
shouldShowTablesBySchemaFilter(
sourceTable,
filteredSchemas
) &&
shouldShowTablesBySchemaFilter(targetTable, filteredSchemas)
);
}),
dependencies: currentDiagram.dependencies?.filter((dep) => {
const table = currentDiagram.tables?.find(
(t) => t.id === dep.tableId
);
const dependentTable = currentDiagram.tables?.find(
(t) => t.id === dep.dependentTableId
);
return (
table &&
dependentTable &&
shouldShowTablesBySchemaFilter(table, filteredSchemas) &&
shouldShowTablesBySchemaFilter(
dependentTable,
filteredSchemas
)
);
}),
};
if (targetDatabaseType === DatabaseType.GENERIC) { if (targetDatabaseType === DatabaseType.GENERIC) {
return Promise.resolve(exportBaseSQL(currentDiagram)); return Promise.resolve(exportBaseSQL(filteredDiagram));
} else { } else {
return exportSQL(currentDiagram, targetDatabaseType, { return exportSQL(filteredDiagram, targetDatabaseType, {
stream: true, stream: true,
onResultStream: (text) => onResultStream: (text) =>
setScript((prev) => (prev ? prev + text : text)), setScript((prev) => (prev ? prev + text : text)),
signal: abortControllerRef.current?.signal, signal: abortControllerRef.current?.signal,
}); });
} }
}, [targetDatabaseType, currentDiagram]); }, [targetDatabaseType, currentDiagram, filteredSchemas]);
useEffect(() => { useEffect(() => {
if (!dialog.open) { if (!dialog.open) {

View File

@@ -1,4 +1,11 @@
import React, { useCallback, useEffect, useState, Suspense } from 'react'; import React, {
useCallback,
useEffect,
useState,
Suspense,
useRef,
} from 'react';
import * as monaco from 'monaco-editor';
import { useDialog } from '@/hooks/use-dialog'; import { useDialog } from '@/hooks/use-dialog';
import { import {
Dialog, Dialog,
@@ -23,11 +30,54 @@ import { useCanvas } from '@/hooks/use-canvas';
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language'; import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
import { useToast } from '@/components/toast/use-toast'; import { useToast } from '@/components/toast/use-toast';
import { Spinner } from '@/components/spinner/spinner'; import { Spinner } from '@/components/spinner/spinner';
import { debounce } from '@/lib/utils';
export interface ImportDBMLDialogProps extends BaseDialogProps {} interface DBMLError {
message: string;
line: number;
column: number;
}
function parseDBMLError(error: unknown): DBMLError | null {
try {
if (typeof error === 'string') {
const parsed = JSON.parse(error);
if (parsed.diags?.[0]) {
const diag = parsed.diags[0];
return {
message: diag.message,
line: diag.location.start.line,
column: diag.location.start.column,
};
}
} else if (error && typeof error === 'object' && 'diags' in error) {
const parsed = error as {
diags: Array<{
message: string;
location: { start: { line: number; column: number } };
}>;
};
if (parsed.diags?.[0]) {
return {
message: parsed.diags[0].message,
line: parsed.diags[0].location.start.line,
column: parsed.diags[0].location.start.column,
};
}
}
} catch (e) {
console.error('Error parsing DBML error:', e);
}
return null;
}
export interface ImportDBMLDialogProps extends BaseDialogProps {
withCreateEmptyDiagram?: boolean;
}
export const ImportDBMLDialog: React.FC<ImportDBMLDialogProps> = ({ export const ImportDBMLDialog: React.FC<ImportDBMLDialogProps> = ({
dialog, dialog,
withCreateEmptyDiagram,
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const initialDBML = `// Use DBML to define your database structure const initialDBML = `// Use DBML to define your database structure
@@ -75,6 +125,16 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
} = useChartDB(); } = useChartDB();
const { reorderTables } = useCanvas(); const { reorderTables } = useCanvas();
const [reorder, setReorder] = useState(false); const [reorder, setReorder] = useState(false);
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
const decorationsCollection =
useRef<monaco.editor.IEditorDecorationsCollection>();
const handleEditorDidMount = (
editor: monaco.editor.IStandaloneCodeEditor
) => {
editorRef.current = editor;
decorationsCollection.current = editor.createDecorationsCollection();
};
useEffect(() => { useEffect(() => {
if (reorder) { if (reorder) {
@@ -85,34 +145,97 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
} }
}, [reorder, reorderTables]); }, [reorder, reorderTables]);
useEffect(() => { const highlightErrorLine = useCallback((error: DBMLError) => {
if (!dialog.open) return; if (!editorRef.current) return;
setErrorMessage(undefined);
setDBMLContent(initialDBML);
}, [dialog.open, initialDBML]);
useEffect(() => { const model = editorRef.current.getModel();
const validateDBML = async () => { if (!model) return;
if (!dbmlContent.trim()) {
const decorations = [
{
range: new monaco.Range(
error.line,
1,
error.line,
model.getLineMaxColumn(error.line)
),
options: {
isWholeLine: true,
className: 'dbml-error-line',
glyphMarginClassName: 'dbml-error-glyph',
hoverMessage: { value: error.message },
overviewRuler: {
color: '#ff0000',
position: monaco.editor.OverviewRulerLane.Right,
darkColor: '#ff0000',
},
},
},
];
decorationsCollection.current?.set(decorations);
}, []);
const clearDecorations = useCallback(() => {
decorationsCollection.current?.clear();
}, []);
const validateDBML = useCallback(
async (content: string) => {
// Clear previous errors
setErrorMessage(undefined); setErrorMessage(undefined);
return; clearDecorations();
}
if (!content.trim()) return;
try { try {
const parser = new Parser(); const parser = new Parser();
parser.parse(dbmlContent, 'dbml'); parser.parse(content, 'dbml');
setErrorMessage(undefined);
} catch (e) { } catch (e) {
const parsedError = parseDBMLError(e);
if (parsedError) {
setErrorMessage( setErrorMessage(
e instanceof Error t('import_dbml_dialog.error.description') +
? e.message ` (1 error found - in line ${parsedError.line})`
: t('import_dbml_dialog.error.description') );
highlightErrorLine(parsedError);
} else {
setErrorMessage(
e instanceof Error ? e.message : JSON.stringify(e)
); );
} }
}; }
},
[clearDecorations, highlightErrorLine, t]
);
validateDBML(); const debouncedValidateRef = useRef<((value: string) => void) | null>(null);
}, [dbmlContent, t]);
// Set up debounced validation
useEffect(() => {
debouncedValidateRef.current = debounce((value: string) => {
validateDBML(value);
}, 500);
return () => {
debouncedValidateRef.current = null;
};
}, [validateDBML]);
// Trigger validation when content changes
useEffect(() => {
if (debouncedValidateRef.current) {
debouncedValidateRef.current(dbmlContent);
}
}, [dbmlContent]);
useEffect(() => {
if (!dialog.open) {
setErrorMessage(undefined);
clearDecorations();
setDBMLContent(initialDBML);
}
}, [dialog.open, initialDBML, clearDecorations]);
const handleImport = useCallback(async () => { const handleImport = useCallback(async () => {
if (!dbmlContent.trim() || errorMessage) return; if (!dbmlContent.trim() || errorMessage) return;
@@ -177,7 +300,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
description: ( description: (
<> <>
<div>{t('import_dbml_dialog.error.description')}</div> <div>{t('import_dbml_dialog.error.description')}</div>
{e instanceof Error ? <div>{e.message}</div> : null} {e instanceof Error ? e.message : JSON.stringify(e)}
</> </>
), ),
}); });
@@ -211,7 +334,11 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
showClose showClose
> >
<DialogHeader> <DialogHeader>
<DialogTitle>{t('import_dbml_dialog.title')}</DialogTitle> <DialogTitle>
{withCreateEmptyDiagram
? t('import_dbml_dialog.example_title')
: t('import_dbml_dialog.title')}
</DialogTitle>
<DialogDescription> <DialogDescription>
{t('import_dbml_dialog.description')} {t('import_dbml_dialog.description')}
</DialogDescription> </DialogDescription>
@@ -222,6 +349,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
value={dbmlContent} value={dbmlContent}
onChange={(value) => setDBMLContent(value || '')} onChange={(value) => setDBMLContent(value || '')}
language="dbml" language="dbml"
onMount={handleEditorDidMount}
theme={ theme={
effectiveTheme === 'dark' effectiveTheme === 'dark'
? 'dbml-dark' ? 'dbml-dark'
@@ -232,6 +360,8 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
minimap: { enabled: false }, minimap: { enabled: false },
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
automaticLayout: true, automaticLayout: true,
glyphMargin: true,
lineNumbers: 'on',
scrollbar: { scrollbar: {
vertical: 'visible', vertical: 'visible',
horizontal: 'visible', horizontal: 'visible',
@@ -246,7 +376,9 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<DialogClose asChild> <DialogClose asChild>
<Button variant="secondary"> <Button variant="secondary">
{t('import_dbml_dialog.cancel')} {withCreateEmptyDiagram
? t('import_dbml_dialog.skip_and_empty')
: t('import_dbml_dialog.cancel')}
</Button> </Button>
</DialogClose> </DialogClose>
{errorMessage ? ( {errorMessage ? (
@@ -266,7 +398,9 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
onClick={handleImport} onClick={handleImport}
disabled={!dbmlContent.trim() || !!errorMessage} disabled={!dbmlContent.trim() || !!errorMessage}
> >
{t('import_dbml_dialog.import')} {withCreateEmptyDiagram
? t('import_dbml_dialog.show_example')
: t('import_dbml_dialog.import')}
</Button> </Button>
</div> </div>
</DialogFooter> </DialogFooter>

View File

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

View File

@@ -109,6 +109,10 @@
animation: rainbow-text-simple-animation 0.5s ease-in forwards; animation: rainbow-text-simple-animation 0.5s ease-in forwards;
} }
.dbml-error-line {
background-color: rgba(255, 0, 0, 0.2) !important;
}
@keyframes rainbow-text-simple-animation-rev { @keyframes rainbow-text-simple-animation-rev {
0% { 0% {
background-size: 650%; background-size: 650%;

View File

@@ -0,0 +1,40 @@
import { useCallback, useState } from 'react';
import { useDialog } from '@/hooks/use-dialog';
import { diagramToJSONOutput } from '@/lib/export-import-utils';
import { waitFor } from '@/lib/utils';
import type { Diagram } from '@/lib/domain/diagram';
export const useExportDiagram = () => {
const [isLoading, setIsLoading] = useState(false);
const { closeExportDiagramDialog } = useDialog();
const downloadOutput = useCallback((name: string, dataUrl: string) => {
const a = document.createElement('a');
a.setAttribute('download', `ChartDB(${name}).json`);
a.setAttribute('href', dataUrl);
a.click();
}, []);
const handleExport = useCallback(
async ({ diagram }: { diagram: Diagram }) => {
setIsLoading(true);
await waitFor(1000);
try {
const json = diagramToJSONOutput(diagram);
const blob = new Blob([json], { type: 'application/json' });
const dataUrl = URL.createObjectURL(blob);
downloadOutput(diagram.name, dataUrl);
setIsLoading(false);
closeExportDiagramDialog();
} finally {
setIsLoading(false);
}
},
[downloadOutput, closeExportDiagramDialog]
);
return {
exportDiagram: handleExport,
isExporting: isLoading,
};
};

View File

@@ -34,13 +34,14 @@ export const ar: LanguageTranslation = {
show_minimap: 'Show Mini Map', show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
share: { backup: {
share: 'مشاركة', backup: 'النسخ الاحتياطي',
export_diagram: 'تصدير المخطط', export_diagram: 'تصدير المخطط',
import_diagram: 'استيراد المخطط', restore_diagram: 'استعادة المخطط',
}, },
help: { help: {
help: 'مساعدة', help: 'مساعدة',
docs_website: 'الوثائق',
visit_website: 'ChartDB قم بزيارة', visit_website: 'ChartDB قم بزيارة',
join_discord: 'Discord انضم إلينا على', join_discord: 'Discord انضم إلينا على',
schedule_a_call: '!تحدث معنا', schedule_a_call: '!تحدث معنا',
@@ -378,12 +379,15 @@ export const ar: LanguageTranslation = {
import_dbml_dialog: { import_dbml_dialog: {
// TODO: Translate // TODO: Translate
title: 'Import DBML', title: 'Import DBML',
example_title: 'Import Example DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -402,6 +406,7 @@ export const ar: LanguageTranslation = {
edit_table: 'تعديل الجدول', edit_table: 'تعديل الجدول',
duplicate_table: 'نسخ الجدول', duplicate_table: 'نسخ الجدول',
delete_table: 'حذف الجدول', delete_table: 'حذف الجدول',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
snap_to_grid_tooltip: '({{key}} مغنظة الشبكة (اضغط مع الاستمرار على', snap_to_grid_tooltip: '({{key}} مغنظة الشبكة (اضغط مع الاستمرار على',

View File

@@ -35,13 +35,14 @@ export const bn: LanguageTranslation = {
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
share: { backup: {
share: 'শেয়ার করুন', backup: 'ব্যাকআপ',
export_diagram: 'ডায়াগ্রাম রপ্তানি করুন', export_diagram: 'ডায়াগ্রাম রপ্তানি করুন',
import_diagram: 'ডায়াগ্রাম আমদানি করুন', restore_diagram: 'ডায়াগ্রাম পুনরুদ্ধার করুন',
}, },
help: { help: {
help: 'সাহায্য', help: 'সাহায্য',
docs_website: 'ডকুমেন্টেশন',
visit_website: 'ChartDB ওয়েবসাইটে যান', visit_website: 'ChartDB ওয়েবসাইটে যান',
join_discord: 'আমাদের Discord-এ যোগ দিন', join_discord: 'আমাদের Discord-এ যোগ দিন',
schedule_a_call: 'আমাদের সাথে কথা বলুন!', schedule_a_call: 'আমাদের সাথে কথা বলুন!',
@@ -381,13 +382,16 @@ export const bn: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -406,6 +410,7 @@ export const bn: LanguageTranslation = {
edit_table: 'টেবিল সম্পাদনা করুন', edit_table: 'টেবিল সম্পাদনা করুন',
duplicate_table: 'টেবিল নকল করুন', duplicate_table: 'টেবিল নকল করুন',
delete_table: 'টেবিল মুছে ফেলুন', delete_table: 'টেবিল মুছে ফেলুন',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
snap_to_grid_tooltip: 'গ্রিডে স্ন্যাপ করুন (অবস্থান {{key}})', snap_to_grid_tooltip: 'গ্রিডে স্ন্যাপ করুন (অবস্থান {{key}})',

View File

@@ -35,13 +35,14 @@ export const de: LanguageTranslation = {
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
// TODO: Translate // TODO: Translate
share: { backup: {
share: 'Share', backup: 'Backup',
export_diagram: 'Export Diagram', export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram', restore_diagram: 'Restore Diagram',
}, },
help: { help: {
help: 'Hilfe', help: 'Hilfe',
docs_website: 'Dokumentation',
visit_website: 'ChartDB Webseite', visit_website: 'ChartDB Webseite',
join_discord: 'Auf Discord beitreten', join_discord: 'Auf Discord beitreten',
schedule_a_call: 'Gespräch vereinbaren', schedule_a_call: 'Gespräch vereinbaren',
@@ -384,13 +385,16 @@ export const de: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -409,6 +413,7 @@ export const de: LanguageTranslation = {
edit_table: 'Tabelle bearbeiten', edit_table: 'Tabelle bearbeiten',
duplicate_table: 'Duplicate Table', // TODO: Translate duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Tabelle löschen', delete_table: 'Tabelle löschen',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
// TODO: Add translations // TODO: Add translations

View File

@@ -33,13 +33,14 @@ export const en = {
show_minimap: 'Show Mini Map', show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
share: { backup: {
share: 'Share', backup: 'Backup',
export_diagram: 'Export Diagram', export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram', restore_diagram: 'Restore Diagram',
}, },
help: { help: {
help: 'Help', help: 'Help',
docs_website: 'Docs',
visit_website: 'Visit ChartDB', visit_website: 'Visit ChartDB',
join_discord: 'Join us on Discord', join_discord: 'Join us on Discord',
schedule_a_call: 'Talk with us!', schedule_a_call: 'Talk with us!',
@@ -376,13 +377,16 @@ export const en = {
}, },
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error importing DBML', title: 'Error importing DBML',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -401,6 +405,7 @@ export const en = {
edit_table: 'Edit Table', edit_table: 'Edit Table',
duplicate_table: 'Duplicate Table', duplicate_table: 'Duplicate Table',
delete_table: 'Delete Table', delete_table: 'Delete Table',
add_relationship: 'Add Relationship',
}, },
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})', snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',

View File

@@ -34,14 +34,14 @@ export const es: LanguageTranslation = {
show_minimap: 'Show Mini Map', show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
// TODO: Translate backup: {
share: { backup: 'Respaldo',
share: 'Share', export_diagram: 'Exportar Diagrama',
export_diagram: 'Export Diagram', restore_diagram: 'Restaurar Diagrama',
import_diagram: 'Import Diagram',
}, },
help: { help: {
help: 'Ayuda', help: 'Ayuda',
docs_website: 'Documentación',
visit_website: 'Visitar ChartDB', visit_website: 'Visitar ChartDB',
join_discord: 'Únete a nosotros en Discord', join_discord: 'Únete a nosotros en Discord',
schedule_a_call: '¡Habla con nosotros!', schedule_a_call: '¡Habla con nosotros!',
@@ -383,13 +383,16 @@ export const es: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -408,6 +411,7 @@ export const es: LanguageTranslation = {
edit_table: 'Editar Tabla', edit_table: 'Editar Tabla',
duplicate_table: 'Duplicate Table', // TODO: Translate duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Eliminar Tabla', delete_table: 'Eliminar Tabla',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
// TODO: Add translations // TODO: Add translations

View File

@@ -30,17 +30,17 @@ export const fr: LanguageTranslation = {
theme: 'Thème', theme: 'Thème',
show_dependencies: 'Afficher les Dépendances', show_dependencies: 'Afficher les Dépendances',
hide_dependencies: 'Masquer les Dépendances', hide_dependencies: 'Masquer les Dépendances',
// TODO: Translate show_minimap: 'Afficher la Mini Carte',
show_minimap: 'Show Mini Map', hide_minimap: 'Masquer la Mini Carte',
hide_minimap: 'Hide Mini Map',
}, },
share: { backup: {
share: 'Partage', backup: 'Sauvegarde',
export_diagram: 'Exporter le diagramme', export_diagram: 'Exporter le diagramme',
import_diagram: 'Importer un diagramme', restore_diagram: 'Restaurer le diagramme',
}, },
help: { help: {
help: 'Aide', help: 'Aide',
docs_website: 'Documentation',
visit_website: 'Visitez ChartDB', visit_website: 'Visitez ChartDB',
join_discord: 'Rejoignez-nous sur Discord', join_discord: 'Rejoignez-nous sur Discord',
schedule_a_call: 'Parlez avec nous !', schedule_a_call: 'Parlez avec nous !',
@@ -101,9 +101,8 @@ export const fr: LanguageTranslation = {
clear: 'Effacer', clear: 'Effacer',
show_more: 'Afficher Plus', show_more: 'Afficher Plus',
show_less: 'Afficher Moins', show_less: 'Afficher Moins',
// TODO: Translate copy_to_clipboard: 'Copier dans le presse-papiers',
copy_to_clipboard: 'Copy to Clipboard', copied: 'Copié !',
copied: 'Copied!',
side_panel: { side_panel: {
schema: 'Schéma:', schema: 'Schéma:',
@@ -116,12 +115,11 @@ export const fr: LanguageTranslation = {
add_table: 'Ajouter une Table', add_table: 'Ajouter une Table',
filter: 'Filtrer', filter: 'Filtrer',
collapse: 'Réduire Tout', collapse: 'Réduire Tout',
// TODO: Translate clear: 'Effacer le Filtre',
clear: 'Clear Filter', no_results:
no_results: 'No tables found matching your filter.', 'Aucune table trouvée correspondant à votre filtre.',
// TODO: Translate show_list: 'Afficher la Liste des Tableaux',
show_list: 'Show Table List', show_dbml: "Afficher l'éditeur DBML",
show_dbml: 'Show DBML Editor',
table: { table: {
fields: 'Champs', fields: 'Champs',
@@ -153,7 +151,7 @@ export const fr: LanguageTranslation = {
title: 'Actions de la Table', title: 'Actions de la Table',
add_field: 'Ajouter un Champ', add_field: 'Ajouter un Champ',
add_index: 'Ajouter un Index', add_index: 'Ajouter un Index',
duplicate_table: 'Duplicate Table', // TODO: Translate duplicate_table: 'Tableau dupliqué',
delete_table: 'Supprimer la Table', delete_table: 'Supprimer la Table',
change_schema: 'Changer le Schéma', change_schema: 'Changer le Schéma',
}, },
@@ -236,14 +234,12 @@ export const fr: LanguageTranslation = {
step_2: 'Si vous utilisez "Résultats en Grille", changez le nombre maximum de caractères récupérés pour les données non-XML (définir à 9999999).', step_2: 'Si vous utilisez "Résultats en Grille", changez le nombre maximum de caractères récupérés pour les données non-XML (définir à 9999999).',
}, },
instructions_link: "Besoin d'aide ? Regardez comment", instructions_link: "Besoin d'aide ? Regardez comment",
// TODO: Translate check_script_result: 'Vérifier le résultat du Script',
check_script_result: 'Check Script Result',
}, },
cancel: 'Annuler', cancel: 'Annuler',
back: 'Retour', back: 'Retour',
// TODO: Translate import_from_file: "Importer à partir d'un fichier",
import_from_file: 'Import from File',
empty_diagram: 'Diagramme vide', empty_diagram: 'Diagramme vide',
continue: 'Continuer', continue: 'Continuer',
import: 'Importer', import: 'Importer',
@@ -358,40 +354,42 @@ export const fr: LanguageTranslation = {
cancel: 'Annuler', cancel: 'Annuler',
}, },
}, },
// TODO: Translate
export_diagram_dialog: { export_diagram_dialog: {
title: 'Export Diagram', title: 'Exporter le Diagramme',
description: 'Choose the format for export:', description: "Sélectionner le format d'exportation :",
format_json: 'JSON', format_json: 'JSON',
cancel: 'Cancel', cancel: 'Annuler',
export: 'Export', export: 'Exporter',
error: { error: {
title: 'Error exporting diagram', title: "Erreur lors de l'exportation du diagramme",
description: description:
'Something went wrong. Need help? chartdb.io@gmail.com', "Une erreur s'est produite. Besoin d'aide ? chartdb.io@gmail.com",
}, },
}, },
// TODO: Translate
import_diagram_dialog: { import_diagram_dialog: {
title: 'Import Diagram', title: 'Importer un diagramme',
description: 'Paste the diagram JSON below:', description: 'Coller le diagramme au format JSON ci-dessous :',
cancel: 'Cancel', cancel: 'Annuler',
import: 'Import', import: 'Exporter',
error: { error: {
title: 'Error importing diagram', title: "Erreur lors de l'exportation du diagramme",
description: description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com', "Le diagramme JSON n'est pas valide. Veuillez vérifier le JSON et réessayer. Besoin d'aide ? chartdb.io@gmail.com",
}, },
}, },
// TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: "Exemple d'importation DBML",
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description:
import: 'Import', 'Importer un schéma de base de données à partir du format DBML.',
cancel: 'Cancel', import: 'Importer',
cancel: 'Annuler',
skip_and_empty: 'Passer et vider',
show_example: 'Afficher un exemple',
error: { error: {
title: 'Error', title: 'Erreur',
description: 'Failed to import DBML. Please check the syntax.', description:
"Erreur d'analyse du DBML. Veuillez vérifier la syntaxe.",
}, },
}, },
relationship_type: { relationship_type: {
@@ -408,12 +406,13 @@ export const fr: LanguageTranslation = {
table_node_context_menu: { table_node_context_menu: {
edit_table: 'Éditer la Table', edit_table: 'Éditer la Table',
duplicate_table: 'Duplicate Table', // TODO: Translate duplicate_table: 'Tableau Dupliqué',
delete_table: 'Supprimer la Table', delete_table: 'Supprimer la Table',
add_relationship: 'Ajouter une Relation',
}, },
// TODO: Add translations snap_to_grid_tooltip:
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})', 'Aligner sur la grille (maintenir la touche {{key}})',
tool_tips: { tool_tips: {
double_click_to_edit: 'Double-cliquez pour modifier', double_click_to_edit: 'Double-cliquez pour modifier',

View File

@@ -35,13 +35,14 @@ export const gu: LanguageTranslation = {
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
share: { backup: {
share: 'શેર કરો', backup: 'બેકઅપ',
export_diagram: 'ડાયાગ્રામ નિકાસ કરો', export_diagram: 'ડાયાગ્રામ નિકાસ કરો',
import_diagram: 'ડાયાગ્રામ આયાત કરો', restore_diagram: 'ડાયાગ્રામ પુનઃસ્થાપિત કરો',
}, },
help: { help: {
help: 'મદદ', help: 'મદદ',
docs_website: 'દસ્તાવેજીકરણ',
visit_website: 'ChartDB વેબસાઇટ પર જાઓ', visit_website: 'ChartDB વેબસાઇટ પર જાઓ',
join_discord: 'અમારા Discordમાં જોડાઓ', join_discord: 'અમારા Discordમાં જોડાઓ',
schedule_a_call: 'અમારી સાથે વાત કરો!', schedule_a_call: 'અમારી સાથે વાત કરો!',
@@ -381,13 +382,16 @@ export const gu: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -406,6 +410,7 @@ export const gu: LanguageTranslation = {
edit_table: 'ટેબલ સંપાદિત કરો', edit_table: 'ટેબલ સંપાદિત કરો',
duplicate_table: 'ટેબલ નકલ કરો', duplicate_table: 'ટેબલ નકલ કરો',
delete_table: 'ટેબલ કાઢી નાખો', delete_table: 'ટેબલ કાઢી નાખો',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
snap_to_grid_tooltip: 'ગ્રિડ પર સ્નેપ કરો (જમાવટ {{key}})', snap_to_grid_tooltip: 'ગ્રિડ પર સ્નેપ કરો (જમાવટ {{key}})',

View File

@@ -34,14 +34,14 @@ export const hi: LanguageTranslation = {
show_minimap: 'Show Mini Map', show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
// TODO: Translate backup: {
share: { backup: 'बैकअप',
share: 'Share', export_diagram: 'आरेख निर्यात करें',
export_diagram: 'Export Diagram', restore_diagram: 'आरेख पुनर्स्थापित करें',
import_diagram: 'Import Diagram',
}, },
help: { help: {
help: 'मदद', help: 'मदद',
docs_website: 'દસ્તાવેજીકરણ',
visit_website: 'ChartDB वेबसाइट पर जाएँ', visit_website: 'ChartDB वेबसाइट पर जाएँ',
join_discord: 'हमसे Discord पर जुड़ें', join_discord: 'हमसे Discord पर जुड़ें',
schedule_a_call: 'हमसे बात करें!', schedule_a_call: 'हमसे बात करें!',
@@ -385,13 +385,16 @@ export const hi: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -410,6 +413,7 @@ export const hi: LanguageTranslation = {
edit_table: 'तालिका संपादित करें', edit_table: 'तालिका संपादित करें',
duplicate_table: 'Duplicate Table', // TODO: Translate duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'तालिका हटाएँ', delete_table: 'तालिका हटाएँ',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
// TODO: Add translations // TODO: Add translations

View File

@@ -34,13 +34,14 @@ export const id_ID: LanguageTranslation = {
show_minimap: 'Show Mini Map', show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
share: { backup: {
share: 'Bagikan', backup: 'Cadangan',
export_diagram: 'Ekspor Diagram', export_diagram: 'Ekspor Diagram',
import_diagram: 'Impor Diagram', restore_diagram: 'Pulihkan Diagram',
}, },
help: { help: {
help: 'Bantuan', help: 'Bantuan',
docs_website: 'દસ્તાવેજીકરણ',
visit_website: 'Kunjungi ChartDB', visit_website: 'Kunjungi ChartDB',
join_discord: 'Bergabunglah di Discord kami', join_discord: 'Bergabunglah di Discord kami',
schedule_a_call: 'Berbicara dengan kami!', schedule_a_call: 'Berbicara dengan kami!',
@@ -379,13 +380,16 @@ export const id_ID: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
@@ -405,6 +409,7 @@ export const id_ID: LanguageTranslation = {
edit_table: 'Ubah Tabel', edit_table: 'Ubah Tabel',
delete_table: 'Hapus Tabel', delete_table: 'Hapus Tabel',
duplicate_table: 'Duplikat Tabel', duplicate_table: 'Duplikat Tabel',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
snap_to_grid_tooltip: 'Snap ke Kisi (Tahan {{key}})', snap_to_grid_tooltip: 'Snap ke Kisi (Tahan {{key}})',

View File

@@ -36,13 +36,14 @@ export const ja: LanguageTranslation = {
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
// TODO: Translate // TODO: Translate
share: { backup: {
share: 'Share', backup: 'Backup',
export_diagram: 'Export Diagram', export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram', restore_diagram: 'Restore Diagram',
}, },
help: { help: {
help: 'ヘルプ', help: 'ヘルプ',
docs_website: 'ドキュメント',
visit_website: 'ChartDBにアクセス', visit_website: 'ChartDBにアクセス',
join_discord: 'Discordに参加', join_discord: 'Discordに参加',
schedule_a_call: '話しかけてください!', schedule_a_call: '話しかけてください!',
@@ -388,13 +389,16 @@ export const ja: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -413,6 +417,7 @@ export const ja: LanguageTranslation = {
edit_table: 'テーブルを編集', edit_table: 'テーブルを編集',
duplicate_table: 'Duplicate Table', // TODO: Translate duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'テーブルを削除', delete_table: 'テーブルを削除',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
// TODO: Add translations // TODO: Add translations

View File

@@ -34,13 +34,14 @@ export const ko_KR: LanguageTranslation = {
show_minimap: 'Show Mini Map', show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
share: { backup: {
share: '공유', backup: '백업',
export_diagram: '다이어그램 내보내기', export_diagram: '다이어그램 내보내기',
import_diagram: '다이어그램 가져오기', restore_diagram: '다이어그램 복구',
}, },
help: { help: {
help: '도움말', help: '도움말',
docs_website: '선적 서류 비치',
visit_website: 'ChartDB 사이트 방문', visit_website: 'ChartDB 사이트 방문',
join_discord: 'Discord 가입', join_discord: 'Discord 가입',
schedule_a_call: 'Talk with us!', schedule_a_call: 'Talk with us!',
@@ -377,13 +378,16 @@ export const ko_KR: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -402,6 +406,7 @@ export const ko_KR: LanguageTranslation = {
edit_table: '테이블 수정', edit_table: '테이블 수정',
duplicate_table: '테이블 복제', duplicate_table: '테이블 복제',
delete_table: '테이블 삭제', delete_table: '테이블 삭제',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
snap_to_grid_tooltip: '그리드에 맞추기 ({{key}}를 누른채 유지)', snap_to_grid_tooltip: '그리드에 맞추기 ({{key}}를 누른채 유지)',

View File

@@ -34,14 +34,15 @@ export const mr: LanguageTranslation = {
show_minimap: 'Show Mini Map', show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
share: { backup: {
// TODO: Add translations // TODO: Add translations
share: 'Share', backup: 'Backup',
export_diagram: 'Export Diagram', export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram', restore_diagram: 'Restore Diagram',
}, },
help: { help: {
help: 'मदत', help: 'मदत',
docs_website: 'दस्तऐवजीकरण',
visit_website: 'ChartDB ला भेट द्या', visit_website: 'ChartDB ला भेट द्या',
join_discord: 'आमच्या डिस्कॉर्डमध्ये सामील व्हा', join_discord: 'आमच्या डिस्कॉर्डमध्ये सामील व्हा',
schedule_a_call: 'आमच्याशी बोला!', schedule_a_call: 'आमच्याशी बोला!',
@@ -389,13 +390,16 @@ export const mr: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
@@ -414,8 +418,8 @@ export const mr: LanguageTranslation = {
table_node_context_menu: { table_node_context_menu: {
edit_table: 'टेबल संपादित करा', edit_table: 'टेबल संपादित करा',
delete_table: 'टेबल हटवा', delete_table: 'टेबल हटवा',
// TODO: Add translations duplicate_table: 'Duplicate Table', // TODO: Translate
duplicate_table: 'Duplicate Table', add_relationship: 'Add Relationship', // TODO: Translate
}, },
// TODO: Add translations // TODO: Add translations

View File

@@ -34,13 +34,15 @@ export const ne: LanguageTranslation = {
show_minimap: 'Show Mini Map', show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
share: { // TODO: Translate
share: 'शेयर गर्नुहोस्', backup: {
export_diagram: 'डायाग्राम निर्यात गर्नुहोस्', backup: 'Backup',
import_diagram: 'डायाग्राम आयात गर्नुहोस्', export_diagram: 'Export Diagram',
restore_diagram: 'Restore Diagram',
}, },
help: { help: {
help: 'मद्दत', help: 'मद्दत',
docs_website: 'कागजात',
visit_website: 'वेबसाइटमा जानुहोस्', visit_website: 'वेबसाइटमा जानुहोस्',
join_discord: 'डिस्कोर्डमा सामिल हुनुहोस्', join_discord: 'डिस्कोर्डमा सामिल हुनुहोस्',
schedule_a_call: 'कल अनुसूची गर्नुहोस्', schedule_a_call: 'कल अनुसूची गर्नुहोस्',
@@ -382,13 +384,16 @@ export const ne: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
@@ -408,6 +413,7 @@ export const ne: LanguageTranslation = {
edit_table: 'तालिका सम्पादन गर्नुहोस्', edit_table: 'तालिका सम्पादन गर्नुहोस्',
duplicate_table: 'तालिका नक्कली गर्नुहोस्', duplicate_table: 'तालिका नक्कली गर्नुहोस्',
delete_table: 'तालिका हटाउनुहोस्', delete_table: 'तालिका हटाउनुहोस्',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
snap_to_grid_tooltip: 'ग्रिडमा स्न्याप गर्नुहोस् ({{key}} थिच्नुहोस)', snap_to_grid_tooltip: 'ग्रिडमा स्न्याप गर्नुहोस् ({{key}} थिच्नुहोस)',

View File

@@ -35,13 +35,14 @@ export const pt_BR: LanguageTranslation = {
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
// TODO: Translate // TODO: Translate
share: { backup: {
share: 'Share', backup: 'Backup',
export_diagram: 'Export Diagram', export_diagram: 'Exportar Diagrama',
import_diagram: 'Import Diagram', restore_diagram: 'Restaurar Diagrama',
}, },
help: { help: {
help: 'Ajuda', help: 'Ajuda',
docs_website: 'Documentação',
visit_website: 'Visitar ChartDB', visit_website: 'Visitar ChartDB',
join_discord: 'Junte-se a nós no Discord', join_discord: 'Junte-se a nós no Discord',
schedule_a_call: 'Fale Conosco!', schedule_a_call: 'Fale Conosco!',
@@ -382,13 +383,16 @@ export const pt_BR: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -407,6 +411,7 @@ export const pt_BR: LanguageTranslation = {
edit_table: 'Editar Tabela', edit_table: 'Editar Tabela',
duplicate_table: 'Duplicate Table', // TODO: Translate duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Excluir Tabela', delete_table: 'Excluir Tabela',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
// TODO: Add translations // TODO: Add translations

View File

@@ -34,13 +34,15 @@ export const ru: LanguageTranslation = {
show_minimap: 'Show Mini Map', show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
share: { // TODO: Translate
share: 'Поделиться', backup: {
export_diagram: 'Экспорт кода диаграммы', backup: 'Backup',
import_diagram: 'Импорт кода диаграммы', export_diagram: 'Export Diagram',
restore_diagram: 'Restore Diagram',
}, },
help: { help: {
help: 'Помощь', help: 'Помощь',
docs_website: 'Документация',
visit_website: 'Перейти на сайт ChartDB', visit_website: 'Перейти на сайт ChartDB',
join_discord: 'Присоединиться к сообществу в Discord', join_discord: 'Присоединиться к сообществу в Discord',
schedule_a_call: 'Поговорите с нами!', schedule_a_call: 'Поговорите с нами!',
@@ -378,13 +380,16 @@ export const ru: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -403,6 +408,7 @@ export const ru: LanguageTranslation = {
edit_table: 'Изменить таблицу', edit_table: 'Изменить таблицу',
duplicate_table: 'Duplicate Table', // TODO: Translate duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: 'Удалить таблицу', delete_table: 'Удалить таблицу',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
copy_to_clipboard: 'Скопировать в буфер обмена', copy_to_clipboard: 'Скопировать в буфер обмена',

View File

@@ -35,13 +35,14 @@ export const te: LanguageTranslation = {
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
// TODO: Translate // TODO: Translate
share: { backup: {
share: 'Share', backup: 'Backup',
export_diagram: 'Export Diagram', export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram', restore_diagram: 'Restore Diagram',
}, },
help: { help: {
help: 'సహాయం', help: 'సహాయం',
docs_website: 'డాక్యుమెంటేషన్',
visit_website: 'ChartDB సందర్శించండి', visit_website: 'ChartDB సందర్శించండి',
join_discord: 'డిస్కార్డ్‌లో మా నుంచి చేరండి', join_discord: 'డిస్కార్డ్‌లో మా నుంచి చేరండి',
schedule_a_call: 'మాతో మాట్లాడండి!', schedule_a_call: 'మాతో మాట్లాడండి!',
@@ -385,13 +386,16 @@ export const te: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
@@ -409,9 +413,9 @@ export const te: LanguageTranslation = {
table_node_context_menu: { table_node_context_menu: {
edit_table: 'పట్టికను సవరించు', edit_table: 'పట్టికను సవరించు',
// TODO: Translate duplicate_table: 'Duplicate Table', // TODO: Translate
duplicate_table: 'Duplicate Table',
delete_table: 'పట్టికను తొలగించు', delete_table: 'పట్టికను తొలగించు',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
// TODO: Translate // TODO: Translate

View File

@@ -35,13 +35,14 @@ export const tr: LanguageTranslation = {
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
// TODO: Translate // TODO: Translate
share: { backup: {
share: 'Share', backup: 'Backup',
export_diagram: 'Export Diagram', export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram', restore_diagram: 'Restore Diagram',
}, },
help: { help: {
help: 'Yardım', help: 'Yardım',
docs_website: 'Belgeleme',
visit_website: "ChartDB'yi Ziyaret Et", visit_website: "ChartDB'yi Ziyaret Et",
join_discord: "Discord'a Katıl", join_discord: "Discord'a Katıl",
schedule_a_call: 'Bize Ulaş!', schedule_a_call: 'Bize Ulaş!',
@@ -372,13 +373,16 @@ export const tr: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -394,8 +398,8 @@ export const tr: LanguageTranslation = {
table_node_context_menu: { table_node_context_menu: {
edit_table: 'Tabloyu Düzenle', edit_table: 'Tabloyu Düzenle',
delete_table: 'Tabloyu Sil', delete_table: 'Tabloyu Sil',
// TODO: Translate duplicate_table: 'Duplicate Table', // TODO: Translate
duplicate_table: 'Duplicate Table', add_relationship: 'Add Relationship', // TODO: Translate
}, },
// TODO: Translate // TODO: Translate

View File

@@ -26,20 +26,21 @@ export const uk: LanguageTranslation = {
hide_sidebar: 'Приховати бічну панель', hide_sidebar: 'Приховати бічну панель',
hide_cardinality: 'Приховати потужність', hide_cardinality: 'Приховати потужність',
show_cardinality: 'Показати кардинальність', show_cardinality: 'Показати кардинальність',
zoom_on_scroll: 'Збільшити прокручування', zoom_on_scroll: 'Масштабувати прокручуванням',
theme: 'Тема', theme: 'Тема',
show_dependencies: 'Показати залежності', show_dependencies: 'Показати залежності',
hide_dependencies: 'Приховати залежності', hide_dependencies: 'Приховати залежності',
show_minimap: 'Показати мінімапу', show_minimap: 'Показати мінімапу',
hide_minimap: 'Приховати мінімапу', hide_minimap: 'Приховати мінімапу',
}, },
share: { backup: {
share: 'Поширити', backup: 'Резервне копіювання',
export_diagram: 'Експорт діаграми', export_diagram: 'Експорт діаграми',
import_diagram: 'Імпорт діаграми', restore_diagram: 'Відновити діаграму',
}, },
help: { help: {
help: 'Довідка', help: 'Довідка',
docs_website: 'Документація',
visit_website: 'Сайт ChartDB', visit_website: 'Сайт ChartDB',
join_discord: 'Приєднуйтесь до нас в Діскорд', join_discord: 'Приєднуйтесь до нас в Діскорд',
schedule_a_call: 'Забронювати зустріч!', schedule_a_call: 'Забронювати зустріч!',
@@ -377,13 +378,16 @@ export const uk: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -402,6 +406,7 @@ export const uk: LanguageTranslation = {
edit_table: 'Редагувати таблицю', edit_table: 'Редагувати таблицю',
duplicate_table: 'Дублювати таблицю', duplicate_table: 'Дублювати таблицю',
delete_table: 'Видалити таблицю', delete_table: 'Видалити таблицю',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
snap_to_grid_tooltip: 'Вирівнювати за сіткою (Отримуйте {{key}})', snap_to_grid_tooltip: 'Вирівнювати за сіткою (Отримуйте {{key}})',

View File

@@ -34,13 +34,14 @@ export const vi: LanguageTranslation = {
show_minimap: 'Show Mini Map', show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
share: { backup: {
share: 'Chia sẻ', backup: 'Hỗ trợ',
export_diagram: 'Xuất sơ đồ', export_diagram: 'Xuất sơ đồ',
import_diagram: 'Nhập sơ đồ', restore_diagram: 'Khôi phục sơ đồ',
}, },
help: { help: {
help: 'Trợ giúp', help: 'Trợ giúp',
docs_website: 'Tài liệu',
visit_website: 'Truy cập ChartDB', visit_website: 'Truy cập ChartDB',
join_discord: 'Tham gia Discord', join_discord: 'Tham gia Discord',
schedule_a_call: 'Trò chuyện cùng chúng tôi!', schedule_a_call: 'Trò chuyện cùng chúng tôi!',
@@ -378,13 +379,16 @@ export const vi: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -403,6 +407,7 @@ export const vi: LanguageTranslation = {
edit_table: 'Sửa bảng', edit_table: 'Sửa bảng',
duplicate_table: 'Nhân đôi bảng', duplicate_table: 'Nhân đôi bảng',
delete_table: 'Xóa bảng', delete_table: 'Xóa bảng',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
snap_to_grid_tooltip: 'Căn lưới (Giữ phím {{key}})', snap_to_grid_tooltip: 'Căn lưới (Giữ phím {{key}})',

View File

@@ -34,13 +34,14 @@ export const zh_CN: LanguageTranslation = {
show_minimap: 'Show Mini Map', show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
share: { backup: {
share: '分享', backup: '备份',
export_diagram: '导出关系图', export_diagram: '导出关系图',
import_diagram: '导入关系图', restore_diagram: '还原图表',
}, },
help: { help: {
help: '帮助', help: '帮助',
docs_website: '文档',
visit_website: '访问 ChartDB', visit_website: '访问 ChartDB',
join_discord: '在 Discord 上加入我们', join_discord: '在 Discord 上加入我们',
schedule_a_call: '和我们交流!', schedule_a_call: '和我们交流!',
@@ -374,13 +375,16 @@ export const zh_CN: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -399,6 +403,7 @@ export const zh_CN: LanguageTranslation = {
edit_table: '编辑表', edit_table: '编辑表',
duplicate_table: 'Duplicate Table', // TODO: Translate duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: '删除表', delete_table: '删除表',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
snap_to_grid_tooltip: '对齐到网格(按住 {{key}}', snap_to_grid_tooltip: '对齐到网格(按住 {{key}}',

View File

@@ -34,13 +34,14 @@ export const zh_TW: LanguageTranslation = {
show_minimap: 'Show Mini Map', show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map', hide_minimap: 'Hide Mini Map',
}, },
share: { backup: {
share: '分享', backup: '備份',
export_diagram: '匯出圖表', export_diagram: '匯出圖表',
import_diagram: '匯入圖表', restore_diagram: '恢復圖表',
}, },
help: { help: {
help: '幫助', help: '幫助',
docs_website: '文件',
visit_website: '訪問 ChartDB 網站', visit_website: '訪問 ChartDB 網站',
join_discord: '加入 Discord', join_discord: '加入 Discord',
schedule_a_call: '與我們聯絡!', schedule_a_call: '與我們聯絡!',
@@ -373,13 +374,16 @@ export const zh_TW: LanguageTranslation = {
}, },
// TODO: Translate // TODO: Translate
import_dbml_dialog: { import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML', title: 'Import DBML',
description: 'Import a database schema from DBML format.', description: 'Import a database schema from DBML format.',
import: 'Import', import: 'Import',
cancel: 'Cancel', cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
error: { error: {
title: 'Error', title: 'Error',
description: 'Failed to import DBML. Please check the syntax.', description: 'Failed to parse DBML. Please check the syntax.',
}, },
}, },
relationship_type: { relationship_type: {
@@ -398,6 +402,7 @@ export const zh_TW: LanguageTranslation = {
edit_table: '編輯表格', edit_table: '編輯表格',
duplicate_table: 'Duplicate Table', // TODO: Translate duplicate_table: 'Duplicate Table', // TODO: Translate
delete_table: '刪除表格', delete_table: '刪除表格',
add_relationship: 'Add Relationship', // TODO: Translate
}, },
snap_to_grid_tooltip: '對齊網格(按住 {{key}}', snap_to_grid_tooltip: '對齊網格(按住 {{key}}',

View File

@@ -12,6 +12,9 @@ export const sqliteDataTypes: readonly DataType[] = [
// Blob Type // Blob Type
{ name: 'blob', id: 'blob' }, { name: 'blob', id: 'blob' },
// Blob Type
{ name: 'json', id: 'json' },
// Date/Time Types (SQLite uses TEXT, REAL, or INTEGER types for dates and times) // Date/Time Types (SQLite uses TEXT, REAL, or INTEGER types for dates and times)
{ name: 'date', id: 'date' }, { name: 'date', id: 'date' },
{ name: 'datetime', id: 'datetime' }, { name: 'datetime', id: 'datetime' },

View File

@@ -0,0 +1,82 @@
import type { Diagram } from '@/lib/domain/diagram';
import type { DBTable } from '@/lib/domain/db-table';
export function isFunction(value: string): boolean {
// Common SQL functions
const functionPatterns = [
/^CURRENT_TIMESTAMP$/i,
/^NOW\(\)$/i,
/^GETDATE\(\)$/i,
/^CURRENT_DATE$/i,
/^CURRENT_TIME$/i,
/^UUID\(\)$/i,
/^NEWID\(\)$/i,
/^NEXT VALUE FOR/i,
/^IDENTITY\s*\(\d+,\s*\d+\)$/i,
];
return functionPatterns.some((pattern) => pattern.test(value.trim()));
}
export function isKeyword(value: string): boolean {
// Common SQL keywords that can be used as default values
const keywords = [
'NULL',
'TRUE',
'FALSE',
'CURRENT_TIMESTAMP',
'CURRENT_DATE',
'CURRENT_TIME',
'CURRENT_USER',
'SESSION_USER',
'SYSTEM_USER',
];
return keywords.includes(value.trim().toUpperCase());
}
export function strHasQuotes(value: string): boolean {
return /^['"].*['"]$/.test(value.trim());
}
export function exportFieldComment(comment: string): string {
if (!comment) {
return '';
}
return comment
.split('\n')
.map((commentLine) => ` -- ${commentLine}\n`)
.join('');
}
export function getInlineFK(table: DBTable, diagram: Diagram): string {
if (!diagram.relationships) {
return '';
}
const fks = diagram.relationships
.filter((r) => r.sourceTableId === table.id)
.map((r) => {
const targetTable = diagram.tables?.find(
(t) => t.id === r.targetTableId
);
const sourceField = table.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable?.fields.find(
(f) => f.id === r.targetFieldId
);
if (!targetTable || !sourceField || !targetField) {
return '';
}
const targetTableName = targetTable.schema
? `"${targetTable.schema}"."${targetTable.name}"`
: `"${targetTable.name}"`;
return ` FOREIGN KEY ("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}")`;
})
.filter(Boolean);
return fks.join(',\n');
}

View File

@@ -0,0 +1,247 @@
import {
exportFieldComment,
isFunction,
isKeyword,
strHasQuotes,
} from './common';
import type { Diagram } from '@/lib/domain/diagram';
import type { DBTable } from '@/lib/domain/db-table';
import type { DBField } from '@/lib/domain/db-field';
import type { DBRelationship } from '@/lib/domain/db-relationship';
function parseMSSQLDefault(field: DBField): string {
if (!field.default) {
return '';
}
let defaultValue = field.default.trim();
// Remove type casting for SQL Server
defaultValue = defaultValue.split('::')[0];
// Handle nextval sequences for SQL Server
if (defaultValue.includes('nextval')) {
return 'IDENTITY(1,1)';
}
// Special handling for SQL Server DEFAULT values
if (defaultValue.match(/^\(\(.*\)\)$/)) {
// Handle ((0)), ((0.00)) style defaults
return defaultValue.replace(/^\(\(|\)\)$/g, '');
} else if (defaultValue.match(/^\(N'.*'\)$/)) {
// Handle (N'value') style defaults
const innerValue = defaultValue.replace(/^\(N'|'\)$/g, '');
return `N'${innerValue}'`;
} else if (defaultValue.match(/^\(NULL\)$/i)) {
// Handle (NULL) defaults
return 'NULL';
} else if (defaultValue.match(/^\(getdate\(\)\)$/i)) {
// Handle (getdate()) defaults
return 'getdate()';
} else if (defaultValue.match(/^\('?\*'?\)$/i) || defaultValue === '*') {
// Handle ('*') or (*) or * defaults - common for "all" values
return "N'*'";
} else if (defaultValue.match(/^\((['"])(.*)\1\)$/)) {
// Handle ('value') or ("value") style defaults
const matches = defaultValue.match(/^\((['"])(.*)\1\)$/);
return matches ? `N'${matches[2]}'` : defaultValue;
}
// Handle special characters that could be interpreted as operators
const sqlServerSpecialChars = /[*+\-/%&|^!=<>~]/;
if (sqlServerSpecialChars.test(defaultValue)) {
// If the value contains special characters and isn't already properly quoted
if (
!strHasQuotes(defaultValue) &&
!isFunction(defaultValue) &&
!isKeyword(defaultValue)
) {
return `N'${defaultValue.replace(/'/g, "''")}'`;
}
}
if (
strHasQuotes(defaultValue) ||
isFunction(defaultValue) ||
isKeyword(defaultValue) ||
/^-?\d+(\.\d+)?$/.test(defaultValue)
) {
return defaultValue;
}
return `'${defaultValue}'`;
}
export function exportMSSQL(diagram: Diagram): string {
if (!diagram.tables || !diagram.relationships) {
return '';
}
const tables = diagram.tables;
const relationships = diagram.relationships;
// Create CREATE SCHEMA statements for all schemas
let sqlScript = '';
const schemas = new Set<string>();
tables.forEach((table) => {
if (table.schema) {
schemas.add(table.schema);
}
});
// Add schema creation statements
schemas.forEach((schema) => {
sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n\n`;
});
// Generate table creation SQL
sqlScript += tables
.map((table: DBTable) => {
// Skip views
if (table.isView) {
return '';
}
const tableName = table.schema
? `[${table.schema}].[${table.name}]`
: `[${table.name}]`;
return `${
table.comments ? `/**\n${table.comments}\n*/\n` : ''
}CREATE TABLE ${tableName} (\n${table.fields
.map((field: DBField) => {
const fieldName = `[${field.name}]`;
const typeName = field.type.name;
// Handle SQL Server specific type formatting
let typeWithSize = typeName;
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
typeName.toLowerCase() === 'nvarchar' ||
typeName.toLowerCase() === 'char' ||
typeName.toLowerCase() === 'nchar'
) {
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
}
} else if (field.precision && field.scale) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
}
} else if (field.precision) {
if (
typeName.toLowerCase() === 'decimal' ||
typeName.toLowerCase() === 'numeric'
) {
typeWithSize = `${typeName}(${field.precision})`;
}
}
const notNull = field.nullable ? '' : ' NOT NULL';
// Check if identity column
const identity = field.default
?.toLowerCase()
.includes('identity')
? ' IDENTITY(1,1)'
: '';
const unique =
!field.primaryKey && field.unique ? ' UNIQUE' : '';
// Handle default value using SQL Server specific parser
const defaultValue =
field.default &&
!field.default.toLowerCase().includes('identity')
? ` DEFAULT ${parseMSSQLDefault(field)}`
: '';
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
})
.join(',\n')}${
table.fields.filter((f) => f.primaryKey).length > 0
? `,\n PRIMARY KEY (${table.fields
.filter((f) => f.primaryKey)
.map((f) => `[${f.name}]`)
.join(', ')})`
: ''
}\n);\n\n${table.indexes
.map((index) => {
const indexName = table.schema
? `[${table.schema}_${index.name}]`
: `[${index.name}]`;
const indexFields = index.fieldIds
.map((fieldId) => {
const field = table.fields.find(
(f) => f.id === fieldId
);
return field ? `[${field.name}]` : '';
})
.filter(Boolean);
// SQL Server has a limit of 32 columns in an index
if (indexFields.length > 32) {
const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`;
console.warn(
`Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.`
);
indexFields.length = 32;
return indexFields.length > 0
? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
: '';
}
return indexFields.length > 0
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
: '';
})
.join('')}`;
})
.filter(Boolean) // Remove empty strings (views)
.join('\n');
// Generate foreign keys
sqlScript += `\n${relationships
.map((r: DBRelationship) => {
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
const targetTable = tables.find((t) => t.id === r.targetTableId);
if (
!sourceTable ||
!targetTable ||
sourceTable.isView ||
targetTable.isView
) {
return '';
}
const sourceField = sourceTable.fields.find(
(f) => f.id === r.sourceFieldId
);
const targetField = targetTable.fields.find(
(f) => f.id === r.targetFieldId
);
if (!sourceField || !targetField) {
return '';
}
const sourceTableName = sourceTable.schema
? `[${sourceTable.schema}].[${sourceTable.name}]`
: `[${sourceTable.name}]`;
const targetTableName = targetTable.schema
? `[${targetTable.schema}].[${targetTable.name}]`
: `[${targetTable.name}]`;
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT [${r.name}] FOREIGN KEY([${sourceField.name}]) REFERENCES ${targetTableName}([${targetField.name}]);\n`;
})
.filter(Boolean) // Remove empty strings
.join('\n')}`;
return sqlScript;
}

View File

@@ -1,9 +1,10 @@
import type { Diagram } from '../../domain/diagram'; import type { Diagram } from '../../domain/diagram';
import { OPENAI_API_KEY } from '@/lib/env'; import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env';
import type { DatabaseType } from '@/lib/domain/database-type'; import { DatabaseType } from '@/lib/domain/database-type';
import type { DBTable } from '@/lib/domain/db-table'; import type { DBTable } from '@/lib/domain/db-table';
import type { DataType } from '../data-types/data-types'; import type { DataType } from '../data-types/data-types';
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache'; import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
import { exportMSSQL } from './export-per-type/mssql';
export const exportBaseSQL = (diagram: Diagram): string => { export const exportBaseSQL = (diagram: Diagram): string => {
const { tables, relationships } = diagram; const { tables, relationships } = diagram;
@@ -12,6 +13,10 @@ export const exportBaseSQL = (diagram: Diagram): string => {
return ''; return '';
} }
if (diagram.databaseType === DatabaseType.SQL_SERVER) {
return exportMSSQL(diagram);
}
// Filter out the tables that are views // Filter out the tables that are views
const nonViewTables = tables.filter((table) => !table.isView); const nonViewTables = tables.filter((table) => !table.isView);
@@ -196,6 +201,26 @@ export const exportBaseSQL = (diagram: Diagram): string => {
return sqlScript; return sqlScript;
}; };
const validateConfiguration = () => {
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;
// If using custom endpoint and model, don't require OpenAI API key
if (baseUrl && modelName) {
return { useCustomEndpoint: true };
}
// If using OpenAI's service, require API key
if (apiKey) {
return { useCustomEndpoint: false };
}
throw new Error(
'Configuration Error: Either provide an OpenAI API key or both a custom endpoint and model name'
);
};
export const exportSQL = async ( export const exportSQL = async (
diagram: Diagram, diagram: Diagram,
databaseType: DatabaseType, databaseType: DatabaseType,
@@ -206,6 +231,10 @@ export const exportSQL = async (
} }
): Promise<string> => { ): Promise<string> => {
const sqlScript = exportBaseSQL(diagram); const sqlScript = exportBaseSQL(diagram);
if (databaseType === DatabaseType.SQL_SERVER) {
return sqlScript;
}
const cacheKey = await generateCacheKey(databaseType, sqlScript); const cacheKey = await generateCacheKey(databaseType, sqlScript);
const cachedResult = getFromCache(cacheKey); const cachedResult = getFromCache(cacheKey);
@@ -213,20 +242,42 @@ export const exportSQL = async (
return cachedResult; return cachedResult;
} }
// Validate configuration before proceeding
const { useCustomEndpoint } = validateConfiguration();
const [{ streamText, generateText }, { createOpenAI }] = await Promise.all([ const [{ streamText, generateText }, { createOpenAI }] = await Promise.all([
import('ai'), import('ai'),
import('@ai-sdk/openai'), import('@ai-sdk/openai'),
]); ]);
const openai = createOpenAI({ const apiKey = window?.env?.OPENAI_API_KEY ?? OPENAI_API_KEY;
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';
let config: { apiKey: string; baseUrl?: string };
if (useCustomEndpoint) {
config = {
apiKey: apiKey,
baseUrl: baseUrl,
};
} else {
config = {
apiKey: apiKey,
};
}
const openai = createOpenAI(config);
const prompt = generateSQLPrompt(databaseType, sqlScript); const prompt = generateSQLPrompt(databaseType, sqlScript);
try {
if (options?.stream) { if (options?.stream) {
const { textStream, text: textPromise } = await streamText({ const { textStream, text: textPromise } = await streamText({
model: openai('gpt-4o-mini-2024-07-18'), model: openai(modelName),
prompt: prompt, prompt: prompt,
}); });
@@ -244,12 +295,23 @@ export const exportSQL = async (
} }
const { text } = await generateText({ const { text } = await generateText({
model: openai('gpt-4o-mini-2024-07-18'), model: openai(modelName),
prompt: prompt, prompt: prompt,
}); });
setInCache(cacheKey, text); setInCache(cacheKey, text);
return text; return text;
} catch (error: unknown) {
console.error('Error generating SQL:', error);
if (error instanceof Error && error.message.includes('API key')) {
throw new Error(
'Error: Please check your API configuration. If using a custom endpoint, make sure the endpoint URL is correct.'
);
}
throw new Error(
'Error generating SQL script. Please check your configuration and try again.'
);
}
}; };
function getMySQLDataTypeSize(type: DataType) { function getMySQLDataTypeSize(type: DataType) {

View File

@@ -85,7 +85,7 @@ export const sqliteQuery = `WITH fk_info AS (
ELSE LOWER(p.type) ELSE LOWER(p.type)
END, END,
'ordinal_position', p.cid, 'ordinal_position', p.cid,
'nullable', (CASE WHEN p."notnull" = 0 THEN 'true' ELSE 'false' END), 'nullable', (CASE WHEN p."notnull" = 0 THEN true ELSE false END),
'collation', '', 'collation', '',
'character_maximum_length', 'character_maximum_length',
CASE CASE

View File

@@ -1,23 +1,26 @@
import { DatabaseEdition } from '@/lib/domain/database-edition'; import { DatabaseEdition } from '@/lib/domain/database-edition';
const sqlServerQuery = `WITH fk_info AS ( const sqlServerQuery = `${`/* SQL Server 2017 and above edition (14.0, 15.0, 16.0, 17.0)*/`}
WITH fk_info AS (
SELECT SELECT
JSON_QUERY( JSON_QUERY(
'[' + STRING_AGG( N'[' + STRING_AGG(
CONVERT(nvarchar(max), CONVERT(nvarchar(max),
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(tp_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + JSON_QUERY(N'{
'", "table": "' + COALESCE(REPLACE(tp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tp_schema.name, '"', ''), ''), 'json') +
'", "column": "' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(tp.name, '"', ''), ''), 'json') +
'", "foreign_key_name": "' + COALESCE(REPLACE(fk.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
'", "reference_schema": "' + COALESCE(REPLACE(tr_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "foreign_key_name": "' + STRING_ESCAPE(COALESCE(REPLACE(fk.name, '"', ''), ''), 'json') +
'", "reference_table": "' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "reference_schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tr_schema.name, '"', ''), ''), 'json') +
'", "reference_column": "' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "reference_table": "' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
'", "fk_def": "FOREIGN KEY (' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "reference_column": "' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
') REFERENCES ' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "fk_def": "FOREIGN KEY (' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
'(' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + ') REFERENCES ' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
') ON DELETE ' + fk.delete_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS + '(' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
' ON UPDATE ' + fk.update_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS + '"}') ') ON DELETE ' + STRING_ESCAPE(fk.delete_referential_action_desc, 'json') +
), ',' ' ON UPDATE ' + STRING_ESCAPE(fk.update_referential_action_desc, 'json') +
'"}') COLLATE DATABASE_DEFAULT
), N','
) + N']' ) + N']'
) AS all_fks_json ) AS all_fks_json
FROM sys.foreign_keys AS fk FROM sys.foreign_keys AS fk
@@ -31,153 +34,138 @@ const sqlServerQuery = `WITH fk_info AS (
), pk_info AS ( ), pk_info AS (
SELECT SELECT
JSON_QUERY( JSON_QUERY(
'[' + STRING_AGG( N'[' +
STRING_AGG(
CONVERT(nvarchar(max), CONVERT(nvarchar(max),
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + JSON_QUERY(N'{
'", "table": "' + COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), ''), 'json') +
'", "column": "' + COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), ''), 'json') +
'", "pk_def": "PRIMARY KEY (' + pk.COLUMN_NAME COLLATE SQL_Latin1_General_CP1_CI_AS + ')"}') '", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), ''), 'json') +
), ',' '", "pk_def": "PRIMARY KEY (' + STRING_ESCAPE(pk.COLUMN_NAME, 'json') + N')"}') COLLATE DATABASE_DEFAULT
), N','
) + N']' ) + N']'
) AS all_pks_json ) AS all_pks_json
FROM FROM (
(
SELECT SELECT
kcu.TABLE_SCHEMA, kcu.TABLE_SCHEMA,
kcu.TABLE_NAME, kcu.TABLE_NAME,
kcu.COLUMN_NAME kcu.COLUMN_NAME
FROM FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
JOIN
INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
WHERE WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
) pk ) pk
), ),
cols AS ( cols AS (
SELECT SELECT
JSON_QUERY( JSON_QUERY(N'[' +
'[' + STRING_AGG( STRING_AGG(
CONVERT(nvarchar(max), CONVERT(nvarchar(max),
JSON_QUERY('{"schema": "' + COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), '') + JSON_QUERY(N'{
'", "table": "' + COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), '') + "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), ''), 'json') +
'", "name": "' + COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), '') + '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), ''), 'json') +
'", "ordinal_position": "' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) + '", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), ''), 'json') +
'", "type": "' + LOWER(cols.DATA_TYPE) + '", "ordinal_position": ' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
'", "character_maximum_length": "' + ', "type": "' + STRING_ESCAPE(LOWER(cols.DATA_TYPE), 'json') +
COALESCE(CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX)), 'null') + '", "character_maximum_length": ' +
'", "precision": ' +
CASE CASE
WHEN cols.DATA_TYPE IN ('numeric', 'decimal') THEN WHEN cols.CHARACTER_MAXIMUM_LENGTH IS NULL THEN 'null'
CONCAT('{"precision":', COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null'), ELSE CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX))
',"scale":', COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null'), '}')
ELSE
'null'
END + END +
', "nullable": ' + ', "precision": ' +
CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END + CASE
', "default": "' + WHEN cols.DATA_TYPE IN ('numeric', 'decimal')
COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), '') + THEN '{"precision":' + COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null') +
'", "collation": "' + ',"scale":' + COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null') + '}'
COALESCE(cols.COLLATION_NAME, '') + ELSE 'null'
'"}') END +
), ',' ', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
) + ']' ', "default": ' +
) AS all_columns_json '"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"' +
FROM ', "collation": ' + CASE
INFORMATION_SCHEMA.COLUMNS cols WHEN cols.COLLATION_NAME IS NULL THEN 'null'
WHERE ELSE '"' + STRING_ESCAPE(cols.COLLATION_NAME, 'json') + '"'
cols.TABLE_CATALOG = DB_NAME() END +
N'}') COLLATE DATABASE_DEFAULT
), N','
) +
N']') AS all_columns_json
FROM INFORMATION_SCHEMA.COLUMNS cols
WHERE cols.TABLE_CATALOG = DB_NAME()
), ),
indexes AS ( indexes AS (
SELECT SELECT
'[' + STRING_AGG( N'[' +
STRING_AGG(
CONVERT(nvarchar(max), CONVERT(nvarchar(max),
JSON_QUERY( JSON_QUERY(N'{
N'{"schema": "' + COALESCE(REPLACE(s.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
'", "table": "' + COALESCE(REPLACE(t.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(t.name, '"', ''), ''), 'json') +
'", "name": "' + COALESCE(REPLACE(i.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(i.name, '"', ''), ''), 'json') +
'", "column": "' + COALESCE(REPLACE(c.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(c.name, '"', ''), ''), 'json') +
'", "index_type": "' + LOWER(i.type_desc) COLLATE SQL_Latin1_General_CP1_CI_AS + '", "index_type": "' + STRING_ESCAPE(LOWER(i.type_desc), 'json') +
'", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END + '", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END +
', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END COLLATE SQL_Latin1_General_CP1_CI_AS + ', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END +
'", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}' '", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}'
) ) COLLATE DATABASE_DEFAULT
), ',' ), N','
) + N']' AS all_indexes_json ) +
FROM N']' AS all_indexes_json
sys.indexes i FROM sys.indexes i
JOIN JOIN sys.tables t ON i.object_id = t.object_id
sys.tables t ON i.object_id = t.object_id JOIN sys.schemas s ON t.schema_id = s.schema_id
JOIN JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
sys.schemas s ON t.schema_id = s.schema_id JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
JOIN WHERE s.name LIKE '%' AND i.name IS NOT NULL AND ic.is_included_column = 0
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
), ),
tbls AS ( tbls AS (
SELECT SELECT
'[' + STRING_AGG( N'[' + STRING_AGG(
CONVERT(nvarchar(max), CONVERT(nvarchar(max),
JSON_QUERY( JSON_QUERY(N'{
N'{"schema": "' + COALESCE(REPLACE(aggregated.schema_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.schema_name, '"', ''), ''), 'json') +
'", "table": "' + COALESCE(REPLACE(aggregated.table_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.table_name, '"', ''), ''), 'json') +
'", "row_count": "' + CAST(aggregated.row_count AS NVARCHAR(MAX)) + '", "row_count": ' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
'", "table_type": "' + aggregated.table_type COLLATE SQL_Latin1_General_CP1_CI_AS + ', "table_type": "' + STRING_ESCAPE(aggregated.table_type, 'json') +
'", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + '"}' '", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + N'"}'
) ) COLLATE DATABASE_DEFAULT
), ',' ), N','
) + N']' AS all_tables_json ) +
FROM N']' AS all_tables_json
( FROM (
-- Select from tables
SELECT SELECT
COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name, COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name,
COALESCE(REPLACE(t.name, '"', ''), '') AS table_name, COALESCE(REPLACE(t.name, '"', ''), '') AS table_name,
SUM(p.rows) AS row_count, SUM(p.rows) AS row_count,
t.type_desc AS table_type, t.type_desc AS table_type,
t.create_date AS creation_date t.create_date AS creation_date
FROM FROM sys.tables t
sys.tables t JOIN sys.schemas s ON t.schema_id = s.schema_id
JOIN JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
sys.schemas s ON t.schema_id = s.schema_id WHERE s.name LIKE '%'
JOIN GROUP BY s.name, t.name, t.type_desc, t.create_date
sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
WHERE
s.name LIKE '%'
GROUP BY
s.name, t.name, t.type_desc, t.create_date
UNION ALL UNION ALL
-- Select from views
SELECT SELECT
COALESCE(REPLACE(s.name, '"', ''), '') AS table_name, COALESCE(REPLACE(s.name, '"', ''), '') AS table_name,
COALESCE(REPLACE(v.name, '"', ''), '') AS object_name, COALESCE(REPLACE(v.name, '"', ''), '') AS object_name,
0 AS row_count, -- Views don't have row counts 0 AS row_count,
'VIEW' AS table_type, 'VIEW' AS table_type,
v.create_date AS creation_date v.create_date AS creation_date
FROM FROM sys.views v
sys.views v JOIN sys.schemas s ON v.schema_id = s.schema_id
JOIN WHERE s.name LIKE '%'
sys.schemas s ON v.schema_id = s.schema_id
WHERE
s.name LIKE '%'
) AS aggregated ) AS aggregated
), ),
views AS ( views AS (
SELECT SELECT
'[' + STRING_AGG( '[' + STRING_AGG(
CONVERT(nvarchar(max), CONVERT(nvarchar(max),
JSON_QUERY( JSON_QUERY(N'{
N'{"schema": "' + STRING_ESCAPE(COALESCE(s.name, ''), 'json') + "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
'", "view_name": "' + STRING_ESCAPE(COALESCE(v.name, ''), 'json') + '", "view_name": "' + STRING_ESCAPE(COALESCE(REPLACE(v.name, '"', ''), ''), 'json') +
'", "view_definition": "' + '", "view_definition": "' +
STRING_ESCAPE( STRING_ESCAPE(
CAST( CAST(
@@ -186,135 +174,123 @@ views AS (
'xs:base64Binary(sql:column("DefinitionBinary"))', 'xs:base64Binary(sql:column("DefinitionBinary"))',
'VARCHAR(MAX)' 'VARCHAR(MAX)'
), 'json') + ), 'json') +
'"}' N'"}') COLLATE DATABASE_DEFAULT
) ), N','
), ','
) + N']' AS all_views_json ) + N']' AS all_views_json
FROM FROM sys.views v
sys.views v JOIN sys.schemas s ON v.schema_id = s.schema_id
JOIN JOIN sys.sql_modules m ON v.object_id = m.object_id
sys.schemas s ON v.schema_id = s.schema_id
JOIN
sys.sql_modules m ON v.object_id = m.object_id
CROSS APPLY CROSS APPLY
(SELECT CONVERT(VARBINARY(MAX), m.definition) AS DefinitionBinary) AS bin (SELECT CONVERT(VARBINARY(MAX), m.definition) AS DefinitionBinary) AS bin
WHERE WHERE s.name LIKE '%'
s.name LIKE '%'
) )
SELECT JSON_QUERY( SELECT JSON_QUERY(
N'{"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') + N'{
"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
', "pk_info": ' + ISNULL((SELECT cast(all_pks_json as nvarchar(max)) FROM pk_info), N'[]') + ', "pk_info": ' + ISNULL((SELECT cast(all_pks_json as nvarchar(max)) FROM pk_info), N'[]') +
', "columns": ' + ISNULL((SELECT cast(all_columns_json as nvarchar(max)) FROM cols), N'[]') + ', "columns": ' + ISNULL((SELECT cast(all_columns_json as nvarchar(max)) FROM cols), N'[]') +
', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') + ', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') +
', "tables": ' + ISNULL((SELECT cast(all_tables_json as nvarchar(max)) FROM tbls), N'[]') + ', "tables": ' + ISNULL((SELECT cast(all_tables_json as nvarchar(max)) FROM tbls), N'[]') +
', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') + ', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') +
', "database_name": "' + DB_NAME() + '"' + ', "database_name": "' + STRING_ESCAPE(DB_NAME(), 'json') +
', "version": ""}' '", "version": ""
}'
) AS metadata_json_to_import; ) AS metadata_json_to_import;
`; `;
const sqlServer2016AndBelowQuery = `WITH fk_info AS ( const sqlServer2016AndBelowQuery = `${`/* SQL Server 2016 and below edition (13.0, 12.0, 11.0..) */`}
SELECT WITH fk_info AS (
JSON_QUERY( SELECT JSON_QUERY('[' +
'[' + ISNULL( ISNULL(
STUFF(( STUFF((
SELECT ',' + SELECT ',' +
CONVERT(nvarchar(max), CONVERT(nvarchar(max),
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(tp_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + JSON_QUERY(N'{
'", "table": "' + COALESCE(REPLACE(tp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tp_schema.name, '"', ''), ''), 'json') +
'", "column": "' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(tp.name, '"', ''), ''), 'json') +
'", "foreign_key_name": "' + COALESCE(REPLACE(fk.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
'", "reference_schema": "' + COALESCE(REPLACE(tr_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "foreign_key_name": "' + STRING_ESCAPE(COALESCE(REPLACE(fk.name, '"', ''), ''), 'json') +
'", "reference_table": "' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "reference_schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tr_schema.name, '"', ''), ''), 'json') +
'", "reference_column": "' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "reference_table": "' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
'", "fk_def": "FOREIGN KEY (' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "reference_column": "' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
') REFERENCES ' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "fk_def": "FOREIGN KEY (' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
'(' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + ') REFERENCES ' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
') ON DELETE ' + fk.delete_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS + '(' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
' ON UPDATE ' + fk.update_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS + '"}') ') ON DELETE ' + STRING_ESCAPE(fk.delete_referential_action_desc, 'json') +
' ON UPDATE ' + STRING_ESCAPE(fk.update_referential_action_desc, 'json') +
'"}') COLLATE DATABASE_DEFAULT
) )
FROM FROM sys.foreign_keys AS fk
sys.foreign_keys AS fk JOIN sys.foreign_key_columns AS fkc ON fk.object_id = fkc.constraint_object_id
JOIN JOIN sys.tables AS tp ON fkc.parent_object_id = tp.object_id
sys.foreign_key_columns AS fkc ON fk.object_id = fkc.constraint_object_id JOIN sys.schemas AS tp_schema ON tp.schema_id = tp_schema.schema_id
JOIN JOIN sys.columns AS cp ON fkc.parent_object_id = cp.object_id AND fkc.parent_column_id = cp.column_id
sys.tables AS tp ON fkc.parent_object_id = tp.object_id JOIN sys.tables AS tr ON fkc.referenced_object_id = tr.object_id
JOIN JOIN sys.schemas AS tr_schema ON tr.schema_id = tr_schema.schema_id
sys.schemas AS tp_schema ON tp.schema_id = tp_schema.schema_id JOIN sys.columns AS cr ON fkc.referenced_object_id = cr.object_id AND fkc.referenced_column_id = cr.column_id
JOIN
sys.columns AS cp ON fkc.parent_object_id = cp.object_id AND fkc.parent_column_id = cp.column_id
JOIN
sys.tables AS tr ON fkc.referenced_object_id = tr.object_id
JOIN
sys.schemas AS tr_schema ON tr.schema_id = tr_schema.schema_id
JOIN
sys.columns AS cr ON fkc.referenced_object_id = cr.object_id AND fkc.referenced_column_id = cr.column_id
FOR XML PATH('') FOR XML PATH('')
), 1, 1, ''), '') ), 1, 1, ''), '')
+ N']' + N']') AS all_fks_json
) AS all_fks_json
), ),
pk_info AS ( pk_info AS (
SELECT SELECT JSON_QUERY('[' +
JSON_QUERY( ISNULL(STUFF((
'[' + ISNULL(
STUFF((
SELECT ',' + SELECT ',' +
CONVERT(nvarchar(max), CONVERT(nvarchar(max),
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + JSON_QUERY(N'{
'", "table": "' + COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), ''), 'json') +
'", "column": "' + COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), ''), 'json') +
'", "pk_def": "PRIMARY KEY (' + pk.COLUMN_NAME COLLATE SQL_Latin1_General_CP1_CI_AS + ')"}') '", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), ''), 'json') +
'", "pk_def": "PRIMARY KEY (' + STRING_ESCAPE(pk.COLUMN_NAME, 'json') + N')"}') COLLATE DATABASE_DEFAULT
) )
FROM FROM
( (
SELECT SELECT kcu.TABLE_SCHEMA,
kcu.TABLE_SCHEMA,
kcu.TABLE_NAME, kcu.TABLE_NAME,
kcu.COLUMN_NAME kcu.COLUMN_NAME
FROM FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
JOIN
INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
WHERE WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
) pk ) pk
FOR XML PATH('') FOR XML PATH('')
), 1, 1, ''), '') ), 1, 1, ''), '')
+ N']' + N']') AS all_pks_json
) AS all_pks_json
), ),
cols AS ( cols AS (
SELECT SELECT JSON_QUERY('[' +
JSON_QUERY( ISNULL(
'[' + ISNULL(
STUFF(( STUFF((
SELECT ',' + SELECT ',' +
CONVERT(nvarchar(max), CONVERT(nvarchar(max),
JSON_QUERY('{"schema": "' + COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), '') + JSON_QUERY('{
'", "table": "' + COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), '') + "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), ''), 'json') +
'", "name": "' + COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), '') + '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), ''), 'json') +
'", "ordinal_position": "' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) + '", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), ''), 'json') +
'", "type": "' + LOWER(cols.DATA_TYPE) + '", "ordinal_position": ' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
'", "character_maximum_length": "' + ', "type": "' + STRING_ESCAPE(LOWER(cols.DATA_TYPE), 'json') +
COALESCE(CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX)), 'null') + '", "character_maximum_length": ' +
'", "precision": ' +
CASE CASE
WHEN cols.DATA_TYPE IN ('numeric', 'decimal') THEN WHEN cols.CHARACTER_MAXIMUM_LENGTH IS NULL THEN 'null'
CONCAT('{"precision":', COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null'), ELSE CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX))
',"scale":', COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null'), '}')
ELSE
'null'
END + END +
', "nullable": ' + ', "precision": ' +
CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END + CASE
', "default": "' + WHEN cols.DATA_TYPE IN ('numeric', 'decimal')
COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '"'), '') + THEN '{"precision":' + COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null') +
'", "collation": "' + ',"scale":' + COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null') + '}'
COALESCE(cols.COLLATION_NAME, '') + ELSE 'null'
'"}') END +
', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
', "default": ' +
'"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"' +
', "collation": ' +
CASE
WHEN cols.COLLATION_NAME IS NULL THEN 'null'
ELSE '"' + STRING_ESCAPE(cols.COLLATION_NAME, 'json') + '"'
END +
N'}')
) )
FROM FROM
INFORMATION_SCHEMA.COLUMNS cols INFORMATION_SCHEMA.COLUMNS cols
@@ -322,8 +298,7 @@ cols AS (
cols.TABLE_CATALOG = DB_NAME() cols.TABLE_CATALOG = DB_NAME()
FOR XML PATH('') FOR XML PATH('')
), 1, 1, ''), '') ), 1, 1, ''), '')
+ ']' + ']') AS all_columns_json
) AS all_columns_json
), ),
indexes AS ( indexes AS (
SELECT SELECT
@@ -331,30 +306,25 @@ indexes AS (
STUFF(( STUFF((
SELECT ',' + SELECT ',' +
CONVERT(nvarchar(max), CONVERT(nvarchar(max),
JSON_QUERY( JSON_QUERY(N'{
N'{"schema": "' + COALESCE(REPLACE(s.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
'", "table": "' + COALESCE(REPLACE(t.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(t.name, '"', ''), ''), 'json') +
'", "name": "' + COALESCE(REPLACE(i.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(i.name, '"', ''), ''), 'json') +
'", "column": "' + COALESCE(REPLACE(c.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(c.name, '"', ''), ''), 'json') +
'", "index_type": "' + LOWER(i.type_desc) COLLATE SQL_Latin1_General_CP1_CI_AS + '", "index_type": "' + STRING_ESCAPE(LOWER(i.type_desc), 'json') +
'", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END + '", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END +
', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END COLLATE SQL_Latin1_General_CP1_CI_AS + ', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END +
'", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}' '", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}'
) COLLATE DATABASE_DEFAULT
) )
) FROM sys.indexes i
FROM JOIN sys.tables t ON i.object_id = t.object_id
sys.indexes i JOIN sys.schemas s ON t.schema_id = s.schema_id
JOIN JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
sys.tables t ON i.object_id = t.object_id JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
JOIN WHERE s.name LIKE '%'
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 i.name IS NOT NULL
AND ic.is_included_column = 0
FOR XML PATH('') FOR XML PATH('')
), 1, 1, ''), '') ), 1, 1, ''), '')
+ N']' AS all_indexes_json + N']' AS all_indexes_json
@@ -365,12 +335,12 @@ tbls AS (
STUFF(( STUFF((
SELECT ',' + SELECT ',' +
CONVERT(nvarchar(max), CONVERT(nvarchar(max),
JSON_QUERY( JSON_QUERY(N'{
N'{"schema": "' + COALESCE(REPLACE(aggregated.schema_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.schema_name, '"', ''), ''), 'json') +
'", "table": "' + COALESCE(REPLACE(aggregated.object_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS + '", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.table_name, '"', ''), ''), 'json') +
'", "row_count": "' + CAST(aggregated.row_count AS NVARCHAR(MAX)) + '", "row_count": ' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
'", "object_type": "' + aggregated.object_type COLLATE SQL_Latin1_General_CP1_CI_AS + ', "table_type": "' + STRING_ESCAPE(aggregated.table_type, 'json') +
'", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + '"}' '", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + N'"}'
) )
) )
FROM FROM
@@ -378,20 +348,15 @@ tbls AS (
-- Select from tables -- Select from tables
SELECT SELECT
COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name, COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name,
COALESCE(REPLACE(t.name, '"', ''), '') AS object_name, COALESCE(REPLACE(t.name, '"', ''), '') AS table_name,
SUM(p.rows) AS row_count, SUM(p.rows) AS row_count,
t.type_desc AS object_type, t.type_desc AS table_type,
t.create_date AS creation_date t.create_date AS creation_date
FROM FROM sys.tables t
sys.tables t JOIN sys.schemas s ON t.schema_id = s.schema_id
JOIN JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
sys.schemas s ON t.schema_id = s.schema_id WHERE s.name LIKE '%'
JOIN GROUP BY s.name, t.name, t.type_desc, t.create_date
sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
WHERE
s.name LIKE '%'
GROUP BY
s.name, t.name, t.type_desc, t.create_date
UNION ALL UNION ALL
@@ -402,12 +367,9 @@ tbls AS (
0 AS row_count, -- Views don't have row counts 0 AS row_count, -- Views don't have row counts
'VIEW' AS object_type, 'VIEW' AS object_type,
v.create_date AS creation_date v.create_date AS creation_date
FROM FROM sys.views v
sys.views v JOIN sys.schemas s ON v.schema_id = s.schema_id
JOIN WHERE s.name LIKE '%'
sys.schemas s ON v.schema_id = s.schema_id
WHERE
s.name LIKE '%'
) AS aggregated ) AS aggregated
FOR XML PATH('') FOR XML PATH('')
), 1, 1, ''), '') ), 1, 1, ''), '')
@@ -417,18 +379,18 @@ views AS (
SELECT SELECT
'[' + '[' +
( (
SELECT SELECT STUFF((
STUFF((
SELECT ',' + CONVERT(nvarchar(max), SELECT ',' + CONVERT(nvarchar(max),
JSON_QUERY( JSON_QUERY(
N'{"schema": "' + COALESCE(REPLACE(s.name, '"', ''), '') + N'{
'", "view_name": "' + COALESCE(REPLACE(v.name, '"', ''), '') + "schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
'", "view_name": "' + STRING_ESCAPE(COALESCE(REPLACE(v.name, '"', ''), ''), 'json') +
'", "view_definition": "' + '", "view_definition": "' +
CAST( CAST(
( (
SELECT CAST(OBJECT_DEFINITION(v.object_id) AS VARBINARY(MAX)) FOR XML PATH('') SELECT CAST(OBJECT_DEFINITION(v.object_id) AS VARBINARY(MAX)) FOR XML PATH('')
) AS NVARCHAR(MAX) ) AS NVARCHAR(MAX)
) + '"}' ) + N'"}'
) )
) )
FROM FROM
@@ -441,14 +403,16 @@ views AS (
) + ']' AS all_views_json ) + ']' AS all_views_json
) )
SELECT JSON_QUERY( SELECT JSON_QUERY(
N'{"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') + N'{
"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
', "pk_info": ' + ISNULL((SELECT cast(all_pks_json as nvarchar(max)) FROM pk_info), N'[]') + ', "pk_info": ' + ISNULL((SELECT cast(all_pks_json as nvarchar(max)) FROM pk_info), N'[]') +
', "columns": ' + ISNULL((SELECT cast(all_columns_json as nvarchar(max)) FROM cols), N'[]') + ', "columns": ' + ISNULL((SELECT cast(all_columns_json as nvarchar(max)) FROM cols), N'[]') +
', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') + ', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') +
', "tables": ' + ISNULL((SELECT cast(all_objects_json as nvarchar(max)) FROM tbls), N'[]') + ', "tables": ' + ISNULL((SELECT cast(all_objects_json as nvarchar(max)) FROM tbls), N'[]') +
', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') + ', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') +
', "database_name": "' + DB_NAME() + '"' + ', "database_name": "' + DB_NAME() + '"' +
', "version": ""}' ', "version": ""
}'
) AS metadata_json_to_import;`; ) AS metadata_json_to_import;`;
export const getSqlServerQuery = ( export const getSqlServerQuery = (

View File

@@ -10,14 +10,20 @@ export const fixMetadataJson = async (
return ( return (
metadataJson metadataJson
.trim() .trim()
// First unescape the JSON string
.replace(/\\"/g, '"')
.replace(/\\\\/g, '\\')
.replace(/^[^{]*/, '') // Remove everything before the first '{' .replace(/^[^{]*/, '') // Remove everything before the first '{'
.replace(/}[^}]*$/, '}') // Remove everything after the last '}' .replace(/}[^}]*$/, '}') // Remove everything after the last '}'
.replace(/:""([^"]+)""/g, ':"$1"') // Convert :""value"" to :"value"
.replace(/""(\w+)""/g, '"$1"') // Convert ""key"" to "key"
.replace(/^\s+|\s+$/g, '') .replace(/^\s+|\s+$/g, '')
.replace(/^"|"$/g, '') .replace(/^"|"$/g, '')
.replace(/^'|'$/g, '') .replace(/^'|'$/g, '')
.replace(/""""/g, '""') // Remove Quadruple quotes from keys .replace(/""""/g, '""') // Remove Quadruple quotes from keys
.replace(/"""([^",}]+)"""/g, '"$1"') // Remove tripple quotes from keys .replace(/"""([^",}]+)"""/g, '"$1"') // Remove tripple quotes from keys
.replace(/""([^",}]+)""/g, '"$1"') // Remove double quotes from keys .replace(/""([^",}]+)""/g, '"$1"') // Remove double quotes from keys
/* eslint-disable-next-line no-useless-escape */ /* eslint-disable-next-line no-useless-escape */
.replace(/\"/g, '___ESCAPED_QUOTE___') // Temporarily replace empty strings .replace(/\"/g, '___ESCAPED_QUOTE___') // Temporarily replace empty strings
.replace(/(?<=:\s*)""(?=\s*[,}])/g, '___EMPTY___') // Temporarily replace empty strings .replace(/(?<=:\s*)""(?=\s*[,}])/g, '___EMPTY___') // Temporarily replace empty strings

View File

@@ -28,10 +28,24 @@ interface DBMLField {
increment?: boolean; increment?: boolean;
} }
interface DBMLIndexColumn {
value: string;
type?: string;
length?: number;
order?: 'asc' | 'desc';
}
interface DBMLIndex {
columns: string | (string | DBMLIndexColumn)[];
unique?: boolean;
name?: string;
}
interface DBMLTable { interface DBMLTable {
name: string; name: string;
schema?: string | { name: string }; schema?: string | { name: string };
fields: DBMLField[]; fields: DBMLField[];
indexes?: DBMLIndex[];
} }
interface DBMLEndpoint { interface DBMLEndpoint {
@@ -99,7 +113,8 @@ export const importDBMLToDiagram = async (
// Extract only the necessary data from the parsed DBML // Extract only the necessary data from the parsed DBML
const extractedData = { const extractedData = {
tables: dbmlData.tables.map((table: DBMLTable) => ({ tables: (dbmlData.tables as unknown as DBMLTable[]).map(
(table) => ({
name: table.name, name: table.name,
schema: table.schema, schema: table.schema,
fields: table.fields.map((field: DBMLField) => ({ fields: table.fields.map((field: DBMLField) => ({
@@ -110,7 +125,48 @@ export const importDBMLToDiagram = async (
not_null: field.not_null, not_null: field.not_null,
increment: field.increment, increment: field.increment,
})), })),
})), indexes:
table.indexes?.map((dbmlIndex) => {
let indexColumns: string[];
// Handle composite index case "(col1, col2)"
if (typeof dbmlIndex.columns === 'string') {
if (dbmlIndex.columns.includes('(')) {
// Composite index
const columnsStr =
dbmlIndex.columns.replace(/[()]/g, '');
indexColumns = columnsStr
.split(',')
.map((c) => c.trim());
} else {
// Single column
indexColumns = [dbmlIndex.columns.trim()];
}
} else {
// Handle array of columns
indexColumns = Array.isArray(dbmlIndex.columns)
? dbmlIndex.columns.map((col) =>
typeof col === 'object' &&
'value' in col
? (col.value as string).trim()
: (col as string).trim()
)
: [String(dbmlIndex.columns).trim()];
}
// Generate a consistent index name
const indexName =
dbmlIndex.name ||
`idx_${table.name}_${indexColumns.join('_')}`;
return {
columns: indexColumns,
unique: dbmlIndex.unique || false,
name: indexName,
};
}) || [],
})
),
refs: (dbmlData.refs as unknown as DBMLRef[]).map((ref) => ({ refs: (dbmlData.refs as unknown as DBMLRef[]).map((ref) => ({
endpoints: (ref.endpoints as [DBMLEndpoint, DBMLEndpoint]).map( endpoints: (ref.endpoints as [DBMLEndpoint, DBMLEndpoint]).map(
(endpoint) => ({ (endpoint) => ({
@@ -126,7 +182,42 @@ export const importDBMLToDiagram = async (
const tables: DBTable[] = extractedData.tables.map((table, index) => { const tables: DBTable[] = extractedData.tables.map((table, index) => {
const row = Math.floor(index / 4); const row = Math.floor(index / 4);
const col = index % 4; const col = index % 4;
const tableSpacing = 300; // Increased spacing between tables const tableSpacing = 300;
// Create fields first so we have their IDs
const fields = table.fields.map((field) => ({
id: generateId(),
name: field.name.replace(/['"]/g, ''),
type: mapDBMLTypeToGenericType(field.type.type_name),
nullable: !field.not_null,
primaryKey: field.pk || false,
unique: field.unique || false,
createdAt: Date.now(),
}));
// Convert DBML indexes to ChartDB indexes
const indexes =
table.indexes?.map((dbmlIndex) => {
const fieldIds = dbmlIndex.columns.map((columnName) => {
const field = fields.find((f) => f.name === columnName);
if (!field) {
throw new Error(
`Index references non-existent column: ${columnName}`
);
}
return field.id;
});
return {
id: generateId(),
name:
dbmlIndex.name ||
`idx_${table.name}_${dbmlIndex.columns.join('_')}`,
fieldIds,
unique: dbmlIndex.unique || false,
createdAt: Date.now(),
};
}) || [];
return { return {
id: generateId(), id: generateId(),
@@ -136,18 +227,10 @@ export const importDBMLToDiagram = async (
? table.schema ? table.schema
: table.schema?.name || '', : table.schema?.name || '',
order: index, order: index,
fields: table.fields.map((field) => ({ fields,
id: generateId(), indexes,
name: field.name.replace(/['"]/g, ''),
type: mapDBMLTypeToGenericType(field.type.type_name),
nullable: !field.not_null,
primaryKey: field.pk || false,
unique: field.unique || false,
createdAt: Date.now(),
})),
x: col * tableSpacing, x: col * tableSpacing,
y: row * tableSpacing, y: row * tableSpacing,
indexes: [],
color: randomColor(), color: randomColor(),
isView: false, isView: false,
createdAt: Date.now(), createdAt: Date.now(),

View File

@@ -1,5 +1,11 @@
export const OPENAI_API_KEY: string = import.meta.env.VITE_OPENAI_API_KEY; export const OPENAI_API_KEY: string = import.meta.env.VITE_OPENAI_API_KEY;
export const OPENAI_API_ENDPOINT: string = import.meta.env
.VITE_OPENAI_API_ENDPOINT;
export const LLM_MODEL_NAME: string = import.meta.env.VITE_LLM_MODEL_NAME;
export const IS_CHARTDB_IO: boolean = export const IS_CHARTDB_IO: boolean =
import.meta.env.VITE_IS_CHARTDB_IO === 'true'; import.meta.env.VITE_IS_CHARTDB_IO === 'true';
export const APP_URL: string = import.meta.env.VITE_APP_URL; export const APP_URL: string = import.meta.env.VITE_APP_URL;
export const HOST_URL: string = import.meta.env.VITE_HOST_URL ?? ''; export const HOST_URL: string = import.meta.env.VITE_HOST_URL ?? '';
export const HIDE_BUCKLE_DOT_DEV: boolean =
(window?.env?.HIDE_BUCKLE_DOT_DEV ??
import.meta.env.VITE_HIDE_BUCKLE_DOT_DEV) === 'true';

View File

@@ -9,9 +9,10 @@ import { useChartDB } from '@/hooks/use-chartdb';
import { useLayout } from '@/hooks/use-layout'; import { useLayout } from '@/hooks/use-layout';
import { cloneTable } from '@/lib/clone'; import { cloneTable } from '@/lib/clone';
import type { DBTable } from '@/lib/domain/db-table'; import type { DBTable } from '@/lib/domain/db-table';
import { Copy, Pencil, Trash2 } from 'lucide-react'; import { Copy, Pencil, Trash2, Workflow } from 'lucide-react';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDialog } from '@/hooks/use-dialog';
export interface TableNodeContextMenuProps { export interface TableNodeContextMenuProps {
table: DBTable; table: DBTable;
@@ -24,6 +25,7 @@ export const TableNodeContextMenu: React.FC<
const { openTableFromSidebar } = useLayout(); const { openTableFromSidebar } = useLayout();
const { t } = useTranslation(); const { t } = useTranslation();
const { isMd: isDesktop } = useBreakpoint('md'); const { isMd: isDesktop } = useBreakpoint('md');
const { openCreateRelationshipDialog } = useDialog();
const duplicateTableHandler = useCallback(() => { const duplicateTableHandler = useCallback(() => {
const clonedTable = cloneTable(table); const clonedTable = cloneTable(table);
@@ -43,6 +45,12 @@ export const TableNodeContextMenu: React.FC<
removeTable(table.id); removeTable(table.id);
}, [removeTable, table.id]); }, [removeTable, table.id]);
const addRelationshipHandler = useCallback(() => {
openCreateRelationshipDialog({
sourceTableId: table.id,
});
}, [openCreateRelationshipDialog, table.id]);
if (!isDesktop || readonly) { if (!isDesktop || readonly) {
return <>{children}</>; return <>{children}</>;
} }
@@ -64,6 +72,13 @@ export const TableNodeContextMenu: React.FC<
<span>{t('table_node_context_menu.duplicate_table')}</span> <span>{t('table_node_context_menu.duplicate_table')}</span>
<Copy className="size-3.5" /> <Copy className="size-3.5" />
</ContextMenuItem> </ContextMenuItem>
<ContextMenuItem
onClick={addRelationshipHandler}
className="flex justify-between gap-3"
>
<span>{t('table_node_context_menu.add_relationship')}</span>
<Workflow className="size-3.5" />
</ContextMenuItem>
<ContextMenuItem <ContextMenuItem
onClick={removeTableHandler} onClick={removeTableHandler}
className="flex justify-between gap-3" className="flex justify-between gap-3"

View File

@@ -5,11 +5,11 @@ import { Button } from '@/components/button/button';
import { import {
ChevronsLeftRight, ChevronsLeftRight,
ChevronsRightLeft, ChevronsRightLeft,
Pencil,
Table2, Table2,
ChevronDown, ChevronDown,
ChevronUp, ChevronUp,
Check, Check,
CircleDotDashed,
} from 'lucide-react'; } from 'lucide-react';
import { Label } from '@/components/label/label'; import { Label } from '@/components/label/label';
import type { DBTable } from '@/lib/domain/db-table'; import type { DBTable } from '@/lib/domain/db-table';
@@ -247,7 +247,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200" className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
onClick={openTableInEditor} onClick={openTableInEditor}
> >
<Pencil className="size-4" /> <CircleDotDashed className="size-4" />
</Button> </Button>
)} )}
{editMode ? null : ( {editMode ? null : (

View File

@@ -1,22 +1,12 @@
import React, { import React, { Suspense, useCallback, useEffect, useRef } from 'react';
Suspense,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { TopNavbar } from './top-navbar/top-navbar'; import { TopNavbar } from './top-navbar/top-navbar';
import { useNavigate, useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useConfig } from '@/hooks/use-config';
import { useChartDB } from '@/hooks/use-chartdb'; import { useChartDB } from '@/hooks/use-chartdb';
import { useDialog } from '@/hooks/use-dialog'; import { useDialog } from '@/hooks/use-dialog';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
import { Toaster } from '@/components/toast/toaster'; import { Toaster } from '@/components/toast/toaster';
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
import { useBreakpoint } from '@/hooks/use-breakpoint'; import { useBreakpoint } from '@/hooks/use-breakpoint';
import { useLayout } from '@/hooks/use-layout'; import { useLayout } from '@/hooks/use-layout';
import { useToast } from '@/components/toast/use-toast'; import { useToast } from '@/components/toast/use-toast';
import type { Diagram } from '@/lib/domain/diagram';
import { ToastAction } from '@/components/toast/toast'; import { ToastAction } from '@/components/toast/toast';
import { useLocalConfig } from '@/hooks/use-local-config'; import { useLocalConfig } from '@/hooks/use-local-config';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -35,9 +25,10 @@ import { DialogProvider } from '@/context/dialog-context/dialog-provider';
import { KeyboardShortcutsProvider } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts-provider'; import { KeyboardShortcutsProvider } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts-provider';
import { Spinner } from '@/components/spinner/spinner'; import { Spinner } from '@/components/spinner/spinner';
import { Helmet } from 'react-helmet-async'; import { Helmet } from 'react-helmet-async';
import { useStorage } from '@/hooks/use-storage';
import { AlertProvider } from '@/context/alert-context/alert-provider'; import { AlertProvider } from '@/context/alert-context/alert-provider';
import { CanvasProvider } from '@/context/canvas-context/canvas-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 OPEN_STAR_US_AFTER_SECONDS = 30;
const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1; const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1;
@@ -55,23 +46,12 @@ export const EditorMobileLayoutLazy = React.lazy(
); );
const EditorPageComponent: React.FC = () => { const EditorPageComponent: React.FC = () => {
const { const { diagramName, currentDiagram, schemas, filteredSchemas } =
loadDiagram, useChartDB();
diagramName,
currentDiagram,
schemas,
filteredSchemas,
} = useChartDB();
const { openSelectSchema, showSidePanel } = useLayout(); const { openSelectSchema, showSidePanel } = useLayout();
const { resetRedoStack, resetUndoStack } = useRedoUndoStack(); const { openStarUsDialog, openBuckleDialog } = useDialog();
const { showLoader, hideLoader } = useFullScreenLoader();
const { openCreateDiagramDialog, openStarUsDialog, openBuckleDialog } =
useDialog();
const { diagramId } = useParams<{ diagramId: string }>(); const { diagramId } = useParams<{ diagramId: string }>();
const { config, updateConfig } = useConfig();
const navigate = useNavigate();
const { isMd: isDesktop } = useBreakpoint('md'); const { isMd: isDesktop } = useBreakpoint('md');
const [initialDiagram, setInitialDiagram] = useState<Diagram | undefined>();
const { const {
hideMultiSchemaNotification, hideMultiSchemaNotification,
setHideMultiSchemaNotification, setHideMultiSchemaNotification,
@@ -84,75 +64,13 @@ const EditorPageComponent: React.FC = () => {
} = useLocalConfig(); } = useLocalConfig();
const { toast } = useToast(); const { toast } = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
const { listDiagrams } = useStorage(); const { initialDiagram } = useDiagramLoader();
useEffect(() => { useEffect(() => {
if (!config) { if (HIDE_BUCKLE_DOT_DEV) {
return; 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 (!currentDiagram?.id || githubRepoOpened) { if (!currentDiagram?.id || githubRepoOpened) {
return; return;
} }
@@ -174,6 +92,10 @@ const EditorPageComponent: React.FC = () => {
]); ]);
useEffect(() => { useEffect(() => {
if (HIDE_BUCKLE_DOT_DEV) {
return;
}
if (!currentDiagram?.id) { if (!currentDiagram?.id) {
return; return;
} }

View File

@@ -98,10 +98,16 @@ export const RelationshipListItemContent: React.FC<
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<div className="truncate text-left text-sm"> <div className="truncate text-left text-sm">
{sourceTable?.schema
? `${sourceTable.schema}.`
: ''}
{sourceTable?.name}({sourceField?.name}) {sourceTable?.name}({sourceField?.name})
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{sourceTable?.schema
? `${sourceTable.schema}.`
: ''}
{sourceTable?.name}({sourceField?.name}) {sourceTable?.name}({sourceField?.name})
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@@ -117,11 +123,17 @@ export const RelationshipListItemContent: React.FC<
</div> </div>
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<div className="truncate text-left text-sm "> <div className="truncate text-left text-sm">
{targetTable?.schema
? `${targetTable.schema}.`
: ''}
{targetTable?.name}({targetField?.name}) {targetTable?.name}({targetField?.name})
</div> </div>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{targetTable?.schema
? `${targetTable.schema}.`
: ''}
{targetTable?.name}({targetField?.name}) {targetTable?.name}({targetField?.name})
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>

View File

@@ -0,0 +1,18 @@
import React from 'react';
import { Toggle } from '@/components/toggle/toggle';
export const TableIndexToggle = React.forwardRef<
React.ElementRef<typeof Toggle>,
React.ComponentPropsWithoutRef<typeof Toggle>
>((props, ref) => {
return (
<Toggle
{...props}
ref={ref}
variant="default"
className="h-8 w-[32px] p-2 text-xs text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
/>
);
});
TableIndexToggle.displayName = Toggle.displayName;

View File

@@ -14,6 +14,12 @@ import { Label } from '@/components/label/label';
import { Input } from '@/components/input/input'; import { Input } from '@/components/input/input';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SelectBox } from '@/components/select-box/select-box'; import { SelectBox } from '@/components/select-box/select-box';
import { TableIndexToggle } from './table-index-toggle';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
export interface TableIndexProps { export interface TableIndexProps {
index: DBIndex; index: DBIndex;
@@ -54,7 +60,28 @@ export const TableIndex: React.FC<TableIndexProps> = ({
)} )}
keepOrder keepOrder
/> />
<div className="flex shrink-0"> <div className="flex shrink-0 gap-1">
<Tooltip>
<TooltipTrigger asChild>
<span>
<TableIndexToggle
pressed={index.unique}
onPressedChange={(value) =>
updateIndex({
unique: !!value,
})
}
>
U
</TableIndexToggle>
</span>
</TooltipTrigger>
<TooltipContent>
{t(
'side_panel.tables_section.table.index_actions.unique'
)}
</TooltipContent>
</Tooltip>
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button <Button

View File

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

View File

@@ -162,7 +162,7 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
<EllipsisVertical /> <EllipsisVertical />
</ListItemHeaderButton> </ListItemHeaderButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="w-fit"> <DropdownMenuContent className="w-fit min-w-40">
<DropdownMenuLabel> <DropdownMenuLabel>
{t( {t(
'side_panel.tables_section.table.table_actions.title' 'side_panel.tables_section.table.table_actions.title'

View File

@@ -102,6 +102,10 @@ export const Menu: React.FC<MenuProps> = () => {
window.location.href = 'https://chartdb.io'; window.location.href = 'https://chartdb.io';
}, []); }, []);
const openChartDBDocs = useCallback(() => {
window.open('https://docs.chartdb.io', '_blank');
}, []);
const openJoinDiscord = useCallback(() => { const openJoinDiscord = useCallback(() => {
window.open('https://discord.gg/QeFwyWSKwC', '_blank'); window.open('https://discord.gg/QeFwyWSKwC', '_blank');
}, []); }, []);
@@ -225,6 +229,9 @@ export const Menu: React.FC<MenuProps> = () => {
{t('menu.file.import')} {t('menu.file.import')}
</MenubarSubTrigger> </MenubarSubTrigger>
<MenubarSubContent> <MenubarSubContent>
<MenubarItem onClick={openImportDiagramDialog}>
.json
</MenubarItem>
<MenubarItem onClick={() => openImportDBMLDialog()}> <MenubarItem onClick={() => openImportDBMLDialog()}>
.dbml .dbml
</MenubarItem> </MenubarItem>
@@ -341,6 +348,10 @@ export const Menu: React.FC<MenuProps> = () => {
<MenubarItem onClick={exportPNG}>PNG</MenubarItem> <MenubarItem onClick={exportPNG}>PNG</MenubarItem>
<MenubarItem onClick={exportJPG}>JPG</MenubarItem> <MenubarItem onClick={exportJPG}>JPG</MenubarItem>
<MenubarItem onClick={exportSVG}>SVG</MenubarItem> <MenubarItem onClick={exportSVG}>SVG</MenubarItem>
<MenubarSeparator />
<MenubarItem onClick={openExportDiagramDialog}>
JSON
</MenubarItem>
</MenubarSubContent> </MenubarSubContent>
</MenubarSub> </MenubarSub>
<MenubarSeparator /> <MenubarSeparator />
@@ -487,13 +498,13 @@ export const Menu: React.FC<MenuProps> = () => {
</MenubarMenu> </MenubarMenu>
<MenubarMenu> <MenubarMenu>
<MenubarTrigger>{t('menu.share.share')}</MenubarTrigger> <MenubarTrigger>{t('menu.backup.backup')}</MenubarTrigger>
<MenubarContent> <MenubarContent>
<MenubarItem onClick={openExportDiagramDialog}> <MenubarItem onClick={openExportDiagramDialog}>
{t('menu.share.export_diagram')} {t('menu.backup.export_diagram')}
</MenubarItem> </MenubarItem>
<MenubarItem onClick={openImportDiagramDialog}> <MenubarItem onClick={openImportDiagramDialog}>
{t('menu.share.import_diagram')} {t('menu.backup.restore_diagram')}
</MenubarItem> </MenubarItem>
</MenubarContent> </MenubarContent>
</MenubarMenu> </MenubarMenu>
@@ -501,6 +512,9 @@ export const Menu: React.FC<MenuProps> = () => {
<MenubarMenu> <MenubarMenu>
<MenubarTrigger>{t('menu.help.help')}</MenubarTrigger> <MenubarTrigger>{t('menu.help.help')}</MenubarTrigger>
<MenubarContent> <MenubarContent>
<MenubarItem onClick={openChartDBDocs}>
{t('menu.help.docs_website')}
</MenubarItem>
<MenubarItem onClick={openChartDBIO}> <MenubarItem onClick={openChartDBIO}>
{t('menu.help.visit_website')} {t('menu.help.visit_website')}
</MenubarItem> </MenubarItem>

View File

@@ -7,6 +7,7 @@ import { DiagramName } from './diagram-name';
import { LastSaved } from './last-saved'; import { LastSaved } from './last-saved';
import { LanguageNav } from './language-nav/language-nav'; import { LanguageNav } from './language-nav/language-nav';
import { Menu } from './menu/menu'; import { Menu } from './menu/menu';
import { HIDE_BUCKLE_DOT_DEV } from '@/lib/env';
export interface TopNavbarProps {} export interface TopNavbarProps {}
@@ -30,6 +31,10 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
}, []); }, []);
const renderGetBuckleButton = useCallback(() => { const renderGetBuckleButton = useCallback(() => {
if (HIDE_BUCKLE_DOT_DEV) {
return null;
}
return ( return (
<button <button
className="gradient-background relative inline-flex items-center justify-center overflow-hidden rounded-lg p-0.5 text-base text-gray-700 focus:outline-none focus:ring-0" className="gradient-background relative inline-flex items-center justify-center overflow-hidden rounded-lg p-0.5 text-base text-gray-700 focus:outline-none focus:ring-0"

View File

@@ -0,0 +1,92 @@
import { useChartDB } from '@/hooks/use-chartdb';
import { useConfig } from '@/hooks/use-config';
import { useDialog } from '@/hooks/use-dialog';
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
import { useStorage } from '@/hooks/use-storage';
import type { Diagram } from '@/lib/domain/diagram';
import { useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
export const useDiagramLoader = () => {
const [initialDiagram, setInitialDiagram] = useState<Diagram | undefined>();
const { diagramId } = useParams<{ diagramId: string }>();
const { config } = useConfig();
const { loadDiagram, currentDiagram } = useChartDB();
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
const { showLoader, hideLoader } = useFullScreenLoader();
const { openCreateDiagramDialog, openOpenDiagramDialog } = useDialog();
const navigate = useNavigate();
const { listDiagrams } = useStorage();
const currentDiagramLoadingRef = useRef<string | undefined>(undefined);
useEffect(() => {
if (!config) {
return;
}
if (currentDiagram?.id === diagramId) {
return;
}
const loadDefaultDiagram = async () => {
if (diagramId) {
setInitialDiagram(undefined);
showLoader();
resetRedoStack();
resetUndoStack();
const diagram = await loadDiagram(diagramId);
if (!diagram) {
openOpenDiagramDialog({ canClose: false });
hideLoader();
return;
}
setInitialDiagram(diagram);
hideLoader();
return;
} else if (!diagramId && config.defaultDiagramId) {
const diagram = await loadDiagram(config.defaultDiagramId);
if (diagram) {
navigate(`/diagrams/${config.defaultDiagramId}`);
return;
}
}
const diagrams = await listDiagrams();
if (diagrams.length > 0) {
openOpenDiagramDialog({ canClose: false });
} else {
openCreateDiagramDialog();
}
};
if (
currentDiagramLoadingRef.current === (diagramId ?? '') &&
currentDiagramLoadingRef.current !== undefined
) {
return;
}
currentDiagramLoadingRef.current = diagramId ?? '';
loadDefaultDiagram();
}, [
diagramId,
openCreateDiagramDialog,
config,
navigate,
listDiagrams,
loadDiagram,
resetRedoStack,
resetUndoStack,
hideLoader,
showLoader,
currentDiagram?.id,
openOpenDiagramDialog,
]);
return { initialDiagram };
};