Compare commits

...

35 Commits

Author SHA1 Message Date
Guy Ben-Aharon
86840a8822 chore(main): release 1.12.0 (#662) 2025-05-21 12:03:11 +03:00
Jonathan Fishner
487fb2d5c1 fix(sql-script): change ddl to be sql-script (#710) 2025-05-20 17:52:08 +03:00
Jonathan Fishner
54d5e96a6d fix(expanded-table): persist expanded state across renders (#707)
* fix(expanded-table): persist expanded state across renders

* fix(reorder-tables): account for table sizes when reordering

* some fixes

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-20 12:04:11 +03:00
Jonathan Fishner
481ad3c844 fix(import-database): remove view_definition when importing via query (#702)
* fix(import-database): remove view_definition when importing via query

* fix(view_definition): remove view_definition in cockroachdb & maria

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-14 18:59:09 +03:00
Guy Ben-Aharon
0ce85cf76b fix(export image): Fix usage of advanced options accordion (#703) 2025-05-14 18:41:05 +03:00
Jonathan Fishner
5849e4586c fix(ddl): inline fks ddl script (#701)
* fix(import-ddl): when importing postgres with inline fks

* fix(import-ddl): for postgres and mysql

* remove logs

* fix build

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-13 11:47:22 +03:00
Jonathan Fishner
34c0a7163f fix(import): dbml and query - senetize before import (#699) 2025-05-12 19:10:40 +03:00
Jonathan Fishner
89e3ceab00 fix(postgres): fix import of postgres fks (#700)
* Add regex-based foreign key detection for PostgreSQL to handle complex formatting

* fix(import-ddl): for postgres know to deal with SERIAL type

* fix(export-sql): for postgres know to deal with SERIAL type

* fix(import-ddl): when importing default values
2025-05-12 18:22:30 +03:00
Jonathan Fishner
5a5e64abef fix(import-database): auto detect when user try to import ddl script (#698)
* fix(import-database): auto detect when user try to import ddl script

* fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-12 09:44:35 +03:00
Jonathan Fishner
2368e0d263 fix(import-json): for broken json imports (#697)
* fix(impoort-json): for broken json imports

* fix(import-db): after checking the JSON keep it formated
2025-05-11 18:17:52 +03:00
Jonathan Fishner
547149da44 fix(dependencies): hide icon when diagram has no dependencies (#684) 2025-05-08 16:28:31 +03:00
Jonathan Fishner
a1144bbf76 fix(ddl-import): fix datatypes when importing via ddl (#696)
* fix(ddl-import): fix datatypes when importing via ddl

* fix build

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-08 14:48:08 +03:00
STPN
6b8d637b75 feat(image-export): add transparent and pattern export image toggles (#671)
* feat(image-export): add watermark, transparent and pattern export image toggles

* fix(export-image-provider): use pattern background enabling

* add translations + small ui fixes

* fix build

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-08 14:35:17 +03:00
Guy Ben-Aharon
fd47eb7f4b update email address (#695) 2025-05-07 19:34:14 +03:00
Guy Ben-Aharon
7db86dcf8c fix(navbar): open diagram directly from diagram icon (#694) 2025-05-07 18:24:22 +03:00
Guy Ben-Aharon
e75323c16e update config fix (#693) 2025-05-07 18:20:15 +03:00
Тоха Лис
97d01d7201 fix(translations): Add some translations for ru-RU language (#690)
* chore: install unmet deps

* fix(i18n): Update Russian translation

* fix(dialog): adjust width of import DBML dialog for better responsiveness

* revert package.json

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-07 11:21:26 +03:00
Guy Ben-Aharon
90b42a4bb7 update created at on examples (#691)
* update created at on examples + templates

* update created at on examples + templates
2025-05-04 13:47:39 +03:00
Jonathan Fishner
fbf2fe919c fix(dbml-editor): add inline refs mode + fix issues with DBML syntax (#687)
* fix(dbml-editor): support & fix cases when mismatching with special chars

* fix(dbml-editor): add inline refs mode

* fix(dbml-editor): more fixes for dbml parser

* fix(dbml-editor): show colors for datatypes like char, varchar etc

* fix(sql-export): normalize verbose SQL types to simplified names

* some fix

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-05-03 17:53:36 +03:00
Guy Ben-Aharon
d3ddf7c51e fix(performance): update field only when changed (#685) 2025-04-30 17:16:57 +03:00
Jonathan Fishner
5759241573 fix(dbml-editor): remove invalid fields before showing DBML + warning (#683) 2025-04-30 12:08:35 +03:00
Guy Ben-Aharon
3747abbc3b refactor(dialog): pass params to create diagram dialog (#682) 2025-04-29 17:39:48 +03:00
Jonathan Fishner
226e6cf1ce fix(import-json): simplify import script for fixing invalid JSON (#681) 2025-04-29 17:36:28 +03:00
Guy Ben-Aharon
1778abb683 fix(examples): fix clone examples (#679) 2025-04-27 15:25:49 +03:00
Guy Ben-Aharon
90a20dd1b0 fix(examples): add loader (#678) 2025-04-27 15:04:42 +03:00
Jonathan Fishner
21c9129e14 feat(examples): update examples to have areas (#677) 2025-04-27 14:32:03 +03:00
Guy Ben-Aharon
19d2d0bddd fix(table): enhance field focus behavior to include table hover state (#676) 2025-04-27 12:44:07 +03:00
Guy Ben-Aharon
83c43332d4 fix(performance): Only render visible (#672) 2025-04-23 12:11:27 +03:00
Jonathan Fishner
3a1b8d1db1 fix: add sorting based on how common the datatype on side-panel (#651)
* fix: add sorting based on how common the datatype on side-panel

* fix type to new version

* remove colors

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-04-23 11:48:31 +03:00
Guy Ben-Aharon
46426e27b4 add cla workflow (#667) 2025-04-22 17:27:29 +03:00
Guy Ben-Aharon
9402822fa3 fix(canvas): disable edit area name on read only (#666) 2025-04-22 16:39:21 +03:00
Guy Ben-Aharon
651fe361fc fix(canvas): read only mode (#665) 2025-04-22 16:29:39 +03:00
Guy Ben-Aharon
aee1713aec fix(clone): add areas to clone diagram (#664) 2025-04-22 15:44:38 +03:00
Guy Ben-Aharon
ecfa14829b fix(schema): add areas to diagram schema (#663) 2025-04-22 15:24:21 +03:00
Guy Ben-Aharon
92e3ec785c feat(areas): implement area to enable logical diagram arrangement (#661)
* initial

* translations + update from canvas

* create area from canvas

* fix build

* fix build

* fix build
2025-04-22 15:00:52 +03:00
158 changed files with 16954 additions and 12756 deletions

33
.github/workflows/cla.yaml vendored Normal file
View File

@@ -0,0 +1,33 @@
name: "CLA Assistant"
on:
issue_comment:
types: [created]
pull_request_target:
types: [opened,closed,synchronize]
permissions:
actions: write
contents: write # this can be 'read' if the signatures are in remote repository
pull-requests: write
statuses: write
jobs:
CLAAssistant:
runs-on: ubuntu-latest
steps:
- name: "CLA Assistant"
if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target'
# Beta Release
uses: contributor-assistant/github-action@v2.6.1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PERSONAL_ACCESS_TOKEN: ${{ secrets.CHARTDB_CLA_SIGNATURES_PAT }}
with:
remote-organization-name: 'chartdb'
remote-repository-name: 'cla-signatures'
path-to-signatures: 'signatures/version1/cla.json'
path-to-document: 'https://github.com/chartdb/chartdb/blob/main/CLA.md'
# branch should not be protected
branch: 'main'
allowlist:

View File

@@ -1,5 +1,44 @@
# Changelog
## [1.12.0](https://github.com/chartdb/chartdb/compare/v1.11.0...v1.12.0) (2025-05-20)
### Features
* **areas:** implement area to enable logical diagram arrangement ([#661](https://github.com/chartdb/chartdb/issues/661)) ([92e3ec7](https://github.com/chartdb/chartdb/commit/92e3ec785c91f7f19881c6d9d0692257af4651bc))
* **examples:** update examples to have areas ([#677](https://github.com/chartdb/chartdb/issues/677)) ([21c9129](https://github.com/chartdb/chartdb/commit/21c9129e14670c744950cd43a5cbdd4b7d47c639))
* **image-export:** add transparent and pattern export image toggles ([#671](https://github.com/chartdb/chartdb/issues/671)) ([6b8d637](https://github.com/chartdb/chartdb/commit/6b8d637b757b94630ecd7521b4a2c99634afae69))
### Bug Fixes
* add sorting based on how common the datatype on side-panel ([#651](https://github.com/chartdb/chartdb/issues/651)) ([3a1b8d1](https://github.com/chartdb/chartdb/commit/3a1b8d1db13d8dd7cb6cbe5ef8c5a60faccfeae5))
* **canvas:** disable edit area name on read only ([#666](https://github.com/chartdb/chartdb/issues/666)) ([9402822](https://github.com/chartdb/chartdb/commit/9402822fa31f8cd94fe7971277839ee5425e29bf))
* **canvas:** read only mode ([#665](https://github.com/chartdb/chartdb/issues/665)) ([651fe36](https://github.com/chartdb/chartdb/commit/651fe361fce61fe0577d2593f268131e9ca359d0))
* **clone:** add areas to clone diagram ([#664](https://github.com/chartdb/chartdb/issues/664)) ([aee1713](https://github.com/chartdb/chartdb/commit/aee1713aecdd5e54228a16cbc3c4fc184661c56b))
* **dbml-editor:** add inline refs mode + fix issues with DBML syntax ([#687](https://github.com/chartdb/chartdb/issues/687)) ([fbf2fe9](https://github.com/chartdb/chartdb/commit/fbf2fe919c2168c715f8231c0246753b19635f14))
* **dbml-editor:** remove invalid fields before showing DBML + warning ([#683](https://github.com/chartdb/chartdb/issues/683)) ([5759241](https://github.com/chartdb/chartdb/commit/5759241573db204183c92599588d59f4aadaeafb))
* **ddl-import:** fix datatypes when importing via ddl ([#696](https://github.com/chartdb/chartdb/issues/696)) ([a1144bb](https://github.com/chartdb/chartdb/commit/a1144bbf761a0daedd546b5d9b92300be59e0157))
* **ddl:** inline fks ddl script ([#701](https://github.com/chartdb/chartdb/issues/701)) ([5849e45](https://github.com/chartdb/chartdb/commit/5849e4586c7c2a7cd86bd064df8916b130fc6234))
* **dependencies:** hide icon when diagram has no dependencies ([#684](https://github.com/chartdb/chartdb/issues/684)) ([547149d](https://github.com/chartdb/chartdb/commit/547149da44db6d3d1e36d619d475fe52ff83a472))
* **examples:** add loader ([#678](https://github.com/chartdb/chartdb/issues/678)) ([90a20dd](https://github.com/chartdb/chartdb/commit/90a20dd1b0277c4aee848fae5ed7a8347c5ba77d))
* **examples:** fix clone examples ([#679](https://github.com/chartdb/chartdb/issues/679)) ([1778abb](https://github.com/chartdb/chartdb/commit/1778abb683d575af244edcd9a11f8d03f903f719))
* **expanded-table:** persist expanded state across renders ([#707](https://github.com/chartdb/chartdb/issues/707)) ([54d5e96](https://github.com/chartdb/chartdb/commit/54d5e96a6db1e3abd52229a89ac503ff31885386))
* **export image:** Fix usage of advanced options accordion ([#703](https://github.com/chartdb/chartdb/issues/703)) ([0ce85cf](https://github.com/chartdb/chartdb/commit/0ce85cf76b733f441f661608278c0db3122c5074))
* **import-database:** auto detect when user try to import ddl script ([#698](https://github.com/chartdb/chartdb/issues/698)) ([5a5e64a](https://github.com/chartdb/chartdb/commit/5a5e64abef510cff28b3d8972520d0b9df29b024))
* **import-database:** remove view_definition when importing via query ([#702](https://github.com/chartdb/chartdb/issues/702)) ([481ad3c](https://github.com/chartdb/chartdb/commit/481ad3c8449f469bf2b4418e4cdcc5b5608dfd36))
* **import-json:** for broken json imports ([#697](https://github.com/chartdb/chartdb/issues/697)) ([2368e0d](https://github.com/chartdb/chartdb/commit/2368e0d2639021c4a11a8e5131d6af44fb6a47db))
* **import-json:** simplify import script for fixing invalid JSON ([#681](https://github.com/chartdb/chartdb/issues/681)) ([226e6cf](https://github.com/chartdb/chartdb/commit/226e6cf1ce4d2edcfbee6a4de7ab0bc0cfeb17fe))
* **import:** dbml and query - senetize before import ([#699](https://github.com/chartdb/chartdb/issues/699)) ([34c0a71](https://github.com/chartdb/chartdb/commit/34c0a7163f47bde7ddfaa8f044341e3c971b7e03))
* **navbar:** open diagram directly from diagram icon ([#694](https://github.com/chartdb/chartdb/issues/694)) ([7db86dc](https://github.com/chartdb/chartdb/commit/7db86dcf8c97d34b056e4b5b85a0dda0438322ea))
* **performance:** Only render visible ([#672](https://github.com/chartdb/chartdb/issues/672)) ([83c4333](https://github.com/chartdb/chartdb/commit/83c43332d497e9fc148a18b9cb4d9ecc85e44183))
* **performance:** update field only when changed ([#685](https://github.com/chartdb/chartdb/issues/685)) ([d3ddf7c](https://github.com/chartdb/chartdb/commit/d3ddf7c51eaa4b9cddb961defd52d423f39f281d))
* **postgres:** fix import of postgres fks ([#700](https://github.com/chartdb/chartdb/issues/700)) ([89e3cea](https://github.com/chartdb/chartdb/commit/89e3ceab00defaabc079e165fc90e92ca00722cf))
* **schema:** add areas to diagram schema ([#663](https://github.com/chartdb/chartdb/issues/663)) ([ecfa148](https://github.com/chartdb/chartdb/commit/ecfa14829bcb1b813c7b154b4bd59f24e3032d8f))
* **sql-script:** change ddl to be sql-script ([#710](https://github.com/chartdb/chartdb/issues/710)) ([487fb2d](https://github.com/chartdb/chartdb/commit/487fb2d5c17b70ac54aa17af9a2ac9aded6b40ba))
* **table:** enhance field focus behavior to include table hover state ([#676](https://github.com/chartdb/chartdb/issues/676)) ([19d2d0b](https://github.com/chartdb/chartdb/commit/19d2d0bddd3a464995b79e97e6caf6e652836081))
* **translations:** Add some translations for ru-RU language ([#690](https://github.com/chartdb/chartdb/issues/690)) ([97d01d7](https://github.com/chartdb/chartdb/commit/97d01d72014e473c42348c9ebcbe7a0b973d31aa))
## [1.11.0](https://github.com/chartdb/chartdb/compare/v1.10.0...v1.11.0) (2025-04-17)

45
CLA.md Normal file
View File

@@ -0,0 +1,45 @@
# ChartDB Contributors License Agreement
This Contributors License Agreement ("CLA") is entered into between the Contributor, and ChartDB, Inc. ("ChartDB"), collectively referred to as the "Parties."
## Background:
ChartDB is an open-source project aimed at providing an open-source database diagramming and visualization tool for all parties.This CLA governs the rights and contributions made by the Contributor to the ChartDB project.
## Agreement:
**Contributor Grant of License:**
By submitting code, documentation, or any other materials (collectively, "Contributions") to the ChartDB project, the Contributor grants ChartDB a perpetual, worldwide, non-exclusive, royalty-free, sublicensable license to use, modify, distribute, and otherwise exploit the Contributions, including any intellectual property rights therein, for the purposes of the ChartDB project.
**Representation of Ownership and Right to Contribute:**
The Contributor represents that they have the legal right to grant the license stated in Section 1, and that the Contributions do not infringe upon the intellectual property rights of any third party. The Contributor also represents that they have the authority to submit the Contributions on their own behalf or, if applicable, on behalf of their employer or any other entity.
**Patent Grant:**
If the Contributions include any method, process, or apparatus that is covered by a patent, the Contributor agrees to grant ChartDB a non-exclusive, worldwide, royalty-free license under any patent claims necessary to use, modify, distribute, and otherwise exploit the Contributions for the purposes of the ChartDB project.
**No Implied Warranties or Support:**
The Contributor acknowledges that the Contributions are provided "as is," without any warranties or support of any kind. ChartDB shall have no obligation to provide maintenance, updates, bug fixes, or support for the Contributions.
**Retention of Contributor Rights:**
The Contributor retains all right, title, and interest in and to their Contributions. This CLA does not restrict the Contributor from using their own Contributions for any other purpose.
**Governing Law:**
This CLA shall be governed by and construed in accordance with the laws of Delaware (DE), without regard to its conflict of laws principles.
**Entire Agreement:**
This CLA constitutes the entire agreement between the Parties with respect to the subject matter hereof and supersedes all prior and contemporaneous understandings, agreements, representations, and warranties.
**Acceptance:**
By submitting Contributions to the ChartDB project, the Contributor acknowledges and agrees to the terms and conditions of this CLA. If the Contributor is agreeing to this CLA on behalf of an entity, they represent that they have the necessary authority to bind that entity to these terms.
**Effective Date:**
This CLA is effective as of the date of the first Contribution made by the Contributor to the ChartDB project.

View File

@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
chartdb.io@gmail.com.
support@chartdb.io.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the

View File

@@ -18,7 +18,7 @@ To submit a pull request:
If you find a bug, check [GitHub issues](https://github.com/chartdb/chartdb/issues) to see if its already reported. If not, feel free to [report it](https://github.com/chartdb/chartdb/issues/new?labels=bug).
For questions about using ChartDB, reach out to us via Email (chartdb.io@gmail.com) or [Discord](https://discord.gg/QeFwyWSKwC). For feature requests, create a [new feature](https://github.com/chartdb/chartdb/issues/new?labels=enhancement).
For questions about using ChartDB, reach out to us via Email (support@chartdb.io) or [Discord](https://discord.gg/QeFwyWSKwC). For feature requests, create a [new feature](https://github.com/chartdb/chartdb/issues/new?labels=enhancement).
### Creating a Branch
@@ -35,7 +35,7 @@ By contributing, you agree that your work will be licensed under ChartDB's [lice
## Questions?
Feel free to ask in `#contributing` on [Discord](https://discord.gg/QeFwyWSKwC) if you have questions about our process, how to proceed, etc.
or [Email](chartdb.io@gmail.com)
or [Email](support@chartdb.io)
---

4
package-lock.json generated
View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 416 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 KiB

After

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 KiB

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

After

Width:  |  Height:  |  Size: 488 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

After

Width:  |  Height:  |  Size: 359 KiB

View File

@@ -5,6 +5,7 @@ import { useTheme } from '@/hooks/use-theme';
import { useMonaco } from '@monaco-editor/react';
import { useToast } from '@/components/toast/use-toast';
import { Button } from '../button/button';
import type { LucideIcon } from 'lucide-react';
import { Copy, CopyCheck } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip';
import { useTranslation } from 'react-i18next';
@@ -26,6 +27,12 @@ export const DiffEditor = lazy(() =>
type EditorType = typeof Editor;
export interface CodeSnippetAction {
label: string;
icon: LucideIcon;
onClick: () => void;
}
export interface CodeSnippetProps {
className?: string;
code: string;
@@ -35,6 +42,7 @@ export interface CodeSnippetProps {
autoScroll?: boolean;
isComplete?: boolean;
editorProps?: React.ComponentProps<EditorType>;
actions?: CodeSnippetAction[];
}
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
@@ -47,6 +55,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
autoScroll = false,
isComplete = true,
editorProps,
actions,
}) => {
const { t } = useTranslation();
const monaco = useMonaco();
@@ -119,36 +128,58 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
) : (
<Suspense fallback={<Spinner />}>
{isComplete ? (
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<TooltipTrigger
asChild
className="absolute right-1 top-1 z-10"
<div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<span>
<Button
className=" h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t(
isCopied
? 'copied'
: 'copy_to_clipboard'
)}
</TooltipContent>
</Tooltip>
<TooltipTrigger asChild>
<span>
<Button
className="h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t(
isCopied
? 'copied'
: 'copy_to_clipboard'
)}
</TooltipContent>
</Tooltip>
{actions &&
actions.length > 0 &&
actions.map((action, index) => (
<Tooltip key={index}>
<TooltipTrigger asChild>
<span>
<Button
className="h-fit p-1.5"
variant="outline"
onClick={action.onClick}
>
<action.icon
size={16}
/>
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{action.label}
</TooltipContent>
</Tooltip>
))}
</div>
) : null}
<Editor

View File

@@ -22,7 +22,7 @@ export interface DiagramIconProps
export const DiagramIcon = React.forwardRef<
React.ElementRef<typeof TooltipTrigger>,
DiagramIconProps
>(({ databaseType, databaseEdition, className, imgClassName }, ref) =>
>(({ databaseType, databaseEdition, className, imgClassName, onClick }, ref) =>
databaseEdition ? (
<Tooltip>
<TooltipTrigger className={cn('mr-1', className)} ref={ref} asChild>
@@ -30,6 +30,7 @@ export const DiagramIcon = React.forwardRef<
src={databaseEditionToImageMap[databaseEdition]}
className={cn('h-5 max-w-fit rounded-full', imgClassName)}
alt="database"
onClick={onClick}
/>
</TooltipTrigger>
<TooltipContent>
@@ -43,6 +44,7 @@ export const DiagramIcon = React.forwardRef<
src={databaseSecondaryLogoMap[databaseType]}
className={cn('h-5 max-w-fit', imgClassName)}
alt="database"
onClick={onClick}
/>
</TooltipTrigger>
<TooltipContent>

View File

@@ -345,7 +345,6 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
? [option.regex]
: undefined
}
// value={option.value}
onSelect={() =>
handleSelect(
option.value,

View File

@@ -10,6 +10,7 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
import type { DBSchema } from '@/lib/domain/db-schema';
import type { DBDependency } from '@/lib/domain/db-dependency';
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
import type { Area } from '@/lib/domain/area';
export type ChartDBEventType =
| 'add_tables'
@@ -70,6 +71,7 @@ export interface ChartDBContext {
schemas: DBSchema[];
relationships: DBRelationship[];
dependencies: DBDependency[];
areas: Area[];
currentDiagram: Diagram;
events: EventEmitter<ChartDBEvent>;
readonly?: boolean;
@@ -221,6 +223,31 @@ export interface ChartDBContext {
dependency: Partial<DBDependency>,
options?: { updateHistory: boolean }
) => Promise<void>;
// Area operations
createArea: (attributes?: Partial<Omit<Area, 'id'>>) => Promise<Area>;
addArea: (
area: Area,
options?: { updateHistory: boolean }
) => Promise<void>;
addAreas: (
areas: Area[],
options?: { updateHistory: boolean }
) => Promise<void>;
getArea: (id: string) => Area | null;
removeArea: (
id: string,
options?: { updateHistory: boolean }
) => Promise<void>;
removeAreas: (
ids: string[],
options?: { updateHistory: boolean }
) => Promise<void>;
updateArea: (
id: string,
area: Partial<Area>,
options?: { updateHistory: boolean }
) => Promise<void>;
}
export const chartDBContext = createContext<ChartDBContext>({
@@ -230,6 +257,7 @@ export const chartDBContext = createContext<ChartDBContext>({
tables: [],
relationships: [],
dependencies: [],
areas: [],
schemas: [],
filteredSchemas: [],
filterSchemas: emptyFn,
@@ -296,4 +324,13 @@ export const chartDBContext = createContext<ChartDBContext>({
removeDependencies: emptyFn,
addDependencies: emptyFn,
updateDependency: emptyFn,
// Area operations
createArea: emptyFn,
addArea: emptyFn,
addAreas: emptyFn,
getArea: emptyFn,
removeArea: emptyFn,
removeAreas: emptyFn,
updateArea: emptyFn,
});

View File

@@ -21,6 +21,7 @@ import { useLocalConfig } from '@/hooks/use-local-config';
import { defaultSchemas } from '@/lib/data/default-schemas';
import { useEventEmitter } from 'ahooks';
import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
import { storageInitialValue } from '../storage-context/storage-context';
import { useDiff } from '../diff-context/use-diff';
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
@@ -56,6 +57,7 @@ export const ChartDBProvider: React.FC<
const [dependencies, setDependencies] = useState<DBDependency[]>(
diagram?.dependencies ?? []
);
const [areas, setAreas] = useState<Area[]>(diagram?.areas ?? []);
const { events: diffEvents } = useDiff();
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
@@ -152,6 +154,7 @@ export const ChartDBProvider: React.FC<
tables,
relationships,
dependencies,
areas,
}),
[
diagramId,
@@ -161,6 +164,7 @@ export const ChartDBProvider: React.FC<
tables,
relationships,
dependencies,
areas,
diagramCreatedAt,
diagramUpdatedAt,
]
@@ -172,6 +176,7 @@ export const ChartDBProvider: React.FC<
setTables([]);
setRelationships([]);
setDependencies([]);
setAreas([]);
setDiagramUpdatedAt(updatedAt);
resetRedoStack();
@@ -182,6 +187,7 @@ export const ChartDBProvider: React.FC<
db.deleteDiagramTables(diagramId),
db.deleteDiagramRelationships(diagramId),
db.deleteDiagramDependencies(diagramId),
db.deleteDiagramAreas(diagramId),
]);
}, [db, diagramId, resetRedoStack, resetUndoStack]);
@@ -194,6 +200,7 @@ export const ChartDBProvider: React.FC<
setTables([]);
setRelationships([]);
setDependencies([]);
setAreas([]);
resetRedoStack();
resetUndoStack();
@@ -202,6 +209,7 @@ export const ChartDBProvider: React.FC<
db.deleteDiagramRelationships(diagramId),
db.deleteDiagram(diagramId),
db.deleteDiagramDependencies(diagramId),
db.deleteDiagramAreas(diagramId),
]);
}, [db, diagramId, resetRedoStack, resetUndoStack]);
@@ -1363,6 +1371,130 @@ export const ChartDBProvider: React.FC<
]
);
// Area operations
const addAreas: ChartDBContext['addAreas'] = useCallback(
async (areas: Area[], options = { updateHistory: true }) => {
setAreas((currentAreas) => [...currentAreas, ...areas]);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
...areas.map((area) => db.addArea({ diagramId, area })),
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
]);
if (options.updateHistory) {
addUndoAction({
action: 'addAreas',
redoData: { areas },
undoData: { areaIds: areas.map((a) => a.id) },
});
resetRedoStack();
}
},
[db, diagramId, setAreas, addUndoAction, resetRedoStack]
);
const addArea: ChartDBContext['addArea'] = useCallback(
async (area: Area, options = { updateHistory: true }) => {
return addAreas([area], options);
},
[addAreas]
);
const createArea: ChartDBContext['createArea'] = useCallback(
async (attributes) => {
const area: Area = {
id: generateId(),
name: `Area ${areas.length + 1}`,
x: 0,
y: 0,
width: 300,
height: 200,
color: randomColor(),
...attributes,
};
await addArea(area);
return area;
},
[areas, addArea]
);
const getArea: ChartDBContext['getArea'] = useCallback(
(id: string) => areas.find((area) => area.id === id) ?? null,
[areas]
);
const removeAreas: ChartDBContext['removeAreas'] = useCallback(
async (ids: string[], options = { updateHistory: true }) => {
const prevAreas = [
...areas.filter((area) => ids.includes(area.id)),
];
setAreas((areas) => areas.filter((area) => !ids.includes(area.id)));
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
...ids.map((id) => db.deleteArea({ diagramId, id })),
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
]);
if (prevAreas.length > 0 && options.updateHistory) {
addUndoAction({
action: 'removeAreas',
redoData: { areaIds: ids },
undoData: { areas: prevAreas },
});
resetRedoStack();
}
},
[db, diagramId, setAreas, areas, addUndoAction, resetRedoStack]
);
const removeArea: ChartDBContext['removeArea'] = useCallback(
async (id: string, options = { updateHistory: true }) => {
return removeAreas([id], options);
},
[removeAreas]
);
const updateArea: ChartDBContext['updateArea'] = useCallback(
async (
id: string,
area: Partial<Area>,
options = { updateHistory: true }
) => {
const prevArea = getArea(id);
setAreas((areas) =>
areas.map((a) => (a.id === id ? { ...a, ...area } : a))
);
const updatedAt = new Date();
setDiagramUpdatedAt(updatedAt);
await Promise.all([
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
db.updateArea({ id, attributes: area }),
]);
if (!!prevArea && options.updateHistory) {
addUndoAction({
action: 'updateArea',
redoData: { areaId: id, area },
undoData: { areaId: id, area: prevArea },
});
resetRedoStack();
}
},
[db, diagramId, setAreas, getArea, addUndoAction, resetRedoStack]
);
const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
useCallback(
async (diagram) => {
@@ -1373,6 +1505,7 @@ export const ChartDBProvider: React.FC<
setTables(diagram?.tables ?? []);
setRelationships(diagram?.relationships ?? []);
setDependencies(diagram?.dependencies ?? []);
setAreas(diagram?.areas ?? []);
setDiagramCreatedAt(diagram.createdAt);
setDiagramUpdatedAt(diagram.updatedAt);
@@ -1386,6 +1519,7 @@ export const ChartDBProvider: React.FC<
setTables,
setRelationships,
setDependencies,
setAreas,
setDiagramCreatedAt,
setDiagramUpdatedAt,
events,
@@ -1398,6 +1532,7 @@ export const ChartDBProvider: React.FC<
includeRelationships: true,
includeTables: true,
includeDependencies: true,
includeAreas: true,
});
if (diagram) {
@@ -1418,6 +1553,7 @@ export const ChartDBProvider: React.FC<
tables,
relationships,
dependencies,
areas,
currentDiagram,
schemas,
filteredSchemas,
@@ -1465,6 +1601,13 @@ export const ChartDBProvider: React.FC<
removeDependency,
removeDependencies,
updateDependency,
createArea,
addArea,
addAreas,
getArea,
removeArea,
removeAreas,
updateArea,
}}
>
{children}

View File

@@ -4,7 +4,10 @@ import type { ChartDBConfig } from '@/lib/domain/config';
export interface ConfigContext {
config?: ChartDBConfig;
updateConfig: (config: Partial<ChartDBConfig>) => Promise<void>;
updateConfig: (params: {
config?: Partial<ChartDBConfig>;
updateFn?: (config: ChartDBConfig) => ChartDBConfig;
}) => Promise<void>;
}
export const ConfigContext = createContext<ConfigContext>({

View File

@@ -19,15 +19,29 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
loadConfig();
}, [getConfig]);
const updateConfig: ConfigContext['updateConfig'] = async (
config: Partial<ChartDBConfig>
) => {
await updateDataConfig(config);
setConfig((prevConfig) =>
prevConfig
? { ...prevConfig, ...config }
: { ...{ defaultDiagramId: '' }, ...config }
);
const updateConfig: ConfigContext['updateConfig'] = async ({
config,
updateFn,
}) => {
const promise = new Promise<void>((resolve) => {
setConfig((prevConfig) => {
let baseConfig: ChartDBConfig = { defaultDiagramId: '' };
if (prevConfig) {
baseConfig = prevConfig;
}
const updatedConfig = updateFn
? updateFn(baseConfig)
: { ...baseConfig, ...config };
updateDataConfig(updatedConfig).then(() => {
resolve();
});
return updatedConfig;
});
});
return promise;
};
return (

View File

@@ -9,10 +9,13 @@ import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/i
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';
import type { CreateDiagramDialogProps } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
export interface DialogContext {
// Create diagram dialog
openCreateDiagramDialog: () => void;
openCreateDiagramDialog: (
params?: Omit<CreateDiagramDialogProps, 'dialog'>
) => void;
closeCreateDiagramDialog: () => void;
// Open diagram dialog

View File

@@ -1,6 +1,7 @@
import React, { useCallback, useState } from 'react';
import type { DialogContext } from './dialog-context';
import { dialogContext } from './dialog-context';
import type { CreateDiagramDialogProps } 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';
@@ -26,6 +27,17 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false);
const [newDiagramDialogParams, setNewDiagramDialogParams] =
useState<Omit<CreateDiagramDialogProps, 'dialog'>>();
const openNewDiagramDialogHandler: DialogContext['openCreateDiagramDialog'] =
useCallback(
(props) => {
setNewDiagramDialogParams(props);
setOpenNewDiagramDialog(true);
},
[setOpenNewDiagramDialog]
);
const [openOpenDiagramDialog, setOpenOpenDiagramDialog] = useState(false);
const [openDiagramDialogParams, setOpenDiagramDialogParams] =
useState<Omit<OpenDiagramDialogProps, 'dialog'>>();
@@ -128,7 +140,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
return (
<dialogContext.Provider
value={{
openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
openCreateDiagramDialog: openNewDiagramDialogHandler,
closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
openOpenDiagramDialog: openOpenDiagramDialogHandler,
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
@@ -161,7 +173,10 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
}}
>
{children}
<CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} />
<CreateDiagramDialog
dialog={{ open: openNewDiagramDialog }}
{...newDiagramDialogParams}
/>
<OpenDiagramDialog
dialog={{ open: openOpenDiagramDialog }}
{...openDiagramDialogParams}

View File

@@ -3,7 +3,14 @@ import { emptyFn } from '@/lib/utils';
export type ImageType = 'png' | 'jpeg' | 'svg';
export interface ExportImageContext {
exportImage: (type: ImageType, scale: number) => Promise<void>;
exportImage: (
type: ImageType,
options: {
includePatternBG: boolean;
transparent: boolean;
scale: number;
}
) => Promise<void>;
}
export const exportImageContext = createContext<ExportImageContext>({

View File

@@ -8,6 +8,7 @@ import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
import { useTheme } from '@/hooks/use-theme';
import logoDark from '@/assets/logo-dark.png';
import logoLight from '@/assets/logo-light.png';
import type { EffectiveTheme } from '../theme-context/theme-context';
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -57,8 +58,16 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
[]
);
const getBackgroundColor = useCallback(
(theme: EffectiveTheme, transparent: boolean): string => {
if (transparent) return 'transparent';
return theme === 'light' ? '#ffffff' : '#141414';
},
[]
);
const exportImage: ExportImageContext['exportImage'] = useCallback(
async (type, scale = 1) => {
async (type, { includePatternBG, transparent, scale }) => {
showLoader({
animated: false,
});
@@ -114,34 +123,37 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
defs.innerHTML = markerDefs.innerHTML;
}
const pattern = document.createElementNS(
'http://www.w3.org/2000/svg',
'pattern'
);
pattern.setAttribute('id', 'background-pattern');
pattern.setAttribute('width', String(16 * viewport.zoom));
pattern.setAttribute('height', String(16 * viewport.zoom));
pattern.setAttribute('patternUnits', 'userSpaceOnUse');
pattern.setAttribute(
'patternTransform',
`translate(${viewport.x % (16 * viewport.zoom)} ${viewport.y % (16 * viewport.zoom)})`
);
if (includePatternBG) {
const pattern = document.createElementNS(
'http://www.w3.org/2000/svg',
'pattern'
);
pattern.setAttribute('id', 'background-pattern');
pattern.setAttribute('width', String(16 * viewport.zoom));
pattern.setAttribute('height', String(16 * viewport.zoom));
pattern.setAttribute('patternUnits', 'userSpaceOnUse');
pattern.setAttribute(
'patternTransform',
`translate(${viewport.x % (16 * viewport.zoom)} ${viewport.y % (16 * viewport.zoom)})`
);
const dot = document.createElementNS(
'http://www.w3.org/2000/svg',
'circle'
);
const dot = document.createElementNS(
'http://www.w3.org/2000/svg',
'circle'
);
const dotSize = viewport.zoom * 0.5;
dot.setAttribute('cx', String(viewport.zoom));
dot.setAttribute('cy', String(viewport.zoom));
dot.setAttribute('r', String(dotSize));
const dotColor =
effectiveTheme === 'light' ? '#92939C' : '#777777';
dot.setAttribute('fill', dotColor);
const dotSize = viewport.zoom * 0.5;
dot.setAttribute('cx', String(viewport.zoom));
dot.setAttribute('cy', String(viewport.zoom));
dot.setAttribute('r', String(dotSize));
const dotColor =
effectiveTheme === 'light' ? '#92939C' : '#777777';
dot.setAttribute('fill', dotColor);
pattern.appendChild(dot);
defs.appendChild(pattern);
}
pattern.appendChild(dot);
defs.appendChild(pattern);
tempSvg.appendChild(defs);
const backgroundRect = document.createElementNS(
@@ -196,10 +208,10 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
const initialDataUrl = await imageCreateFn(
viewportElement,
{
backgroundColor:
effectiveTheme === 'light'
? '#ffffff'
: '#141414',
backgroundColor: getBackgroundColor(
effectiveTheme,
transparent
),
width: reactFlowBounds.width,
height: reactFlowBounds.height,
style: {
@@ -285,6 +297,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
}, 0);
},
[
getBackgroundColor,
downloadImage,
getViewport,
hideLoader,

View File

@@ -33,6 +33,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
removeIndex,
updateIndex,
removeRelationships,
addAreas,
removeAreas,
updateArea,
} = useChartDB();
const redoActionHandlers = useMemo(
@@ -107,6 +110,15 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
updateHistory: false,
});
},
addAreas: ({ redoData: { areas } }) => {
return addAreas(areas, { updateHistory: false });
},
removeAreas: ({ redoData: { areaIds } }) => {
return removeAreas(areaIds, { updateHistory: false });
},
updateArea: ({ redoData: { areaId, area } }) => {
return updateArea(areaId, area, { updateHistory: false });
},
}),
[
addTables,
@@ -126,6 +138,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
addDependencies,
removeDependencies,
updateDependency,
addAreas,
removeAreas,
updateArea,
]
);
@@ -215,6 +230,15 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
updateHistory: false,
});
},
addAreas: ({ undoData: { areaIds } }) => {
return removeAreas(areaIds, { updateHistory: false });
},
removeAreas: ({ undoData: { areas } }) => {
return addAreas(areas, { updateHistory: false });
},
updateArea: ({ undoData: { areaId, area } }) => {
return updateArea(areaId, area, { updateHistory: false });
},
}),
[
addTables,
@@ -234,6 +258,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
addDependencies,
removeDependencies,
updateDependency,
addAreas,
removeAreas,
updateArea,
]
);

View File

@@ -4,6 +4,7 @@ import type { DBField } from '@/lib/domain/db-field';
import type { DBIndex } from '@/lib/domain/db-index';
import type { DBRelationship } from '@/lib/domain/db-relationship';
import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
type Action = keyof ChartDBContext;
@@ -123,6 +124,24 @@ type RedoUndoActionRemoveDependencies = RedoUndoActionBase<
{ dependencies: DBDependency[] }
>;
type RedoUndoActionAddAreas = RedoUndoActionBase<
'addAreas',
{ areas: Area[] },
{ areaIds: string[] }
>;
type RedoUndoActionUpdateArea = RedoUndoActionBase<
'updateArea',
{ areaId: string; area: Partial<Area> },
{ areaId: string; area: Partial<Area> }
>;
type RedoUndoActionRemoveAreas = RedoUndoActionBase<
'removeAreas',
{ areaIds: string[] },
{ areas: Area[] }
>;
export type RedoUndoAction =
| RedoUndoActionAddTables
| RedoUndoActionRemoveTables
@@ -140,7 +159,10 @@ export type RedoUndoAction =
| RedoUndoActionRemoveRelationships
| RedoUndoActionAddDependencies
| RedoUndoActionUpdateDependency
| RedoUndoActionRemoveDependencies;
| RedoUndoActionRemoveDependencies
| RedoUndoActionAddAreas
| RedoUndoActionUpdateArea
| RedoUndoActionRemoveAreas;
export type RedoActionData<T extends Action> = Extract<
RedoUndoAction,

View File

@@ -1,7 +1,11 @@
import { emptyFn } from '@/lib/utils';
import { createContext } from 'react';
export type SidebarSection = 'tables' | 'relationships' | 'dependencies';
export type SidebarSection =
| 'tables'
| 'relationships'
| 'dependencies'
| 'areas';
export interface LayoutContext {
openedTableInSidebar: string | undefined;
@@ -16,6 +20,10 @@ export interface LayoutContext {
openDependencyFromSidebar: (dependencyId: string) => void;
closeAllDependenciesInSidebar: () => void;
openedAreaInSidebar: string | undefined;
openAreaFromSidebar: (areaId: string) => void;
closeAllAreasInSidebar: () => void;
selectedSidebarSection: SidebarSection;
selectSidebarSection: (section: SidebarSection) => void;
@@ -41,6 +49,10 @@ export const layoutContext = createContext<LayoutContext>({
openDependencyFromSidebar: emptyFn,
closeAllDependenciesInSidebar: emptyFn,
openedAreaInSidebar: undefined,
openAreaFromSidebar: emptyFn,
closeAllAreasInSidebar: emptyFn,
selectSidebarSection: emptyFn,
openTableFromSidebar: emptyFn,
closeAllTablesInSidebar: emptyFn,

View File

@@ -14,6 +14,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
React.useState<string | undefined>();
const [openedDependencyInSidebar, setOpenedDependencyInSidebar] =
React.useState<string | undefined>();
const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
string | undefined
>();
const [selectedSidebarSection, setSelectedSidebarSection] =
React.useState<SidebarSection>('tables');
const [isSidePanelShowed, setIsSidePanelShowed] =
@@ -30,6 +33,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
const closeAllDependenciesInSidebar: LayoutContext['closeAllDependenciesInSidebar'] =
() => setOpenedDependencyInSidebar('');
const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
() => setOpenedAreaInSidebar('');
const hideSidePanel: LayoutContext['hideSidePanel'] = () =>
setIsSidePanelShowed(false);
@@ -62,6 +68,14 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
setOpenedDependencyInSidebar(dependencyId);
};
const openAreaFromSidebar: LayoutContext['openAreaFromSidebar'] = (
areaId
) => {
showSidePanel();
setSelectedSidebarSection('areas');
setOpenedAreaInSidebar(areaId);
};
const openSelectSchema: LayoutContext['openSelectSchema'] = () =>
setIsSelectSchemaOpen(true);
@@ -88,6 +102,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
openedDependencyInSidebar,
openDependencyFromSidebar,
closeAllDependenciesInSidebar,
openedAreaInSidebar,
openAreaFromSidebar,
closeAllAreasInSidebar,
}}
>
{children}

View File

@@ -5,6 +5,7 @@ import type { DBRelationship } from '@/lib/domain/db-relationship';
import type { DBTable } from '@/lib/domain/db-table';
import type { ChartDBConfig } from '@/lib/domain/config';
import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
export interface StorageContext {
// Config operations
@@ -17,6 +18,7 @@ export interface StorageContext {
includeTables?: boolean;
includeRelationships?: boolean;
includeDependencies?: boolean;
includeAreas?: boolean;
}) => Promise<Diagram[]>;
getDiagram: (
id: string,
@@ -24,6 +26,7 @@ export interface StorageContext {
includeTables?: boolean;
includeRelationships?: boolean;
includeDependencies?: boolean;
includeAreas?: boolean;
}
) => Promise<Diagram | undefined>;
updateDiagram: (params: {
@@ -86,6 +89,20 @@ export interface StorageContext {
}) => Promise<void>;
listDependencies: (diagramId: string) => Promise<DBDependency[]>;
deleteDiagramDependencies: (diagramId: string) => Promise<void>;
// Area operations
addArea: (params: { diagramId: string; area: Area }) => Promise<void>;
getArea: (params: {
diagramId: string;
id: string;
}) => Promise<Area | undefined>;
updateArea: (params: {
id: string;
attributes: Partial<Area>;
}) => Promise<void>;
deleteArea: (params: { diagramId: string; id: string }) => Promise<void>;
listAreas: (diagramId: string) => Promise<Area[]>;
deleteDiagramAreas: (diagramId: string) => Promise<void>;
}
export const storageInitialValue: StorageContext = {
@@ -119,6 +136,13 @@ export const storageInitialValue: StorageContext = {
deleteDependency: emptyFn,
listDependencies: emptyFn,
deleteDiagramDependencies: emptyFn,
addArea: emptyFn,
getArea: emptyFn,
updateArea: emptyFn,
deleteArea: emptyFn,
listAreas: emptyFn,
deleteDiagramAreas: emptyFn,
};
export const storageContext =

View File

@@ -8,6 +8,7 @@ import type { DBRelationship } from '@/lib/domain/db-relationship';
import { determineCardinalities } from '@/lib/domain/db-relationship';
import type { ChartDBConfig } from '@/lib/domain/config';
import type { DBDependency } from '@/lib/domain/db-dependency';
import type { Area } from '@/lib/domain/area';
export const StorageProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -29,6 +30,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
DBDependency & { diagramId: string },
'id' // primary key "id" (for the typings only)
>;
areas: EntityTable<
Area & { diagramId: string },
'id' // primary key "id" (for the typings only)
>;
config: EntityTable<
ChartDBConfig & { id: number },
'id' // primary key "id" (for the typings only)
@@ -148,6 +153,19 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
})
);
db.version(10).stores({
diagrams:
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
db_tables:
'++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment, isView, isMaterializedView, order',
db_relationships:
'++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
db_dependencies:
'++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt',
areas: '++id, diagramId, name, x, y, width, height, color',
config: '++id, defaultDiagramId',
});
db.on('ready', async () => {
const config = await getConfig();
@@ -209,6 +227,11 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
)
);
const areas = diagram.areas ?? [];
promises.push(
...areas.map((area) => addArea({ diagramId: diagram.id, area }))
);
await Promise.all(promises);
};
@@ -217,10 +240,12 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
includeTables?: boolean;
includeRelationships?: boolean;
includeDependencies?: boolean;
includeAreas?: boolean;
} = {
includeRelationships: false,
includeTables: false,
includeDependencies: false,
includeAreas: false,
}
): Promise<Diagram[]> => {
let diagrams = await db.diagrams.toArray();
@@ -252,6 +277,15 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
);
}
if (options.includeAreas) {
diagrams = await Promise.all(
diagrams.map(async (diagram) => {
diagram.areas = await listAreas(diagram.id);
return diagram;
})
);
}
return diagrams;
};
@@ -261,10 +295,12 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
includeTables?: boolean;
includeRelationships?: boolean;
includeDependencies?: boolean;
includeAreas?: boolean;
} = {
includeRelationships: false,
includeTables: false,
includeDependencies: false,
includeAreas: false,
}
): Promise<Diagram | undefined> => {
const diagram = await db.diagrams.get(id);
@@ -285,6 +321,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
diagram.dependencies = await listDependencies(id);
}
if (options.includeAreas) {
diagram.areas = await listAreas(id);
}
return diagram;
};
@@ -323,6 +363,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
db.db_tables.where('diagramId').equals(id).delete(),
db.db_relationships.where('diagramId').equals(id).delete(),
db.db_dependencies.where('diagramId').equals(id).delete(),
db.areas.where('diagramId').equals(id).delete(),
]);
};
@@ -504,6 +545,41 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
.delete();
};
const addArea: StorageContext['addArea'] = async ({ area, diagramId }) => {
await db.areas.add({
...area,
diagramId,
});
};
const getArea: StorageContext['getArea'] = async ({ diagramId, id }) => {
return await db.areas.get({ id, diagramId });
};
const updateArea: StorageContext['updateArea'] = async ({
id,
attributes,
}) => {
await db.areas.update(id, attributes);
};
const deleteArea: StorageContext['deleteArea'] = async ({
diagramId,
id,
}) => {
await db.areas.where({ id, diagramId }).delete();
};
const listAreas: StorageContext['listAreas'] = async (diagramId) => {
return await db.areas.where('diagramId').equals(diagramId).toArray();
};
const deleteDiagramAreas: StorageContext['deleteDiagramAreas'] = async (
diagramId
) => {
await db.areas.where('diagramId').equals(diagramId).delete();
};
return (
<storageContext.Provider
value={{
@@ -533,6 +609,12 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
deleteDependency,
listDependencies,
deleteDiagramDependencies,
addArea,
getArea,
updateArea,
deleteArea,
listAreas,
deleteDiagramAreas,
}}
>
{children}

View File

@@ -1,4 +1,10 @@
import React, { Suspense, useCallback, useEffect, useState } from 'react';
import React, {
Suspense,
useCallback,
useEffect,
useState,
useRef,
} from 'react';
import { Button } from '@/components/button/button';
import {
DialogClose,
@@ -32,7 +38,50 @@ import { parseSQLError } from '@/lib/data/sql-import';
import type { editor } from 'monaco-editor';
const errorScriptOutputMessage =
'Invalid JSON. Please correct it or contact us at chartdb.io@gmail.com for help.';
'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.';
// Helper to detect if content is likely SQL DDL or JSON
const detectContentType = (content: string): 'query' | 'ddl' | null => {
if (!content || content.trim().length === 0) return null;
// Common SQL DDL keywords
const ddlKeywords = [
'CREATE TABLE',
'ALTER TABLE',
'DROP TABLE',
'CREATE INDEX',
'CREATE VIEW',
'CREATE PROCEDURE',
'CREATE FUNCTION',
'CREATE SCHEMA',
'CREATE DATABASE',
];
const upperContent = content.toUpperCase();
// Check for SQL DDL patterns
const hasDDLKeywords = ddlKeywords.some((keyword) =>
upperContent.includes(keyword)
);
if (hasDDLKeywords) return 'ddl';
// Check if it looks like JSON
try {
// Just check structure, don't need full parse for detection
if (
(content.trim().startsWith('{') && content.trim().endsWith('}')) ||
(content.trim().startsWith('[') && content.trim().endsWith(']'))
) {
return 'query';
}
} catch (error) {
// Not valid JSON, might be partial
console.error('Error detecting content type:', error);
}
// If we can't confidently detect, return null
return null;
};
export interface ImportDatabaseProps {
goBack?: () => void;
@@ -67,6 +116,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
}) => {
const { effectiveTheme } = useTheme();
const [errorMessage, setErrorMessage] = useState('');
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const { t } = useTranslation();
const { isSm: isDesktop } = useBreakpoint('sm');
@@ -134,6 +184,16 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
}
}, [errorMessage.length, onImport, scriptResult]);
const formatEditor = useCallback(() => {
if (editorRef.current) {
setTimeout(() => {
editorRef.current
?.getAction('editor.action.formatDocument')
?.run();
}, 50);
}
}, []);
const handleInputChange: OnChange = useCallback(
(inputValue) => {
setScriptResult(inputValue ?? '');
@@ -156,22 +216,46 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
if (isStringMetadataJson(fixedJson)) {
setScriptResult(fixedJson);
setErrorMessage('');
formatEditor();
} else {
setScriptResult(fixedJson);
setErrorMessage(errorScriptOutputMessage);
formatEditor();
}
setShowCheckJsonButton(false);
setIsCheckingJson(false);
}, [scriptResult, setScriptResult]);
}, [scriptResult, setScriptResult, formatEditor]);
const detectAndSetImportMethod = useCallback(() => {
const content = editorRef.current?.getValue();
if (content && content.trim()) {
const detectedType = detectContentType(content);
if (detectedType && detectedType !== importMethod) {
setImportMethod(detectedType);
}
}
}, [setImportMethod, importMethod]);
const [editorDidMount, setEditorDidMount] = useState(false);
useEffect(() => {
if (editorRef.current && editorDidMount) {
editorRef.current.onDidPaste(() => {
setTimeout(() => {
editorRef.current
?.getAction('editor.action.formatDocument')
?.run();
}, 0);
setTimeout(detectAndSetImportMethod, 0);
});
}
}, [detectAndSetImportMethod, editorDidMount]);
const handleEditorDidMount = useCallback(
(editor: editor.IStandaloneCodeEditor) => {
editor.onDidPaste(() => {
setTimeout(() => {
editor.getAction('editor.action.formatDocument')?.run();
}, 0);
});
editorRef.current = editor;
setEditorDidMount(true);
},
[]
);
@@ -214,7 +298,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
<div className="w-full text-center text-xs text-muted-foreground">
{importMethod === 'query'
? 'Smart Query Output'
: 'SQL DDL'}
: 'SQL Script'}
</div>
<div className="flex-1 overflow-hidden">
<Suspense fallback={<Spinner />}>
@@ -259,30 +343,9 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
</Suspense>
</div>
{showCheckJsonButton || errorMessage ? (
{errorMessage ? (
<div className="mt-2 flex shrink-0 items-center gap-2">
{showCheckJsonButton ? (
<Button
type="button"
variant="outline"
size="sm"
onClick={handleCheckJson}
disabled={isCheckingJson}
className="h-7"
>
{isCheckingJson ? (
<Spinner size="small" />
) : (
t(
'new_diagram_dialog.import_database.check_script_result'
)
)}
</Button>
) : (
<p className="text-xs text-red-700">
{errorMessage}
</p>
)}
<p className="text-xs text-red-700">{errorMessage}</p>
</div>
) : null}
</div>
@@ -294,10 +357,6 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
effectiveTheme,
debouncedHandleInputChange,
handleEditorDidMount,
showCheckJsonButton,
isCheckingJson,
handleCheckJson,
t,
]
);
@@ -368,7 +427,22 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
</DialogClose>
)}
{keepDialogAfterImport ? (
{showCheckJsonButton ? (
<Button
type="button"
variant="default"
onClick={handleCheckJson}
disabled={isCheckingJson}
>
{isCheckingJson ? (
<Spinner size="small" />
) : (
t(
'new_diagram_dialog.import_database.check_script_result'
)
)}
</Button>
) : keepDialogAfterImport ? (
<Button
type="button"
variant="default"
@@ -386,7 +460,6 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
type="button"
variant="default"
disabled={
showCheckJsonButton ||
scriptResult.trim().length === 0 ||
errorMessage.length > 0
}
@@ -417,6 +490,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
errorMessage.length,
scriptResult,
showCheckJsonButton,
isCheckingJson,
handleCheckJson,
goBack,
t,
]);

View File

@@ -151,7 +151,7 @@ export const InstructionsSection: React.FC<InstructionsSectionProps> = ({
<Avatar className="size-4 rounded-none">
<Code size={16} />
</Avatar>
DDL
SQL Script
</ToggleGroupItem>
</ToggleGroup>
</div>

View File

@@ -6,7 +6,7 @@ import { SSMSInfo } from './ssms-info/ssms-info';
import { useTranslation } from 'react-i18next';
import { Tabs, TabsList, TabsTrigger } from '@/components/tabs/tabs';
import type { DatabaseClient } from '@/lib/domain/database-clients';
import { minimizeQuery } from '@/lib/data/import-metadata/scripts/minimize-script';
import { minimizeQuery } from '@/lib/data/import-metadata/utils';
import {
databaseClientToLabelMap,
databaseTypeToClientsMap,

View File

@@ -91,7 +91,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
}
await addDiagram({ diagram });
await updateConfig({ defaultDiagramId: diagram.id });
await updateConfig({ config: { defaultDiagramId: diagram.id } });
closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`);
}, [
@@ -120,7 +120,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
};
await addDiagram({ diagram });
await updateConfig({ defaultDiagramId: diagram.id });
await updateConfig({ config: { defaultDiagramId: diagram.id } });
closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`);
setTimeout(

View File

@@ -16,11 +16,20 @@ import type { BaseDialogProps } from '../common/base-dialog-props';
import { useTranslation } from 'react-i18next';
import type { ImageType } from '@/context/export-image-context/export-image-context';
import { useExportImage } from '@/hooks/use-export-image';
import { Checkbox } from '@/components/checkbox/checkbox';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/accordion/accordion';
export interface ExportImageDialogProps extends BaseDialogProps {
format: ImageType;
}
const DEFAULT_INCLUDE_PATTERN_BG = true;
const DEFAULT_TRANSPARENT = false;
const DEFAULT_SCALE = '2';
export const ExportImageDialog: React.FC<ExportImageDialogProps> = ({
dialog,
@@ -28,17 +37,28 @@ export const ExportImageDialog: React.FC<ExportImageDialogProps> = ({
}) => {
const { t } = useTranslation();
const [scale, setScale] = useState<string>(DEFAULT_SCALE);
const [includePatternBG, setIncludePatternBG] = useState<boolean>(
DEFAULT_INCLUDE_PATTERN_BG
);
const [transparent, setTransparent] =
useState<boolean>(DEFAULT_TRANSPARENT);
const { exportImage } = useExportImage();
useEffect(() => {
if (!dialog.open) return;
setScale(DEFAULT_SCALE);
setIncludePatternBG(DEFAULT_INCLUDE_PATTERN_BG);
setTransparent(DEFAULT_TRANSPARENT);
}, [dialog.open]);
const { closeExportImageDialog } = useDialog();
const handleExport = useCallback(() => {
exportImage(format, Number(scale));
}, [exportImage, format, scale]);
exportImage(format, {
transparent,
includePatternBG,
scale: Number(scale),
});
}, [exportImage, format, includePatternBG, transparent, scale]);
const scaleOptions: SelectBoxOption[] = useMemo(
() =>
@@ -65,15 +85,79 @@ export const ExportImageDialog: React.FC<ExportImageDialogProps> = ({
{t('export_image_dialog.description')}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-1">
<div className="grid w-full items-center gap-4">
<SelectBox
options={scaleOptions}
multiple={false}
value={scale}
onChange={(value) => setScale(value as string)}
/>
</div>
<div className="flex flex-col gap-4 py-1">
<SelectBox
options={scaleOptions}
multiple={false}
value={scale}
onChange={(value) => setScale(value as string)}
/>
<Accordion type="single" collapsible className="w-full">
<AccordionItem value="settings" className="border-0">
<AccordionTrigger
className="py-1.5"
iconPosition="right"
>
{t('export_image_dialog.advanced_options')}
</AccordionTrigger>
<AccordionContent>
<div className="flex flex-col gap-3 py-2">
<div className="flex items-start gap-3">
<Checkbox
id="pattern-checkbox"
className="mt-1 data-[state=checked]:border-pink-600 data-[state=checked]:bg-pink-600 data-[state=checked]:text-white"
checked={includePatternBG}
onCheckedChange={(value) =>
setIncludePatternBG(
value as boolean
)
}
/>
<div className="flex flex-col">
<label
htmlFor="pattern-checkbox"
className="cursor-pointer font-medium"
>
{t(
'export_image_dialog.pattern'
)}
</label>
<span className="text-sm text-muted-foreground">
{t(
'export_image_dialog.pattern_description'
)}
</span>
</div>
</div>
<div className="flex items-start gap-3">
<Checkbox
id="transparent-checkbox"
className="mt-1 data-[state=checked]:border-pink-600 data-[state=checked]:bg-pink-600 data-[state=checked]:text-white"
checked={transparent}
onCheckedChange={(value) =>
setTransparent(value as boolean)
}
/>
<div className="flex flex-col">
<label
htmlFor="transparent-checkbox"
className="cursor-pointer font-medium"
>
{t(
'export_image_dialog.transparent'
)}
</label>
<span className="text-sm text-muted-foreground">
{t(
'export_image_dialog.transparent_description'
)}
</span>
</div>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<DialogFooter className="flex gap-1 md:justify-between">
<DialogClose asChild>

View File

@@ -140,7 +140,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
components={[
<a
key={0}
href="mailto:chartdb.io@gmail.com"
href="mailto:support@chartdb.io"
target="_blank"
className="text-pink-600 hover:underline"
rel="noreferrer"

View File

@@ -23,7 +23,7 @@ import { useTranslation } from 'react-i18next';
import { Editor } from '@/components/code-snippet/code-snippet';
import { useTheme } from '@/hooks/use-theme';
import { AlertCircle } from 'lucide-react';
import { importDBMLToDiagram } from '@/lib/dbml-import';
import { importDBMLToDiagram, sanitizeDBML } from '@/lib/dbml-import';
import { useChartDB } from '@/hooks/use-chartdb';
import { Parser } from '@dbml/core';
import { useCanvas } from '@/hooks/use-canvas';
@@ -189,8 +189,9 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
if (!content.trim()) return;
try {
const sanitizedContent = sanitizeDBML(content);
const parser = new Parser();
parser.parse(content, 'dbml');
parser.parse(sanitizedContent, 'dbml');
} catch (e) {
const parsedError = parseDBMLError(e);
if (parsedError) {
@@ -241,7 +242,9 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
if (!dbmlContent.trim() || errorMessage) return;
try {
const importedDiagram = await importDBMLToDiagram(dbmlContent);
// Sanitize DBML content before importing
const sanitizedContent = sanitizeDBML(dbmlContent);
const importedDiagram = await importDBMLToDiagram(sanitizedContent);
const tableIdsToRemove = tables
.filter((table) =>
importedDiagram.tables?.some(
@@ -330,7 +333,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
}}
>
<DialogContent
className="flex h-[80vh] max-h-screen flex-col"
className="flex h-[80vh] max-h-screen w-full flex-col md:max-w-[900px]"
showClose
>
<DialogHeader>

View File

@@ -65,7 +65,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
const openDiagram = useCallback(
(diagramId: string) => {
if (diagramId) {
updateConfig({ defaultDiagramId: diagramId });
updateConfig({ config: { defaultDiagramId: diagramId } });
navigate(`/diagrams/${diagramId}`);
}
},

View File

@@ -210,6 +210,27 @@ export const ar: LanguageTranslation = {
description: 'إنشاء اعتماد للبدء',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -236,7 +257,7 @@ export const ar: LanguageTranslation = {
title: 'إسترد قاعدة بياناتك',
database_edition: ':إصدار قاعدة البيانات',
step_1: ':قم بتشغيل هذا البرنامج النصي في قاعدة بياناتك',
step_2: ':إلصق نتيجة البرنامج النصي هنا',
step_2: ':إلصق نتيجة البرنامج النصي هنا',
script_results_placeholder: '...نتيجة البرنامج النصي هنا',
ssms_instructions: {
button_text: 'SSMS تعليمات',
@@ -330,6 +351,12 @@ export const ar: LanguageTranslation = {
scale_4x: '4x',
cancel: 'إلغاء',
export: 'تصدير',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -362,7 +389,7 @@ export const ar: LanguageTranslation = {
error: {
title: 'حدث خطأ أثناء التصدير',
description:
'chartdb.io@gmail.com حدث خطأ ما. هل تحتاج إلى مساعدة؟',
'support@chartdb.io حدث خطأ ما. هل تحتاج إلى مساعدة؟',
},
},
import_diagram_dialog: {
@@ -373,7 +400,7 @@ export const ar: LanguageTranslation = {
error: {
title: 'حدث خطأ أثناء الاستيراد',
description:
'chartdb.io@gmail.com و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
'support@chartdb.io و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
},
},
import_dbml_dialog: {
@@ -400,6 +427,8 @@ export const ar: LanguageTranslation = {
canvas_context_menu: {
new_table: 'جدول جديد',
new_relationship: 'علاقة جديدة',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -211,6 +211,27 @@ export const bn: LanguageTranslation = {
description: 'এই অংশে কোনো নির্ভরতা উপলব্ধ নেই।',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -237,7 +258,7 @@ export const bn: LanguageTranslation = {
title: 'আপনার ডাটাবেস আমদানি করুন',
database_edition: 'ডাটাবেস সংস্করণ:',
step_1: 'আপনার ডাটাবেসে এই স্ক্রিপ্ট চালান:',
step_2: 'স্ক্রিপ্টের ফলাফল এখানে পেস্ট করুন:',
step_2: 'স্ক্রিপ্টের ফলাফল এখানে পেস্ট করুন',
script_results_placeholder: 'স্ক্রিপ্টের ফলাফল এখানে...',
ssms_instructions: {
button_text: 'SSMS নির্দেশনা',
@@ -331,6 +352,12 @@ export const bn: LanguageTranslation = {
scale_4x: '4x',
cancel: 'বাতিল করুন',
export: 'রপ্তানি করুন',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -365,7 +392,7 @@ export const bn: LanguageTranslation = {
error: {
title: 'চিত্র রপ্তানিতে ত্রুটি',
description:
'কিছু ভুল হয়েছে। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
'কিছু ভুল হয়েছে। সাহায্যের প্রয়োজন? support@chartdb.io-এ যোগাযোগ করুন।',
},
},
@@ -377,7 +404,7 @@ export const bn: LanguageTranslation = {
error: {
title: 'চিত্র আমদানিতে ত্রুটি',
description:
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? support@chartdb.io-এ যোগাযোগ করুন।',
},
},
// TODO: Translate
@@ -404,6 +431,8 @@ export const bn: LanguageTranslation = {
canvas_context_menu: {
new_table: 'নতুন টেবিল',
new_relationship: 'নতুন সম্পর্ক',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -213,6 +213,27 @@ export const de: LanguageTranslation = {
description: 'Erstellen Sie eine Ansicht, um zu beginnen',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -239,7 +260,7 @@ export const de: LanguageTranslation = {
title: 'Datenbank importieren',
database_edition: 'Datenbank Edition:',
step_1: 'Führen Sie dieses Skript in Ihrer Datenbank aus:',
step_2: 'Fügen Sie das Skriptergebnis hier ein:',
step_2: 'Fügen Sie das Skriptergebnis hier ein',
script_results_placeholder: 'Skriptergebnisse hier...',
ssms_instructions: {
button_text: 'SSMS Anweisungen',
@@ -334,6 +355,12 @@ export const de: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Abbrechen',
export: 'Exportieren',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -368,7 +395,7 @@ export const de: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -380,7 +407,7 @@ export const de: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -407,6 +434,8 @@ export const de: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Neue Tabelle',
new_relationship: 'Neue Beziehung',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -206,6 +206,26 @@ export const en = {
description: 'Create a view to get started',
},
},
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -232,7 +252,7 @@ export const en = {
title: 'Import your Database',
database_edition: 'Database Edition:',
step_1: 'Run this script in your database:',
step_2: 'Paste the script result into this modal.',
step_2: 'Paste the script result into this modal',
script_results_placeholder: 'Script results here...',
ssms_instructions: {
button_text: 'SSMS Instructions',
@@ -326,6 +346,11 @@ export const en = {
scale_4x: '4x',
cancel: 'Cancel',
export: 'Export',
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -359,7 +384,7 @@ export const en = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
@@ -371,7 +396,7 @@ export const en = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
@@ -398,6 +423,7 @@ export const en = {
canvas_context_menu: {
new_table: 'New Table',
new_relationship: 'New Relationship',
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -201,6 +201,27 @@ export const es: LanguageTranslation = {
description: 'Crea una vista para comenzar',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -227,7 +248,7 @@ export const es: LanguageTranslation = {
title: 'Importa tu Base de Datos',
database_edition: 'Edición de Base de Datos:',
step_1: 'Ejecuta este script en tu base de datos:',
step_2: 'Pega el resultado del script aquí:',
step_2: 'Pega el resultado del script aquí',
script_results_placeholder: 'Resultados del script aquí...',
ssms_instructions: {
button_text: 'Instrucciones SSMS',
@@ -323,6 +344,12 @@ export const es: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Cancelar',
export: 'Exportar',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -366,7 +393,7 @@ export const es: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -378,7 +405,7 @@ export const es: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -405,6 +432,8 @@ export const es: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nueva Tabla',
new_relationship: 'Nueva Relación',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -199,6 +199,27 @@ export const fr: LanguageTranslation = {
description: 'Créez une vue pour commencer',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -225,7 +246,7 @@ export const fr: LanguageTranslation = {
title: 'Importer votre Base de Données',
database_edition: 'Édition de la Base de Données :',
step_1: 'Exécutez ce script dans votre base de données :',
step_2: 'Collez le résultat du script ici :',
step_2: 'Collez le résultat du script ici ',
script_results_placeholder: 'Résultats du script ici...',
ssms_instructions: {
button_text: 'Instructions SSMS',
@@ -286,6 +307,12 @@ export const fr: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Annuler',
export: 'Exporter',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
multiple_schemas_alert: {
@@ -363,7 +390,7 @@ export const fr: LanguageTranslation = {
error: {
title: "Erreur lors de l'exportation du diagramme",
description:
"Une erreur s'est produite. Besoin d'aide ? chartdb.io@gmail.com",
"Une erreur s'est produite. Besoin d'aide ? support@chartdb.io",
},
},
import_diagram_dialog: {
@@ -374,7 +401,7 @@ export const fr: LanguageTranslation = {
error: {
title: "Erreur lors de l'exportation du diagramme",
description:
"Le diagramme JSON n'est pas valide. Veuillez vérifier le JSON et réessayer. Besoin d'aide ? chartdb.io@gmail.com",
"Le diagramme JSON n'est pas valide. Veuillez vérifier le JSON et réessayer. Besoin d'aide ? support@chartdb.io",
},
},
import_dbml_dialog: {
@@ -402,6 +429,8 @@ export const fr: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nouvelle Table',
new_relationship: 'Nouvelle Relation',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -212,6 +212,27 @@ export const gu: LanguageTranslation = {
description: 'આ વિભાગમાં કોઈ નિર્ભરતા ઉપલબ્ધ નથી.',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -237,7 +258,7 @@ export const gu: LanguageTranslation = {
title: 'તમારું ડેટાબેસ આયાત કરો',
database_edition: 'ડેટાબેસ આવૃત્તિ:',
step_1: 'તમારા ડેટાબેસમાં આ સ્ક્રિપ્ટ ચલાવો:',
step_2: 'સ્ક્રિપ્ટનો પરિણામ અહીં પેસ્ટ કરો:',
step_2: 'સ્ક્રિપ્ટનો પરિણામ અહીં પેસ્ટ કરો',
script_results_placeholder: 'સ્ક્રિપ્ટના પરિણામ અહીં...',
ssms_instructions: {
button_text: 'SSMS સૂચનાઓ',
@@ -331,6 +352,12 @@ export const gu: LanguageTranslation = {
scale_4x: '4x',
cancel: 'રદ કરો',
export: 'નિકાસ કરો',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -365,7 +392,7 @@ export const gu: LanguageTranslation = {
error: {
title: 'ડાયાગ્રામ નિકાસમાં ભૂલ',
description:
'કશુક તો ખોટું થયું. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
'કશુક તો ખોટું થયું. મદદ જોઈએ? support@chartdb.io પર સંપર્ક કરો.',
},
},
@@ -377,7 +404,7 @@ export const gu: LanguageTranslation = {
error: {
title: 'ડાયાગ્રામ આયાતમાં ભૂલ',
description:
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? support@chartdb.io પર સંપર્ક કરો.',
},
},
// TODO: Translate
@@ -404,6 +431,8 @@ export const gu: LanguageTranslation = {
canvas_context_menu: {
new_table: 'નવું ટેબલ',
new_relationship: 'નવો સંબંધ',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -212,6 +212,27 @@ export const hi: LanguageTranslation = {
description: 'इस अनुभाग में कोई निर्भरता उपलब्ध नहीं है।',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -238,7 +259,7 @@ export const hi: LanguageTranslation = {
title: 'अपना डेटाबेस आयात करें',
database_edition: 'डेटाबेस संस्करण:',
step_1: 'अपने डेटाबेस में यह स्क्रिप्ट चलाएँ:',
step_2: 'यहाँ स्क्रिप्ट का परिणाम पेस्ट करें:',
step_2: 'यहाँ स्क्रिप्ट का परिणाम पेस्ट करें',
script_results_placeholder: 'स्क्रिप्ट के परिणाम यहाँ...',
ssms_instructions: {
button_text: 'SSMS निर्देश',
@@ -334,6 +355,12 @@ export const hi: LanguageTranslation = {
scale_4x: '4x',
cancel: 'रद्द करें',
export: 'निर्यात करें',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -368,7 +395,7 @@ export const hi: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -380,7 +407,7 @@ export const hi: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -407,6 +434,8 @@ export const hi: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नई तालिका',
new_relationship: 'नया संबंध',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -210,6 +210,27 @@ export const id_ID: LanguageTranslation = {
description: 'Buat tampilan untuk memulai',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -236,7 +257,7 @@ export const id_ID: LanguageTranslation = {
title: 'Impor Database Anda',
database_edition: 'Edisi Database:',
step_1: 'Jalankan skrip ini di database Anda:',
step_2: 'Tempel hasil skrip di sini:',
step_2: 'Tempel hasil skrip di sini',
script_results_placeholder: 'Hasil skrip di sini...',
ssms_instructions: {
button_text: 'Instruksi SSMS',
@@ -329,6 +350,12 @@ export const id_ID: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Batal',
export: 'Ekspor',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -363,7 +390,7 @@ export const id_ID: LanguageTranslation = {
error: {
title: 'Error ekspor diagram',
description:
'Sesuatu yang salah. Butuh bantuan? chartdb.io@gmail.com',
'Sesuatu yang salah. Butuh bantuan? support@chartdb.io',
},
},
@@ -375,7 +402,7 @@ export const id_ID: LanguageTranslation = {
error: {
title: 'Error impor diagram',
description:
'Diagram JSON tidak valid. Silakan cek JSON dan coba lagi. Butuh bantuan? chartdb.io@gmail.com',
'Diagram JSON tidak valid. Silakan cek JSON dan coba lagi. Butuh bantuan? support@chartdb.io',
},
},
// TODO: Translate
@@ -403,6 +430,8 @@ export const id_ID: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Tabel Baru',
new_relationship: 'Hubungan Baru',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -216,6 +216,27 @@ export const ja: LanguageTranslation = {
description: 'Create a view to get started',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -242,7 +263,7 @@ export const ja: LanguageTranslation = {
title: 'データベースをインポート',
database_edition: 'データベースエディション:',
step_1: 'このスクリプトをデータベースで実行してください:',
step_2: 'ここにスクリプトの結果を貼り付けてください:',
step_2: 'ここにスクリプトの結果を貼り付けてください',
script_results_placeholder: 'ここにスクリプトの結果...',
ssms_instructions: {
button_text: 'SSMSの手順',
@@ -338,6 +359,12 @@ export const ja: LanguageTranslation = {
scale_4x: '4x',
cancel: 'キャンセル',
export: 'エクスポート',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -372,7 +399,7 @@ export const ja: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -384,7 +411,7 @@ export const ja: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -411,6 +438,8 @@ export const ja: LanguageTranslation = {
canvas_context_menu: {
new_table: '新しいテーブル',
new_relationship: '新しいリレーションシップ',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -210,6 +210,27 @@ export const ko_KR: LanguageTranslation = {
description: '뷰 테이블을 만들어 시작하세요.',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -236,7 +257,7 @@ export const ko_KR: LanguageTranslation = {
title: '당신의 데이터베이스를 가져오세요',
database_edition: '데이터베이스 세부 종류:',
step_1: '데이터베이스에서 아래의 SQL을 실행해주세요:',
step_2: '이곳에 결과를 붙여넣어주세요:',
step_2: '이곳에 결과를 붙여넣어주세요',
script_results_placeholder: '이곳에 스크립트 결과를 입력...',
ssms_instructions: {
button_text: 'SSMS을 사용하시는 경우',
@@ -329,6 +350,12 @@ export const ko_KR: LanguageTranslation = {
scale_4x: '4x',
cancel: '취소',
export: '내보내기',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -362,7 +389,7 @@ export const ko_KR: LanguageTranslation = {
error: {
title: '다이어그램 내보내기 오류',
description:
'무언가 문제가 발생하였습니다. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
'무언가 문제가 발생하였습니다. 도움이 필요하신 경우 support@chartdb.io으로 연락해주세요.',
},
},
import_diagram_dialog: {
@@ -373,7 +400,7 @@ export const ko_KR: LanguageTranslation = {
error: {
title: '다이어그램 가져오기 오류',
description:
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 support@chartdb.io으로 연락해주세요.',
},
},
// TODO: Translate
@@ -400,6 +427,8 @@ export const ko_KR: LanguageTranslation = {
canvas_context_menu: {
new_table: '새 테이블',
new_relationship: '새 연관관계',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -215,6 +215,27 @@ export const mr: LanguageTranslation = {
description: 'सुरू करण्यासाठी एक दृश्य तयार करा',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -241,7 +262,7 @@ export const mr: LanguageTranslation = {
title: 'तुमचा डेटाबेस आयात करा',
database_edition: 'डेटाबेस संस्करण:',
step_1: 'तुमच्या डेटाबेसमध्ये हा स्क्रिप्ट चालवा:',
step_2: 'स्क्रिप्टचा परिणाम येथे पेस्ट करा:',
step_2: 'स्क्रिप्टचा परिणाम येथे पेस्ट करा',
script_results_placeholder: 'स्क्रिप्ट परिणाम येथे...',
ssms_instructions: {
button_text: 'SSMS सूचना',
@@ -337,6 +358,12 @@ export const mr: LanguageTranslation = {
scale_4x: '4x',
cancel: 'रद्द करा',
export: 'निर्यात करा',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -372,7 +399,7 @@ export const mr: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
@@ -385,7 +412,7 @@ export const mr: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -413,6 +440,8 @@ export const mr: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नवीन टेबल',
new_relationship: 'नवीन रिलेशनशिप',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -212,6 +212,27 @@ export const ne: LanguageTranslation = {
'डिपेन्डेन्सीहरू देखाउनका लागि एक व्यू बनाउनुहोस्',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -239,7 +260,7 @@ export const ne: LanguageTranslation = {
title: 'तपाईंको डाटाबेस आयात गर्नुहोस्',
database_edition: 'डाटाबेस संस्करण:',
step_1: 'तपाईंको डाटाबेसमा यो स्क्रिप्ट चलाउनुहोस्:',
step_2: 'यो स्क्रिप्ट परिणाम यहाँ पेस्ट गर्नुहोस्:',
step_2: 'यो स्क्रिप्ट परिणाम यहाँ पेस्ट गर्नुहोस्',
script_results_placeholder: 'स्क्रिप्ट परिणाम यहाँ...',
ssms_instructions: {
button_text: 'SSMS निर्देशन',
@@ -334,6 +355,12 @@ export const ne: LanguageTranslation = {
scale_4x: '४x',
cancel: 'रद्द गर्नुहोस्',
export: 'निर्यात गर्नुहोस्',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -367,7 +394,7 @@ export const ne: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
@@ -379,7 +406,7 @@ export const ne: LanguageTranslation = {
error: {
title: 'डायाग्राम आयात गर्दा समस्या आयो',
description:
'डायाग्राम JSON अमान्य छ। कृपया JSON जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्। मद्दत चाहिन्छ? chartdb.io@gmail.com मा सम्पर्क गर्नुहोस्',
'डायाग्राम JSON अमान्य छ। कृपया JSON जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्। मद्दत चाहिन्छ? support@chartdb.io मा सम्पर्क गर्नुहोस्',
},
},
// TODO: Translate
@@ -407,6 +434,8 @@ export const ne: LanguageTranslation = {
canvas_context_menu: {
new_table: 'नयाँ तालिका',
new_relationship: 'नयाँ सम्बन्ध',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -211,6 +211,27 @@ export const pt_BR: LanguageTranslation = {
description: 'Crie uma visualização para começar',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -237,7 +258,7 @@ export const pt_BR: LanguageTranslation = {
title: 'Importe seu Banco de Dados',
database_edition: 'Edição do Banco de Dados:',
step_1: 'Execute este script no seu banco de dados:',
step_2: 'Cole o resultado do script aqui:',
step_2: 'Cole o resultado do script aqui',
script_results_placeholder: 'Resultados do script aqui...',
ssms_instructions: {
button_text: 'Instruções do SSMS',
@@ -332,6 +353,12 @@ export const pt_BR: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Cancelar',
export: 'Exportar',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -366,7 +393,7 @@ export const pt_BR: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -378,7 +405,7 @@ export const pt_BR: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -405,6 +432,8 @@ export const pt_BR: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Nova Tabela',
new_relationship: 'Novo Relacionamento',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -24,21 +24,19 @@ export const ru: LanguageTranslation = {
view: 'Вид',
show_sidebar: 'Показать боковую панель',
hide_sidebar: 'Скрыть боковую панель',
hide_cardinality: 'Скрыть множественность связи',
show_cardinality: 'Показать множественность связи',
hide_cardinality: 'Скрыть виды связи',
show_cardinality: 'Показать виды связи',
zoom_on_scroll: 'Увеличение при прокрутке',
theme: 'Тема',
show_dependencies: 'Показать зависимости',
hide_dependencies: 'Скрыть зависимости',
// TODO: Translate
show_minimap: 'Show Mini Map',
hide_minimap: 'Hide Mini Map',
show_minimap: 'Показать мини-карту',
hide_minimap: 'Скрыть мини-карту',
},
// TODO: Translate
backup: {
backup: 'Backup',
export_diagram: 'Export Diagram',
restore_diagram: 'Restore Diagram',
backup: 'Бэкап',
export_diagram: 'Экспорт диаграммы',
restore_diagram: 'Восстановить диаграмму',
},
help: {
help: 'Помощь',
@@ -123,17 +121,17 @@ export const ru: LanguageTranslation = {
add_table: 'Добавить таблицу',
filter: 'Фильтр',
collapse: 'Свернуть все',
// TODO: Translate
clear: 'Clear Filter',
no_results: 'No tables found matching your filter.',
// TODO: Translate
show_list: 'Show Table List',
show_dbml: 'Show DBML Editor',
clear: 'Очистить фильтр',
no_results:
'Таблицы не найдены, соответствующие вашему фильтру.',
show_list: 'Переключиться на список таблиц',
show_dbml: 'Переключиться на редактор DBML',
table: {
fields: 'Поля',
nullable: 'Может содержать NULL?',
primary_key: 'Первичный ключ,',
nullable: 'Может быть NULL?',
primary_key: 'Первичный ключ',
indexes: 'Индексы',
comments: 'Комментарии',
no_comments: 'Нет комментария',
@@ -149,8 +147,7 @@ export const ru: LanguageTranslation = {
comments: 'Комментарии',
no_comments: 'Нет комментария',
delete_field: 'Удалить поле',
// TODO: Translate
character_length: 'Max Length',
character_length: 'Макс. длина',
},
index_actions: {
title: 'Атрибуты индекса',
@@ -163,7 +160,7 @@ export const ru: LanguageTranslation = {
change_schema: 'Изменить схему',
add_field: 'Добавить поле',
add_index: 'Добавить индекс',
duplicate_table: 'Duplicate Table', // TODO: Translate
duplicate_table: 'Создать копию',
delete_table: 'Удалить таблицу',
},
},
@@ -180,7 +177,7 @@ export const ru: LanguageTranslation = {
relationship: {
primary: 'Основная таблица',
foreign: 'Справочная таблица',
cardinality: 'Тип множественности связи',
cardinality: 'Тип множественной связи',
delete_relationship: 'Удалить',
relationship_actions: {
title: 'Действия',
@@ -210,6 +207,28 @@ export const ru: LanguageTranslation = {
description: 'Создайте представление, чтобы начать',
},
},
areas_section: {
areas: 'Области',
add_area: 'Добавить область',
filter: 'Фильтр',
clear: 'Очистить фильтр',
no_results:
'Области не найдены, соответствующие вашему фильтру.',
area: {
area_actions: {
title: 'Действия',
edit_name: 'Изменить название',
delete_area: 'Удалить область',
},
},
empty_state: {
title: 'Нет областей',
description: 'Создайте область, чтобы начать',
},
},
},
toolbar: {
@@ -236,7 +255,7 @@ export const ru: LanguageTranslation = {
title: 'Импортируйте свою базу данных',
database_edition: 'Версия базы данных:',
step_1: 'Запустите этот скрипт в своей базе данных:',
step_2: 'Вставьте вывод скрипта сюда:',
step_2: 'Вставьте вывод скрипта сюда',
script_results_placeholder: 'Вывод скрипта здесь...',
ssms_instructions: {
button_text: 'SSMS Инструкции',
@@ -331,6 +350,12 @@ export const ru: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Отменить',
export: 'Экспортировать',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -364,7 +389,7 @@ export const ru: LanguageTranslation = {
error: {
title: 'Ошибка экспортирования диаграммы',
description:
'Что-то пошло не так. Если вам нужна помощь, напишите нам: chartdb.io@gmail.com',
'Что-то пошло не так. Если вам нужна помощь, напишите нам: support@chartdb.io',
},
},
import_diagram_dialog: {
@@ -375,21 +400,22 @@ export const ru: LanguageTranslation = {
error: {
title: 'Ошибка при импорте диаграммы',
description:
'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: chartdb.io@gmail.com',
'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: support@chartdb.io',
},
},
// TODO: Translate
import_dbml_dialog: {
example_title: 'Import Example DBML',
title: 'Import DBML',
description: 'Import a database schema from DBML format.',
import: 'Import',
cancel: 'Cancel',
skip_and_empty: 'Skip & Empty',
show_example: 'Show Example',
example_title: 'Импорт DBML',
title: 'Импортировать DBML',
description: 'Импортировать схему базы данных из DBML формата.',
import: 'Импортировать',
cancel: 'Отмена',
skip_and_empty: 'Продолжить с пустой диаграммой',
show_example: 'Использовать эту схему',
error: {
title: 'Error',
description: 'Failed to parse DBML. Please check the syntax.',
title: 'Ошибка',
description:
'Ошибка парсинга DBML. Пожалуйста проверьте синтаксис.',
},
},
relationship_type: {
@@ -402,13 +428,14 @@ export const ru: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Создать таблицу',
new_relationship: 'Создать отношение',
new_area: 'Новая область',
},
table_node_context_menu: {
edit_table: 'Изменить таблицу',
duplicate_table: 'Duplicate Table', // TODO: Translate
duplicate_table: 'Создать копию',
delete_table: 'Удалить таблицу',
add_relationship: 'Add Relationship', // TODO: Translate
add_relationship: 'Добавить связь',
},
copy_to_clipboard: 'Скопировать в буфер обмена',

View File

@@ -212,6 +212,27 @@ export const te: LanguageTranslation = {
description: 'ప్రారంభించడానికి ఒక వీక్షణ సృష్టించండి',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -238,7 +259,7 @@ export const te: LanguageTranslation = {
title: 'మీ డేటాబేస్‌ను దిగుమతి చేసుకోండి',
database_edition: 'డేటాబేస్ ఎడిషన్:',
step_1: 'ఈ స్క్రిప్ట్ను మీ డేటాబేస్‌లో అమలు చేయండి:',
step_2: 'స్క్రిప్ట్ ఫలితాన్ని ఇక్కడ పేస్ట్ చేయండి:',
step_2: 'స్క్రిప్ట్ ఫలితాన్ని ఇక్కడ పేస్ట్ చేయండి',
script_results_placeholder: 'స్క్రిప్ట్ ఫలితాలు ఇక్కడ...',
ssms_instructions: {
button_text: 'SSMS సూచనల్ని చూపించు',
@@ -333,6 +354,12 @@ export const te: LanguageTranslation = {
scale_4x: '4x',
cancel: 'రద్దు',
export: 'ఎగుమతి',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -368,7 +395,7 @@ export const te: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
@@ -381,7 +408,7 @@ export const te: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -409,6 +436,8 @@ export const te: LanguageTranslation = {
canvas_context_menu: {
new_table: 'కొత్త పట్టిక',
new_relationship: 'కొత్త సంబంధం',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -211,6 +211,27 @@ export const tr: LanguageTranslation = {
description: 'Başlamak için bir görünüm oluşturun',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
zoom_in: 'Yakınlaştır',
@@ -234,7 +255,7 @@ export const tr: LanguageTranslation = {
title: 'Veritabanını İçe Aktar',
database_edition: 'Veritabanı Sürümü:',
step_1: 'Bu komut dosyasını veritabanınızda çalıştırın:',
step_2: 'Komut dosyası sonucunu buraya yapıştırın:',
step_2: 'Komut dosyası sonucunu buraya yapıştırın',
script_results_placeholder: 'Komut dosyası sonuçları burada...',
ssms_instructions: {
button_text: 'SSMS Talimatları',
@@ -325,6 +346,12 @@ export const tr: LanguageTranslation = {
scale_4x: '4x',
cancel: 'İptal',
export: 'Dışa Aktar',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
title: 'Şema Seç',
@@ -356,7 +383,7 @@ export const tr: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -368,7 +395,7 @@ export const tr: LanguageTranslation = {
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
'The diagram JSON is invalid. Please check the JSON and try again. Need help? support@chartdb.io',
},
},
// TODO: Translate
@@ -394,6 +421,8 @@ export const tr: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Yeni Tablo',
new_relationship: 'Yeni İlişki',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {
edit_table: 'Tabloyu Düzenle',

View File

@@ -209,6 +209,27 @@ export const uk: LanguageTranslation = {
description: 'Створіть подання, щоб почати',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -235,7 +256,7 @@ export const uk: LanguageTranslation = {
title: 'Імпортуйте вашу базу даних',
database_edition: 'Варіант бази даних:',
step_1: 'Запустіть цей сценарій у своїй базі даних:',
step_2: 'Вставте сюди результат сценарію:',
step_2: 'Вставте сюди результат сценарію',
script_results_placeholder: 'Результати сценарію має бути тут…',
ssms_instructions: {
button_text: 'SSMS Інструкції',
@@ -330,6 +351,12 @@ export const uk: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Скасувати',
export: 'Експортувати',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -362,7 +389,7 @@ export const uk: LanguageTranslation = {
error: {
title: 'Помилка експорут діаграми',
description:
'Щось пішло не так. Потрібна допомога? chartdb.io@gmail.com',
'Щось пішло не так. Потрібна допомога? support@chartdb.io',
},
},
import_diagram_dialog: {
@@ -373,7 +400,7 @@ export const uk: LanguageTranslation = {
error: {
title: 'Помилка імпорту діаграми',
description:
'JSON діаграми є неправильним. Будь ласка, перевірте JSON і спробуйте ще раз. Потрібна допомога? chartdb.io@gmail.com',
'JSON діаграми є неправильним. Будь ласка, перевірте JSON і спробуйте ще раз. Потрібна допомога? support@chartdb.io',
},
},
// TODO: Translate
@@ -400,6 +427,8 @@ export const uk: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Нова таблиця',
new_relationship: 'Новий звʼязок',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -210,6 +210,27 @@ export const vi: LanguageTranslation = {
description: 'Tạo bảng xem phụ thuộc để bắt đầu',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -236,7 +257,7 @@ export const vi: LanguageTranslation = {
title: 'Nhập cơ sở dữ liệu của bạn',
database_edition: 'Loại:',
step_1: 'Chạy lệnh này trong cơ sở dữ liệu của bạn:',
step_2: 'Dán kết quả vào đây:',
step_2: 'Dán kết quả vào đây',
script_results_placeholder: 'Kết quả...',
ssms_instructions: {
button_text: 'Hướng dẫn SSMS',
@@ -329,6 +350,12 @@ export const vi: LanguageTranslation = {
scale_4x: '4x',
cancel: 'Hủy',
export: 'Xuất',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -362,7 +389,7 @@ export const vi: LanguageTranslation = {
error: {
title: 'Lỗi khi xuất sơ đồ',
description:
'Có gì đó không ổn. Cần trợ giúp? chartdb.io@gmail.com',
'Có gì đó không ổn. Cần trợ giúp? support@chartdb.io',
},
},
@@ -374,7 +401,7 @@ export const vi: LanguageTranslation = {
error: {
title: 'Lỗi khi nhập sơ đồ',
description:
'Sơ đồ ở dạng JSON không hợp lệ. Vui lòng kiểm tra JSON và thử lại. Bạn cần trợ giúp? chartdb.io@gmail.com',
'Sơ đồ ở dạng JSON không hợp lệ. Vui lòng kiểm tra JSON và thử lại. Bạn cần trợ giúp? support@chartdb.io',
},
},
// TODO: Translate
@@ -401,6 +428,8 @@ export const vi: LanguageTranslation = {
canvas_context_menu: {
new_table: 'Tạo bảng mới',
new_relationship: 'Tạo quan hệ mới',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -207,6 +207,27 @@ export const zh_CN: LanguageTranslation = {
description: '创建视图以开始',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -232,7 +253,7 @@ export const zh_CN: LanguageTranslation = {
title: '导入您的数据库',
database_edition: '数据库类型:',
step_1: '在您的数据库中执行以下脚本:',
step_2: '将结果粘贴于此',
step_2: '将结果粘贴于此',
script_results_placeholder: '结果...',
ssms_instructions: {
button_text: 'SSMS 说明',
@@ -326,6 +347,12 @@ export const zh_CN: LanguageTranslation = {
scale_4x: '4x',
cancel: '取消',
export: '导出',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -358,7 +385,7 @@ export const zh_CN: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
@@ -370,7 +397,7 @@ export const zh_CN: LanguageTranslation = {
error: {
title: '导入关系图时出错',
description:
'关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 chartdb.io@gmail.com',
'关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 support@chartdb.io',
},
},
// TODO: Translate
@@ -397,6 +424,8 @@ export const zh_CN: LanguageTranslation = {
canvas_context_menu: {
new_table: '新建表',
new_relationship: '新建关系',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -207,6 +207,27 @@ export const zh_TW: LanguageTranslation = {
description: '請建立檢視以開始',
},
},
// TODO: Translate
areas_section: {
areas: 'Areas',
add_area: 'Add Area',
filter: 'Filter',
clear: 'Clear Filter',
no_results: 'No areas found matching your filter.',
area: {
area_actions: {
title: 'Area Actions',
edit_name: 'Edit Name',
delete_area: 'Delete Area',
},
},
empty_state: {
title: 'No areas',
description: 'Create an area to get started',
},
},
},
toolbar: {
@@ -232,7 +253,7 @@ export const zh_TW: LanguageTranslation = {
title: '匯入資料庫',
database_edition: '資料庫版本:',
step_1: '請在資料庫中執行以下腳本:',
step_2: '將腳本結果貼到此處:',
step_2: '將腳本結果貼到此處',
script_results_placeholder: '在此處貼上腳本結果...',
ssms_instructions: {
button_text: 'SSMS 操作步驟',
@@ -325,6 +346,12 @@ export const zh_TW: LanguageTranslation = {
scale_4x: '4x',
cancel: '取消',
export: '匯出',
// TODO: Translate
advanced_options: 'Advanced Options',
pattern: 'Include background pattern',
pattern_description: 'Add subtle grid pattern to background.',
transparent: 'Transparent background',
transparent_description: 'Remove background color from image.',
},
new_table_schema_dialog: {
@@ -357,7 +384,7 @@ export const zh_TW: LanguageTranslation = {
error: {
title: 'Error exporting diagram',
description:
'Something went wrong. Need help? chartdb.io@gmail.com',
'Something went wrong. Need help? support@chartdb.io',
},
},
@@ -369,7 +396,7 @@ export const zh_TW: LanguageTranslation = {
error: {
title: '匯入圖表時發生錯誤',
description:
'圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 chartdb.io@gmail.com',
'圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 support@chartdb.io',
},
},
// TODO: Translate
@@ -396,6 +423,8 @@ export const zh_TW: LanguageTranslation = {
canvas_context_menu: {
new_table: '新建表格',
new_relationship: '新建關聯',
// TODO: Translate
new_area: 'New Area',
},
table_node_context_menu: {

View File

@@ -1,3 +1,4 @@
import type { Area } from './domain/area';
import type { DBDependency } from './domain/db-dependency';
import type { DBField } from './domain/db-field';
import type { DBIndex } from './domain/db-index';
@@ -43,6 +44,10 @@ const generateIdsMapFromDiagram = (
idsMap.set(dependency.id, generateId());
});
diagram.areas?.forEach((area) => {
idsMap.set(area.id, generateId());
});
return idsMap;
};
@@ -193,12 +198,28 @@ export const cloneDiagram = (
(dependency): dependency is DBDependency => dependency !== null
) ?? [];
const areas: Area[] =
diagram.areas
?.map((area) => {
const id = getNewId(area.id);
if (!id) {
return null;
}
return {
...area,
id,
} satisfies Area;
})
.filter((area): area is Area => area !== null) ?? [];
return {
...diagram,
id: diagramId,
dependencies,
relationships,
tables,
areas,
createdAt: new Date(),
updatedAt: new Date(),
};

View File

@@ -15,6 +15,7 @@ export interface DataType {
export interface DataTypeData extends DataType {
hasCharMaxLength?: boolean;
usageLevel?: 1 | 2; // Level 1 is most common, Level 2 is second most common
}
export const dataTypeSchema: z.ZodType<DataType> = z.object({
@@ -33,6 +34,45 @@ export const dataTypeMap: Record<DatabaseType, readonly DataTypeData[]> = {
[DatabaseType.COCKROACHDB]: postgresDataTypes,
} as const;
export const sortDataTypes = (dataTypes: DataTypeData[]): DataTypeData[] => {
const types = [...dataTypes];
return types.sort((a, b) => {
// First sort by usage level (lower numbers first)
if ((a.usageLevel || 3) < (b.usageLevel || 3)) return -1;
if ((a.usageLevel || 3) > (b.usageLevel || 3)) return 1;
// Then sort alphabetically by name
return a.name.localeCompare(b.name);
});
};
export const sortedDataTypeMap: Record<DatabaseType, readonly DataTypeData[]> =
{
[DatabaseType.GENERIC]: sortDataTypes([
...dataTypeMap[DatabaseType.GENERIC],
]),
[DatabaseType.POSTGRESQL]: sortDataTypes([
...dataTypeMap[DatabaseType.POSTGRESQL],
]),
[DatabaseType.MYSQL]: sortDataTypes([
...dataTypeMap[DatabaseType.MYSQL],
]),
[DatabaseType.SQL_SERVER]: sortDataTypes([
...dataTypeMap[DatabaseType.SQL_SERVER],
]),
[DatabaseType.MARIADB]: sortDataTypes([
...dataTypeMap[DatabaseType.MARIADB],
]),
[DatabaseType.SQLITE]: sortDataTypes([
...dataTypeMap[DatabaseType.SQLITE],
]),
[DatabaseType.CLICKHOUSE]: sortDataTypes([
...dataTypeMap[DatabaseType.CLICKHOUSE],
]),
[DatabaseType.COCKROACHDB]: sortDataTypes([
...dataTypeMap[DatabaseType.COCKROACHDB],
]),
} as const;
const compatibleTypes: Record<DatabaseType, Record<string, string[]>> = {
[DatabaseType.POSTGRESQL]: {
serial: ['integer'],

View File

@@ -1,27 +1,32 @@
import type { DataTypeData } from './data-types';
export const genericDataTypes: readonly DataTypeData[] = [
// Level 1 - Most commonly used types
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
// Level 2 - Second most common types
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{ name: 'datetime', id: 'datetime', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
{ name: 'uuid', id: 'uuid', usageLevel: 2 },
// Less common types
{ name: 'bigint', id: 'bigint' },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'blob', id: 'blob' },
{ name: 'boolean', id: 'boolean' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'date', id: 'date' },
{ name: 'datetime', id: 'datetime' },
{ name: 'decimal', id: 'decimal' },
{ name: 'double', id: 'double' },
{ name: 'enum', id: 'enum' },
{ name: 'float', id: 'float' },
{ name: 'int', id: 'int' },
{ name: 'json', id: 'json' },
{ name: 'numeric', id: 'numeric' },
{ name: 'real', id: 'real' },
{ name: 'set', id: 'set' },
{ name: 'smallint', id: 'smallint' },
{ name: 'text', id: 'text' },
{ name: 'time', id: 'time' },
{ name: 'timestamp', id: 'timestamp' },
{ name: 'uuid', id: 'uuid' },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
] as const;

View File

@@ -1,30 +1,33 @@
import type { DataTypeData } from './data-types';
export const mariadbDataTypes: readonly DataTypeData[] = [
// Numeric Types
// Level 1 - Most commonly used types
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'bigint', id: 'bigint', usageLevel: 1 },
{ name: 'decimal', id: 'decimal', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
// Level 2 - Second most common types
{ name: 'json', id: 'json', usageLevel: 2 },
{ name: 'uuid', id: 'uuid', usageLevel: 2 },
// Less common types
{ name: 'tinyint', id: 'tinyint' },
{ name: 'smallint', id: 'smallint' },
{ name: 'mediumint', id: 'mediumint' },
{ name: 'int', id: 'int' },
{ name: 'bigint', id: 'bigint' },
{ name: 'decimal', id: 'decimal' },
{ name: 'numeric', id: 'numeric' },
{ name: 'float', id: 'float' },
{ name: 'double', id: 'double' },
{ name: 'bit', id: 'bit' },
{ name: 'bool', id: 'bool' },
{ name: 'boolean', id: 'boolean' },
// Date and Time Types
{ name: 'date', id: 'date' },
{ name: 'datetime', id: 'datetime' },
{ name: 'timestamp', id: 'timestamp' },
{ name: 'time', id: 'time' },
{ name: 'year', id: 'year' },
// String Types
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'tinyblob', id: 'tinyblob' },
@@ -32,13 +35,10 @@ export const mariadbDataTypes: readonly DataTypeData[] = [
{ name: 'mediumblob', id: 'mediumblob' },
{ name: 'longblob', id: 'longblob' },
{ name: 'tinytext', id: 'tinytext' },
{ name: 'text', id: 'text' },
{ name: 'mediumtext', id: 'mediumtext' },
{ name: 'longtext', id: 'longtext' },
{ name: 'enum', id: 'enum' },
{ name: 'set', id: 'set' },
// Spatial Types
{ name: 'geometry', id: 'geometry' },
{ name: 'point', id: 'point' },
{ name: 'linestring', id: 'linestring' },
@@ -47,8 +47,4 @@ export const mariadbDataTypes: readonly DataTypeData[] = [
{ name: 'multilinestring', id: 'multilinestring' },
{ name: 'multipolygon', id: 'multipolygon' },
{ name: 'geometrycollection', id: 'geometrycollection' },
// JSON Type
{ name: 'json', id: 'json' },
{ name: 'uuid', id: 'uuid' },
] as const;

View File

@@ -1,44 +1,41 @@
import type { DataTypeData } from './data-types';
export const mysqlDataTypes: readonly DataTypeData[] = [
// Numeric Types
// Level 1 - Most commonly used types
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
// Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{ name: 'datetime', id: 'datetime', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
// Less common types
{ name: 'tinyint', id: 'tinyint' },
{ name: 'smallint', id: 'smallint' },
{ name: 'mediumint', id: 'mediumint' },
{ name: 'int', id: 'int' },
{ name: 'bigint', id: 'bigint' },
{ name: 'decimal', id: 'decimal' },
{ name: 'numeric', id: 'numeric' },
{ name: 'float', id: 'float' },
{ name: 'double', id: 'double' },
{ name: 'bit', id: 'bit' },
{ name: 'bool', id: 'bool' },
{ name: 'boolean', id: 'boolean' },
// Date and Time Types
{ name: 'date', id: 'date' },
{ name: 'datetime', id: 'datetime' },
{ name: 'timestamp', id: 'timestamp' },
{ name: 'time', id: 'time' },
{ name: 'year', id: 'year' },
// String Types
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'tinytext', id: 'tinytext' },
{ name: 'mediumtext', id: 'mediumtext' },
{ name: 'longtext', id: 'longtext' },
{ name: 'binary', id: 'binary' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{ name: 'mediumblob', id: 'mediumblob' },
{ name: 'longblob', id: 'longblob' },
{ name: 'tinytext', id: 'tinytext' },
{ name: 'text', id: 'text' },
{ name: 'mediumtext', id: 'mediumtext' },
{ name: 'longtext', id: 'longtext' },
{ name: 'enum', id: 'enum' },
{ name: 'set', id: 'set' },
// Spatial Types
{ name: 'time', id: 'time' },
{ name: 'year', id: 'year' },
{ name: 'geometry', id: 'geometry' },
{ name: 'point', id: 'point' },
{ name: 'linestring', id: 'linestring' },
@@ -47,7 +44,4 @@ export const mysqlDataTypes: readonly DataTypeData[] = [
{ name: 'multilinestring', id: 'multilinestring' },
{ name: 'multipolygon', id: 'multipolygon' },
{ name: 'geometrycollection', id: 'geometrycollection' },
// JSON Type
{ name: 'json', id: 'json' },
] as const;

View File

@@ -1,49 +1,48 @@
import type { DataTypeData } from './data-types';
export const postgresDataTypes: readonly DataTypeData[] = [
// Numeric Types
{ name: 'smallint', id: 'smallint' },
{ name: 'integer', id: 'integer' },
{ name: 'bigint', id: 'bigint' },
{ name: 'decimal', id: 'decimal' },
// Level 1 - Most commonly used types
{ name: 'integer', id: 'integer', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
// Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{ name: 'serial', id: 'serial', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
{ name: 'jsonb', id: 'jsonb', usageLevel: 2 },
{ name: 'uuid', id: 'uuid', usageLevel: 2 },
{
name: 'timestamp with time zone',
id: 'timestamp_with_time_zone',
usageLevel: 2,
},
// Less common types
{ name: 'numeric', id: 'numeric' },
{ name: 'real', id: 'real' },
{ name: 'double precision', id: 'double_precision' },
{ name: 'smallserial', id: 'smallserial' },
{ name: 'serial', id: 'serial' },
{ name: 'bigserial', id: 'bigserial' },
{ name: 'money', id: 'money' },
// Character Types
{ name: 'smallint', id: 'smallint' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{
name: 'character varying',
id: 'character_varying',
hasCharMaxLength: true,
},
{ name: 'text', id: 'text' },
// Binary Data Types
{ name: 'bytea', id: 'bytea' },
// Date/Time Types
{ name: 'date', id: 'date' },
{ name: 'timestamp', id: 'timestamp' },
{ name: 'timestamp with time zone', id: 'timestamp_with_time_zone' },
{ name: 'timestamp without time zone', id: 'timestamp_without_time_zone' },
{ name: 'time', id: 'time' },
{ name: 'timestamp without time zone', id: 'timestamp_without_time_zone' },
{ name: 'time with time zone', id: 'time_with_time_zone' },
{ name: 'time without time zone', id: 'time_without_time_zone' },
{ name: 'interval', id: 'interval' },
// Boolean Type
{ name: 'boolean', id: 'boolean' },
// Enumerated Types
{ name: 'bytea', id: 'bytea' },
{ name: 'enum', id: 'enum' },
// Geometric Types
{ name: 'point', id: 'point' },
{ name: 'line', id: 'line' },
{ name: 'lseg', id: 'lseg' },
@@ -51,43 +50,22 @@ export const postgresDataTypes: readonly DataTypeData[] = [
{ name: 'path', id: 'path' },
{ name: 'polygon', id: 'polygon' },
{ name: 'circle', id: 'circle' },
// Network Address Types
{ name: 'cidr', id: 'cidr' },
{ name: 'inet', id: 'inet' },
{ name: 'macaddr', id: 'macaddr' },
{ name: 'macaddr8', id: 'macaddr8' },
// Bit String Types
{ name: 'bit', id: 'bit' },
{ name: 'bit varying', id: 'bit_varying' },
// Text Search Types
{ name: 'tsvector', id: 'tsvector' },
{ name: 'tsquery', id: 'tsquery' },
// UUID Type
{ name: 'uuid', id: 'uuid' },
// XML Type
{ name: 'xml', id: 'xml' },
// JSON Types
{ name: 'json', id: 'json' },
{ name: 'jsonb', id: 'jsonb' },
// Array Types
{ name: 'array', id: 'array' },
// Range Types
{ name: 'int4range', id: 'int4range' },
{ name: 'int8range', id: 'int8range' },
{ name: 'numrange', id: 'numrange' },
{ name: 'tsrange', id: 'tsrange' },
{ name: 'tstzrange', id: 'tstzrange' },
{ name: 'daterange', id: 'daterange' },
// Object Identifier Types
{ name: 'oid', id: 'oid' },
{ name: 'regproc', id: 'regproc' },
{ name: 'regprocedure', id: 'regprocedure' },
@@ -99,7 +77,5 @@ export const postgresDataTypes: readonly DataTypeData[] = [
{ name: 'regnamespace', id: 'regnamespace' },
{ name: 'regconfig', id: 'regconfig' },
{ name: 'regdictionary', id: 'regdictionary' },
// User Defined types
{ name: 'user-defined', id: 'user-defined' },
] as const;

View File

@@ -1,56 +1,44 @@
import type { DataTypeData } from './data-types';
export const sqlServerDataTypes: readonly DataTypeData[] = [
// Exact Numerics
{ name: 'bigint', id: 'bigint' },
{ name: 'bit', id: 'bit' },
{ name: 'decimal', id: 'decimal' },
{ name: 'int', id: 'int' },
{ name: 'money', id: 'money' },
// Level 1 - Most commonly used types
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'bit', id: 'bit', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
// Level 2 - Second most common types
{ name: 'bigint', id: 'bigint', usageLevel: 2 },
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{ name: 'datetime2', id: 'datetime2', usageLevel: 2 },
{ name: 'uniqueidentifier', id: 'uniqueidentifier', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
// Less common types
{ name: 'numeric', id: 'numeric' },
{ name: 'smallint', id: 'smallint' },
{ name: 'smallmoney', id: 'smallmoney' },
{ name: 'tinyint', id: 'tinyint' },
// Approximate Numerics
{ name: 'money', id: 'money' },
{ name: 'float', id: 'float' },
{ name: 'real', id: 'real' },
// Date and Time
{ name: 'date', id: 'date' },
{ name: 'datetime2', id: 'datetime2' },
{ name: 'datetime', id: 'datetime' },
{ name: 'datetimeoffset', id: 'datetimeoffset' },
{ name: 'smalldatetime', id: 'smalldatetime' },
{ name: 'time', id: 'time' },
// Character Strings
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'text', id: 'text' },
// Unicode Character Strings
{ name: 'nchar', id: 'nchar', hasCharMaxLength: true },
{ name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true },
{ name: 'ntext', id: 'ntext' },
// Binary Strings
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'image', id: 'image' },
// Other Data Types
{ name: 'datetimeoffset', id: 'datetimeoffset' },
{ name: 'smalldatetime', id: 'smalldatetime' },
{ name: 'time', id: 'time' },
{ name: 'timestamp', id: 'timestamp' },
{ name: 'xml', id: 'xml' },
{ name: 'cursor', id: 'cursor' },
{ name: 'hierarchyid', id: 'hierarchyid' },
{ name: 'sql_variant', id: 'sql_variant' },
{ name: 'timestamp', id: 'timestamp' },
{ name: 'uniqueidentifier', id: 'uniqueidentifier' },
{ name: 'xml', id: 'xml' },
// Spatial Data Types
{ name: 'geometry', id: 'geometry' },
{ name: 'geography', id: 'geography' },
// JSON
{ name: 'json', id: 'json' },
] as const;

View File

@@ -1,27 +1,34 @@
import type { DataTypeData } from './data-types';
export const sqliteDataTypes: readonly DataTypeData[] = [
// Numeric Types
{ name: 'integer', id: 'integer' },
{ name: 'real', id: 'real' },
{ name: 'numeric', id: 'numeric' },
// Level 1 - Most commonly used types - SQLite's 5 storage classes
{ name: 'integer', id: 'integer', usageLevel: 1 },
{ name: 'text', id: 'text', usageLevel: 1 },
{ name: 'real', id: 'real', usageLevel: 1 },
{ name: 'blob', id: 'blob', usageLevel: 1 },
{ name: 'null', id: 'null', usageLevel: 1 },
// Text Type
{ name: 'text', id: 'text' },
// SQLite type aliases and common types
{ name: 'int', id: 'int', usageLevel: 1 },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true, usageLevel: 1 },
{ name: 'timestamp', id: 'timestamp', usageLevel: 1 },
{ name: 'date', id: 'date', usageLevel: 1 },
{ name: 'datetime', id: 'datetime', usageLevel: 1 },
{ name: 'boolean', id: 'boolean', usageLevel: 1 },
// Blob Type
{ name: 'blob', id: 'blob' },
// Level 2 - Second most common types
{ name: 'numeric', id: 'numeric', usageLevel: 2 },
{ name: 'decimal', id: 'decimal', usageLevel: 2 },
{ name: 'float', id: 'float', usageLevel: 2 },
{ name: 'double', id: 'double', usageLevel: 2 },
{ name: 'json', id: 'json', usageLevel: 2 },
// Blob Type
{ name: 'json', id: 'json' },
// Date/Time Types (SQLite uses TEXT, REAL, or INTEGER types for dates and times)
{ name: 'date', id: 'date' },
{ name: 'datetime', id: 'datetime' },
{ name: 'int', id: 'int' },
{ name: 'float', id: 'float' },
{ name: 'boolean', id: 'boolean' },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'decimal', id: 'decimal' },
// Less common types (all map to SQLite storage classes)
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'binary', id: 'binary' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'smallint', id: 'smallint' },
{ name: 'bigint', id: 'bigint' },
{ name: 'bool', id: 'bool' },
{ name: 'time', id: 'time' },
] as const;

View File

@@ -10,7 +10,7 @@ import type { DBField } from '@/lib/domain/db-field';
import type { DBRelationship } from '@/lib/domain/db-relationship';
function parsePostgresDefault(field: DBField): string {
if (!field.default) {
if (!field.default || typeof field.default !== 'string') {
return '';
}
@@ -165,6 +165,21 @@ export function exportPostgreSQL(diagram: Diagram): string {
// Handle PostgreSQL specific type formatting
let typeWithSize = typeName;
let serialType = null;
if (field.increment && !field.nullable) {
if (
typeName.toLowerCase() === 'integer' ||
typeName.toLowerCase() === 'int'
) {
serialType = 'SERIAL';
} else if (typeName.toLowerCase() === 'bigint') {
serialType = 'BIGSERIAL';
} else if (typeName.toLowerCase() === 'smallint') {
serialType = 'SMALLSERIAL';
}
}
if (field.characterMaximumLength) {
if (
typeName.toLowerCase() === 'varchar' ||
@@ -221,7 +236,7 @@ export function exportPostgreSQL(diagram: Diagram): string {
: '';
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${serialType || typeWithSize}${serialType ? '' : notNull}${identity}${unique}${defaultValue}`;
})
.join(',\n')}${
primaryKeyFields.length > 0

View File

@@ -9,6 +9,29 @@ import { exportPostgreSQL } from './export-per-type/postgresql';
import { exportSQLite } from './export-per-type/sqlite';
import { exportMySQL } from './export-per-type/mysql';
// Function to simplify verbose data type names
const simplifyDataType = (typeName: string): string => {
const typeMap: Record<string, string> = {
'character varying': 'varchar',
'char varying': 'varchar',
integer: 'int',
int4: 'int',
int8: 'bigint',
serial4: 'serial',
serial8: 'bigserial',
float8: 'double precision',
float4: 'real',
bool: 'boolean',
character: 'char',
'timestamp without time zone': 'timestamp',
'timestamp with time zone': 'timestamptz',
'time without time zone': 'time',
'time with time zone': 'timetz',
};
return typeMap[typeName.toLowerCase()] || typeName;
};
export const exportBaseSQL = ({
diagram,
targetDatabaseType,
@@ -93,11 +116,12 @@ export const exportBaseSQL = ({
sqlScript += `CREATE TABLE ${tableName} (\n`;
table.fields.forEach((field, index) => {
let typeName = field.type.name;
let typeName = simplifyDataType(field.type.name);
// Handle ENUM type
if (typeName.toLowerCase() === 'enum') {
typeName = 'varchar';
// Map enum to TEXT for broader compatibility, especially with DBML importer
typeName = 'text';
}
// Temp fix for 'array' to be text[]
@@ -116,6 +140,7 @@ export const exportBaseSQL = ({
if (field.characterMaximumLength) {
sqlScript += `(${field.characterMaximumLength})`;
} else if (field.type.name.toLowerCase().includes('varchar')) {
// Keep varchar sizing, but don't apply to TEXT (previously enum)
sqlScript += `(500)`;
}

View File

@@ -176,8 +176,7 @@ cols AS (
), views AS (
SELECT array_to_string(array_agg(CONCAT('{"schema":"', views.schemaname::TEXT,
'","view_name":"', viewname::TEXT,
'","view_definition":"', encode(convert_to(REPLACE(definition::TEXT, '"', '\\"'), 'UTF8'), 'base64'),
'"}')),
'","view_definition":""}')),
',') AS views_metadata
FROM pg_views views
WHERE views.schemaname NOT IN ('information_schema', 'pg_catalog')${cockroachdbViewsFilter}

View File

@@ -123,7 +123,7 @@ export const mariaDBQuery = `WITH fk_info as (
AND table_schema = DATABASE()
AND (0x00) IN (@views:=CONCAT_WS(',', @views, CONCAT('{', '"schema":"', \`TABLE_SCHEMA\`, '",',
'"view_name":"', \`TABLE_NAME\`, '",',
'"view_definition":"', REPLACE(REPLACE(TO_BASE64(VIEW_DEFINITION), ' ', ''), '\n', ''), '"}'))) ) )
'"view_definition":""}'))) ) )
)
(SELECT CAST(CONCAT('{"fk_info": [',IFNULL(@fk_info,''),
'], "pk_info": [', IFNULL(@pk_info, ''),

View File

@@ -1,9 +0,0 @@
export const minimizeQuery = (query: string) => {
if (!query) return '';
// Split into lines, trim leading spaces from each line, then rejoin
return query
.split('\n')
.map((line) => line.replace(/^\s+/, '')) // Remove only leading spaces
.join('\n');
};

View File

@@ -133,7 +133,7 @@ export const getMySQLQuery = (
AND table_schema = DATABASE()
AND (0x00) IN (@views:=CONCAT_WS(',', @views, CONCAT('{', '"schema":"', \`TABLE_SCHEMA\`, '",',
'"view_name":"', \`TABLE_NAME\`, '",',
'"view_definition":"', REPLACE(REPLACE(TO_BASE64(VIEW_DEFINITION), ' ', ''), '\n', ''), '"}'))) ) )
'"view_definition":""}'))) ) )
)
(SELECT CAST(CONCAT('{"fk_info": [',IFNULL(@fk_info,''),
'], "pk_info": [', IFNULL(@pk_info, ''),
@@ -286,7 +286,7 @@ export const getMySQLQuery = (
) FROM (
SELECT \`TABLE_SCHEMA\`,
\`TABLE_NAME\` AS view_name,
REPLACE(REPLACE(TO_BASE64(\`VIEW_DEFINITION\`), ' ', ''), '\n', '') AS view_definition
null AS view_definition
FROM information_schema.views vws
WHERE vws.table_schema = DATABASE()
) AS vws), ''),

View File

@@ -245,8 +245,7 @@ cols AS (
), views AS (
SELECT array_to_string(array_agg(CONCAT('{"schema":"', views.schemaname,
'","view_name":"', viewname,
'","view_definition":"', encode(convert_to(REPLACE(definition, '"', '\\"'), 'UTF8'), 'base64'),
'"}')),
'","view_definition":""}')),
',') AS views_metadata
FROM pg_views views
WHERE views.schemaname NOT IN ('information_schema', 'pg_catalog') ${

View File

@@ -166,15 +166,7 @@ views AS (
JSON_QUERY(N'{
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
'", "view_name": "' + STRING_ESCAPE(COALESCE(REPLACE(v.name, '"', ''), ''), 'json') +
'", "view_definition": "' +
STRING_ESCAPE(
CAST(
'' AS XML
).value(
'xs:base64Binary(sql:column("DefinitionBinary"))',
'VARCHAR(MAX)'
), 'json') +
N'"}') COLLATE DATABASE_DEFAULT
'", "view_definition": ""}') COLLATE DATABASE_DEFAULT
), N','
) + N']' AS all_views_json
FROM sys.views v
@@ -385,12 +377,7 @@ views AS (
N'{
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
'", "view_name": "' + STRING_ESCAPE(COALESCE(REPLACE(v.name, '"', ''), ''), 'json') +
'", "view_definition": "' +
CAST(
(
SELECT CAST(OBJECT_DEFINITION(v.object_id) AS VARBINARY(MAX)) FOR XML PATH('')
) AS NVARCHAR(MAX)
) + N'"}'
'", "view_definition": ""}'
)
)
FROM

View File

@@ -6,6 +6,18 @@ export const fixMetadataJson = async (
): Promise<string> => {
await waitFor(1000);
// Replace problematic array default values with null
metadataJson = metadataJson.replace(
/"default": "?'?\[[^\]]*\]'?"?(\\")?(,|\})/gs,
'"default": null$2'
);
// Generic fix for all default values with '\ pattern - convert to just '
metadataJson = metadataJson.replace(
/"default":\s*"(.*?)'\\"(,|\})/g,
'"default": "$1"$2'
);
// TODO: remove this temporary eslint disable
return (
metadataJson
@@ -24,6 +36,26 @@ export const fixMetadataJson = async (
.replace(/"""([^",}]+)"""/g, '"$1"') // Remove tripple quotes from keys
.replace(/""([^",}]+)""/g, '"$1"') // Remove double quotes from keys
.replace(/'"([^"]+)"'/g, '\\"$1\\"') // Replace single-quoted double quotes
.replace(/'(".*?")'/g, "'\\$1'") // Handle cases like '"{}"'::json
// Handle specific case for nextval with quoted identifiers
.replace(
/nextval\('(".*?")'::regclass\)/g,
"nextval('\\$1'::regclass)"
)
// Handle cases like "'CHAT'::"CustomType"" (ensures existing quotes are escaped for JSON)
/* eslint-disable-next-line no-useless-escape */
.replace(/'([^']+)'::\"([^\"]+)\"/g, '\'$1\'::\\\"$2\\\"')
// Convert string "null" to actual null for precision field
.replace(/"precision": "null"/g, '"precision": null')
// Convert string "true"/"false" to actual boolean for nullable field
.replace(/"nullable": "false"/g, '"nullable": false')
.replace(/"nullable": "true"/g, '"nullable": true')
/* eslint-disable-next-line no-useless-escape */
.replace(/\"/g, '___ESCAPED_QUOTE___') // Temporarily replace empty strings
.replace(/(?<=:\s*)""(?=\s*[,}])/g, '___EMPTY___') // Temporarily replace empty strings
@@ -46,3 +78,13 @@ export const isStringMetadataJson = (metadataJsonString: string): boolean => {
return result;
};
export const minimizeQuery = (query: string) => {
if (!query) return '';
// Split into lines, trim leading spaces from each line, then rejoin
return query
.split('\n')
.map((line) => line.replace(/^\s+/, '')) // Remove only leading spaces
.join('\n');
};

View File

@@ -53,6 +53,8 @@ export interface SQLForeignKey {
targetTableId: string;
updateAction?: string;
deleteAction?: string;
sourceCardinality?: Cardinality;
targetCardinality?: Cardinality;
}
export interface SQLParserResult {
@@ -187,33 +189,70 @@ export function determineCardinality(
): { sourceCardinality: Cardinality; targetCardinality: Cardinality } {
if (isSourceUnique && isTargetUnique) {
return {
sourceCardinality: 'one' as Cardinality,
targetCardinality: 'one' as Cardinality,
sourceCardinality: 'one',
targetCardinality: 'one',
};
} else if (isSourceUnique) {
return {
sourceCardinality: 'one' as Cardinality,
targetCardinality: 'many' as Cardinality,
sourceCardinality: 'one',
targetCardinality: 'many',
};
} else if (isTargetUnique) {
return {
sourceCardinality: 'many' as Cardinality,
targetCardinality: 'one' as Cardinality,
sourceCardinality: 'many',
targetCardinality: 'one',
};
} else {
return {
sourceCardinality: 'many' as Cardinality,
targetCardinality: 'many' as Cardinality,
sourceCardinality: 'many',
targetCardinality: 'many',
};
}
}
// Map SQL data type to generic data type in our system
export function mapSQLTypeToGenericType(sqlType: string): DataType {
const normalizedType = sqlType.toLowerCase().replace(/\(.*\)/, '');
export function mapSQLTypeToGenericType(
sqlType: string,
databaseType?: DatabaseType
): DataType {
if (!sqlType) {
return genericDataTypes.find((t) => t.id === 'text')!;
}
// Normalize the SQL type to lowercase for consistency
const normalizedSqlType = sqlType.toLowerCase();
// Add special case handling for SQLite INTEGER type
if (
databaseType === DatabaseType.SQLITE &&
(normalizedSqlType === 'integer' || normalizedSqlType === 'int')
) {
return genericDataTypes.find((t) => t.id === 'integer')!;
}
// Get dialect-specific type mappings
const dialectAffinity =
(databaseType && typeAffinity[databaseType]) ||
typeAffinity[DatabaseType.GENERIC];
// Handle specific database dialect mappings
if (databaseType) {
// Try to find a mapping for the normalized type
const typeMapping = dialectAffinity[normalizedSqlType];
if (typeMapping) {
const foundType = genericDataTypes.find(
(t) => t.id === typeMapping
);
if (foundType) return foundType;
}
}
// Try direct mapping by normalizing the input type
const normalizedType = normalizedSqlType.replace(/\(.*\)/, '');
const matchedType = genericDataTypes.find((t) => t.id === normalizedType);
if (matchedType) return matchedType;
// Generic type mappings as a fallback
const typeMap: Record<string, string> = {
int: 'integer',
integer: 'integer',
@@ -236,6 +275,8 @@ export function mapSQLTypeToGenericType(sqlType: string): DataType {
time: 'time',
json: 'json',
jsonb: 'json',
real: 'real',
blob: 'blob',
};
const mappedType = typeMap[normalizedType];
@@ -244,80 +285,164 @@ export function mapSQLTypeToGenericType(sqlType: string): DataType {
if (foundType) return foundType;
}
return genericDataTypes.find((t) => t.id === 'varchar')!;
// Default to text as last resort
return genericDataTypes.find((t) => t.id === 'text')!;
}
// Type affinity definitions for different database dialects
export const typeAffinity: Record<string, Record<string, string>> = {
[DatabaseType.POSTGRESQL]: {
INT: 'INTEGER',
INTEGER: 'INTEGER',
MEDIUMINT: 'INTEGER',
BIT: 'BOOLEAN',
// PostgreSQL data types (all lowercase for consistency)
int: 'integer',
integer: 'integer',
int4: 'integer',
smallint: 'smallint',
int2: 'smallint',
bigint: 'bigint',
int8: 'bigint',
decimal: 'decimal',
numeric: 'numeric',
real: 'real',
'double precision': 'double',
float: 'float',
float4: 'float',
float8: 'double',
boolean: 'boolean',
bool: 'boolean',
varchar: 'varchar',
'character varying': 'varchar',
char: 'char',
character: 'char',
text: 'text',
date: 'date',
timestamp: 'timestamp',
time: 'time',
json: 'json',
jsonb: 'jsonb',
},
[DatabaseType.MYSQL]: {
INT: 'INTEGER',
INTEGER: 'INTEGER',
BOOL: 'BOOLEAN',
BOOLEAN: 'TINYINT',
// MySQL data types (all lowercase for consistency)
int: 'integer',
integer: 'integer',
smallint: 'smallint',
tinyint: 'tinyint',
bigint: 'bigint',
decimal: 'decimal',
numeric: 'numeric',
float: 'float',
double: 'double',
boolean: 'tinyint',
bool: 'tinyint',
varchar: 'varchar',
char: 'char',
text: 'text',
date: 'date',
datetime: 'datetime',
timestamp: 'timestamp',
time: 'time',
json: 'json',
},
[DatabaseType.MARIADB]: {
INT: 'INTEGER',
INTEGER: 'INTEGER',
BOOL: 'BOOLEAN',
BOOLEAN: 'TINYINT',
// MariaDB data types (all lowercase for consistency)
int: 'integer',
integer: 'integer',
smallint: 'smallint',
tinyint: 'tinyint',
bigint: 'bigint',
decimal: 'decimal',
numeric: 'numeric',
float: 'float',
double: 'double',
boolean: 'tinyint',
bool: 'tinyint',
varchar: 'varchar',
char: 'char',
text: 'text',
date: 'date',
datetime: 'datetime',
timestamp: 'timestamp',
time: 'time',
json: 'json',
},
[DatabaseType.SQL_SERVER]: {
INT: 'INTEGER',
INTEGER: 'INT',
BOOL: 'BIT',
BOOLEAN: 'BIT',
// SQL Server data types (all lowercase for consistency)
int: 'integer',
integer: 'integer',
smallint: 'smallint',
bigint: 'bigint',
decimal: 'decimal',
numeric: 'numeric',
float: 'float',
real: 'real',
bit: 'bit',
boolean: 'bit',
bool: 'bit',
varchar: 'varchar',
nvarchar: 'nvarchar',
char: 'char',
nchar: 'nchar',
text: 'text',
ntext: 'ntext',
date: 'date',
datetime: 'datetime',
datetime2: 'datetime2',
time: 'time',
uniqueidentifier: 'uniqueidentifier',
},
[DatabaseType.SQLITE]: {
INT: 'INTEGER',
BOOL: 'INTEGER',
BOOLEAN: 'INTEGER',
// SQLite storage classes (all lowercase for consistency)
integer: 'integer',
int: 'integer',
bigint: 'bigint',
smallint: 'smallint',
tinyint: 'tinyint',
real: 'real',
float: 'real',
double: 'real',
numeric: 'real',
decimal: 'real',
text: 'text',
varchar: 'text',
char: 'text',
blob: 'blob',
binary: 'blob',
varbinary: 'blob',
timestamp: 'timestamp',
datetime: 'timestamp',
date: 'date',
boolean: 'integer',
bool: 'integer',
time: 'text',
json: 'text',
},
[DatabaseType.GENERIC]: {
INTEGER: 'integer',
INT: 'integer',
MEDIUMINT: 'integer',
BIT: 'boolean',
VARCHAR: 'varchar',
'CHARACTER VARYING': 'varchar',
CHAR: 'char',
CHARACTER: 'char',
TEXT: 'text',
BOOLEAN: 'boolean',
BOOL: 'boolean',
TIMESTAMP: 'timestamp',
DATETIME: 'timestamp',
DATE: 'date',
TIME: 'time',
JSON: 'json',
JSONB: 'json',
DECIMAL: 'decimal',
NUMERIC: 'numeric',
FLOAT: 'float',
DOUBLE: 'double',
BIGINT: 'bigint',
SMALLINT: 'smallint',
// Generic fallback types (all lowercase for consistency)
integer: 'integer',
int: 'integer',
smallint: 'smallint',
bigint: 'bigint',
decimal: 'decimal',
numeric: 'numeric',
float: 'float',
double: 'double',
real: 'real',
boolean: 'boolean',
bool: 'boolean',
varchar: 'varchar',
'character varying': 'varchar',
char: 'char',
character: 'char',
text: 'text',
date: 'date',
timestamp: 'timestamp',
datetime: 'timestamp',
time: 'time',
json: 'json',
jsonb: 'json',
blob: 'blob',
},
};
// For safe type conversions
export function getTypeAffinity(
databaseType: DatabaseType,
sqlType: string
): string {
if (!sqlType) return 'varchar';
const normalizedType = sqlType.toUpperCase();
const dialectAffinity =
typeAffinity[databaseType] || typeAffinity[DatabaseType.GENERIC];
return dialectAffinity[normalizedType] || sqlType.toLowerCase();
}
// Convert SQLParserResult to ChartDB Diagram structure
export function convertToChartDBDiagram(
parserResult: SQLParserResult,
@@ -337,15 +462,95 @@ export function convertToChartDBDiagram(
// Create fields from columns
const fields: DBField[] = table.columns.map((column) => {
// Use special case handling for specific database types to ensure correct mapping
let mappedType: DataType;
// SQLite-specific handling for numeric types
if (sourceDatabaseType === DatabaseType.SQLITE) {
const normalizedType = column.type.toLowerCase();
if (normalizedType === 'integer' || normalizedType === 'int') {
// Ensure integer types are preserved
mappedType = { id: 'integer', name: 'integer' };
} else if (
normalizedType === 'real' ||
normalizedType === 'float' ||
normalizedType === 'double' ||
normalizedType === 'numeric' ||
normalizedType === 'decimal'
) {
// Ensure real types are preserved
mappedType = { id: 'real', name: 'real' };
} else if (normalizedType === 'blob') {
// Ensure blob types are preserved
mappedType = { id: 'blob', name: 'blob' };
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
column.type,
sourceDatabaseType
);
}
}
// Handle MySQL/MariaDB integer types specifically
else if (
sourceDatabaseType === DatabaseType.MYSQL ||
sourceDatabaseType === DatabaseType.MARIADB
) {
const normalizedType = column.type
.toLowerCase()
.replace(/\(\d+\)/, '')
.trim();
// Handle various integer types
if (normalizedType === 'tinyint') {
mappedType = { id: 'tinyint', name: 'tinyint' };
} else if (
normalizedType === 'int' ||
normalizedType === 'integer'
) {
mappedType = { id: 'int', name: 'int' };
} else if (normalizedType === 'smallint') {
mappedType = { id: 'smallint', name: 'smallint' };
} else if (normalizedType === 'mediumint') {
mappedType = { id: 'mediumint', name: 'mediumint' };
} else if (normalizedType === 'bigint') {
mappedType = { id: 'bigint', name: 'bigint' };
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
column.type,
sourceDatabaseType
);
}
}
// Handle PostgreSQL integer type specifically
else if (
sourceDatabaseType === DatabaseType.POSTGRESQL &&
(column.type.toLowerCase() === 'integer' ||
column.type.toLowerCase() === 'int' ||
column.type.toLowerCase() === 'int4')
) {
// Ensure integer types are preserved
mappedType = { id: 'integer', name: 'integer' };
} else {
// Use the standard mapping for other types
mappedType = mapSQLTypeToGenericType(
column.type,
sourceDatabaseType
);
}
const field: DBField = {
id: generateId(),
name: column.name,
type: mapSQLTypeToGenericType(column.type),
type: mappedType,
nullable: column.nullable,
primaryKey: column.primaryKey,
unique: column.unique,
default: column.default || '',
createdAt: Date.now(),
increment: column.increment,
};
// Add type arguments if present
@@ -461,7 +666,7 @@ export function convertToChartDBDiagram(
);
if (!sourceField || !targetField) {
console.warn('Relationship refers to non-existent field:', {
console.log('Relationship refers to non-existent field:', {
sourceTable: rel.sourceTable,
sourceField: rel.sourceColumn,
targetTable: rel.targetTable,
@@ -470,10 +675,13 @@ export function convertToChartDBDiagram(
return;
}
const { sourceCardinality, targetCardinality } = determineCardinality(
sourceField.unique || sourceField.primaryKey,
targetField.unique || targetField.primaryKey
);
// Use the cardinality from the SQL parser if available, otherwise determine it
const sourceCardinality =
rel.sourceCardinality ||
(sourceField.unique || sourceField.primaryKey ? 'one' : 'many');
const targetCardinality =
rel.targetCardinality ||
(targetField.unique || targetField.primaryKey ? 'one' : 'many');
relationships.push({
id: generateId(),

View File

@@ -208,11 +208,39 @@ function processCreateIndexStatement(
}
}
/**
* Detects if a CREATE TABLE statement contains inline REFERENCES (PostgreSQL-style)
* which is not supported in MySQL
*/
function detectInlineReferences(sqlContent: string): {
found: boolean;
line: number;
} {
const lines = sqlContent.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Match column definitions with inline REFERENCES
if (/\w+\s+\w+\s+(?:PRIMARY\s+KEY\s+)?REFERENCES\s+/i.test(line)) {
return { found: true, line: i + 1 };
}
}
return { found: false, line: 0 };
}
export async function fromMySQL(sqlContent: string): Promise<SQLParserResult> {
// Check for inline REFERENCES before proceeding
const { found, line } = detectInlineReferences(sqlContent);
if (found) {
throw new Error(
`MySQL does not support inline REFERENCES in column definitions (line ${line}). Please use FOREIGN KEY constraints instead:\n\nCREATE TABLE UserProfile (\n user_id INT PRIMARY KEY,\n bio TEXT,\n avatar_url VARCHAR(255),\n FOREIGN KEY (user_id) REFERENCES Users(id)\n);`
);
}
const tables: SQLTable[] = [];
const relationships: SQLForeignKey[] = [];
const tableMap: Record<string, string> = {}; // Maps table name to its ID
const pendingForeignKeys: PendingForeignKey[] = []; // Store FKs that reference tables not yet created
const addedRelationships = new Set<string>();
try {
// Extract SQL statements from the dump
@@ -877,6 +905,13 @@ export async function fromMySQL(sqlContent: string): Promise<SQLParserResult> {
}
}
findForeignKeysUsingRegex(
sqlContent,
tableMap,
relationships,
addedRelationships
);
return { tables, relationships };
} catch (error) {
console.error('Error in MySQL dump parser:', error);
@@ -927,3 +962,150 @@ export function isMySQLFormat(sqlContent: string): boolean {
return isLikelyMysql;
}
function findForeignKeysUsingRegex(
sqlContent: string,
tableMap: Record<string, string>,
relationships: SQLForeignKey[],
addedRelationships: Set<string>
): void {
// Normalize SQL content: replace multiple whitespaces and newlines with single space
const normalizedSQL = sqlContent
.replace(/\s+/g, ' ')
// Replace common bracket/brace formatting issues
.replace(/\[\s*(\d+)\s*\]/g, '[$1]')
.replace(/\{\s*(\d+)\s*\}/g, '{$1}')
// Normalize commas and parentheses to help regex matching
.replace(/\s*,\s*/g, ', ')
.replace(/\s*\(\s*/g, ' (')
.replace(/\s*\)\s*/g, ') ')
// Ensure spaces around keywords
.replace(/\bREFERENCES\b/g, ' REFERENCES ')
.replace(/\bINT\b/g, ' INT ')
.replace(/\bPRIMARY\s+KEY\b/g, ' PRIMARY KEY ')
.replace(/\bUNIQUE\b/g, ' UNIQUE ')
.replace(/\bFOREIGN\s+KEY\b/g, ' FOREIGN KEY ')
.replace(/\bENGINE\s*=\s*InnoDB\b/gi, ' ENGINE=InnoDB ');
// First extract all table names to ensure they're in the tableMap
const tableNamePattern =
/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+(?:`?([^`\s.]+)`?\.)?(?:`([^`]+)`|([A-Za-z0-9_]+))/gi;
let match;
tableNamePattern.lastIndex = 0;
while ((match = tableNamePattern.exec(normalizedSQL)) !== null) {
const schemaName = match[1] || 'public';
const tableName = match[2] || match[3]; // match[2] for backtick quoted, match[3] for unquoted
// Skip invalid table names
if (!tableName || tableName.toUpperCase() === 'CREATE') continue;
// Ensure the table is in our tableMap
const tableKey = `${schemaName}.${tableName}`;
if (!tableMap[tableKey]) {
const tableId = generateId();
tableMap[tableKey] = tableId;
}
}
// Now process each CREATE TABLE statement separately to find REFERENCES
const createTableStatements = normalizedSQL.split(';');
for (const stmt of createTableStatements) {
if (!stmt.trim().toUpperCase().startsWith('CREATE TABLE')) continue;
// Extract the table name from the CREATE TABLE statement
const tableMatch = stmt.match(
/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?\s+(?:`?([^`\s.]+)`?\.)?(?:`([^`]+)`|([A-Za-z0-9_]+))/i
);
if (!tableMatch) continue;
const sourceSchema = tableMatch[1] || 'public';
const sourceTable = tableMatch[2] || tableMatch[3];
if (!sourceTable) continue;
// Check if this table has a composite primary key that includes the foreign key columns
const pkMatch = stmt.match(/PRIMARY\s+KEY\s*\(\s*([^)]+)\)/i);
const pkColumns = pkMatch
? pkMatch[1]
.split(',')
.map((col) => col.trim().replace(/[`'"]/g, ''))
: [];
// Find single-column PRIMARY KEY declarations
const singlePkPattern =
/`?(\w+)`?\s+(?:INT|INTEGER|BIGINT|SMALLINT)(?:\([^)]*\))?\s+PRIMARY\s+KEY\b/gi;
let pkMatch2;
while ((pkMatch2 = singlePkPattern.exec(stmt)) !== null) {
const pkCol = pkMatch2[1];
if (!pkColumns.includes(pkCol)) {
pkColumns.push(pkCol);
}
}
// Find FOREIGN KEY constraints
const fkPattern =
/FOREIGN\s+KEY\s*\(\s*`?(\w+)`?\s*\)\s*REFERENCES\s+(?:`?([^`\s.]+)`?|(\w+))\s*\(\s*`?(\w+)`?\s*\)/gi;
let fkMatch;
while ((fkMatch = fkPattern.exec(stmt)) !== null) {
const sourceColumn = fkMatch[1];
const targetTable = fkMatch[2] || fkMatch[3]; // fkMatch[2] for backtick quoted, fkMatch[3] for unquoted
const targetColumn = fkMatch[4];
// Skip if any part is invalid
if (!sourceColumn || !targetTable || !targetColumn) {
continue;
}
// Create a unique key to track this relationship
const relationshipKey = `${sourceTable}.${sourceColumn}-${targetTable}.${targetColumn}`;
// Skip if we've already added this relationship
if (addedRelationships.has(relationshipKey)) {
continue;
}
// Get table IDs
const sourceTableKey = `${sourceSchema}.${sourceTable}`;
const targetTableKey = `${sourceSchema}.${targetTable}`;
const sourceTableId = tableMap[sourceTableKey];
const targetTableId = tableMap[targetTableKey];
// Skip if either table ID is missing
if (!sourceTableId || !targetTableId) {
continue;
}
// Check if this is a one-to-one relationship
const isUnique =
stmt.toLowerCase().includes(`unique (${sourceColumn})`) ||
stmt.toLowerCase().includes(`unique(\`${sourceColumn}\`)`) ||
stmt.toLowerCase().includes(`unique key (${sourceColumn})`) ||
stmt
.toLowerCase()
.includes(`unique key(\`${sourceColumn}\`)`) ||
(pkColumns.length === 1 && pkColumns[0] === sourceColumn);
// For one-to-one relationships, both sides are 'one'
const sourceCardinality = isUnique ? 'one' : 'many';
const targetCardinality = 'one'; // Referenced PK is always one
// Add the relationship
relationships.push({
name: `FK_${sourceTable}_${sourceColumn}_${targetTable}`,
sourceTable,
sourceSchema,
sourceColumn,
targetTable,
targetSchema: sourceSchema,
targetColumn,
sourceTableId,
targetTableId,
sourceCardinality,
targetCardinality,
});
addedRelationships.add(relationshipKey);
}
}
}

View File

@@ -26,6 +26,218 @@ import {
getTableIdWithSchemaSupport,
} from './postgresql-common';
/**
* Uses regular expressions to find foreign key relationships in PostgreSQL SQL content.
* This is a fallback method to catch relationships that might be missed by the parser.
*/
function findForeignKeysUsingRegex(
sqlContent: string,
tableMap: Record<string, string>,
relationships: SQLForeignKey[],
addedRelationships: Set<string>
): void {
// Normalize SQL content: replace multiple whitespaces and newlines with single space
const normalizedSQL = sqlContent
.replace(/\s+/g, ' ')
// Replace common bracket/brace formatting issues
.replace(/\[\s*(\d+)\s*\]/g, '[$1]')
.replace(/\{\s*(\d+)\s*\}/g, '{$1}')
// Normalize commas and parentheses to help regex matching
.replace(/\s*,\s*/g, ', ')
.replace(/\s*\(\s*/g, ' (')
.replace(/\s*\)\s*/g, ') ')
// Ensure spaces around keywords
.replace(/\bREFERENCES\b/g, ' REFERENCES ')
.replace(/\bINT\b/g, ' INT ')
.replace(/\bINTEGER\b/g, ' INTEGER ')
.replace(/\bPRIMARY\s+KEY\b/g, ' PRIMARY KEY ')
.replace(/\bUNIQUE\b/g, ' UNIQUE ')
.replace(/\bFOREIGN\s+KEY\b/g, ' FOREIGN KEY ')
.replace(/\bNOT\s+NULL\b/g, ' NOT NULL ');
// First extract all table names to ensure they're in the tableMap
const tableNamePattern =
/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?(?:\s+ONLY)?\s+(?:"?([^"\s.]+)"?\.)?["'`]?([^"'`\s.(]+)["'`]?/gi;
let match;
tableNamePattern.lastIndex = 0;
while ((match = tableNamePattern.exec(normalizedSQL)) !== null) {
const schemaName = match[1] || 'public';
const tableName = match[2];
// Skip invalid table names
if (!tableName || tableName.toUpperCase() === 'CREATE') continue;
// Ensure the table is in our tableMap
const tableKey = `${schemaName}.${tableName}`;
if (!tableMap[tableKey]) {
const tableId = generateId();
tableMap[tableKey] = tableId;
}
}
// Now process each CREATE TABLE statement separately to find REFERENCES
const createTableStatements = normalizedSQL.split(';');
for (const stmt of createTableStatements) {
if (!stmt.trim().toUpperCase().startsWith('CREATE TABLE')) continue;
// Extract the table name from the CREATE TABLE statement
const tableMatch = stmt.match(
/CREATE\s+TABLE(?:\s+IF\s+NOT\s+EXISTS)?(?:\s+ONLY)?\s+(?:"?([^"\s.]+)"?\.)?["'`]?([^"'`\s.(]+)["'`]?/i
);
if (!tableMatch) continue;
const sourceSchema = tableMatch[1] || 'public';
const sourceTable = tableMatch[2];
if (!sourceTable) continue;
// Find all REFERENCES clauses in this CREATE TABLE statement
// Updated pattern to handle both inline and FOREIGN KEY REFERENCES with better column name capture
const referencesPattern =
/(?:["'`]?(\w+)["'`]?\s+(?:INTEGER|INT|BIGINT|SMALLINT)(?:\s+NOT\s+NULL)?(?:\s+PRIMARY\s+KEY)?\s+REFERENCES\s+["'`]?([^"'`\s.(]+)["'`]?\s*\(\s*["'`]?(\w+)["'`]?\s*\)|FOREIGN\s+KEY\s*\(\s*["'`]?(\w+)["'`]?\s*\)\s*REFERENCES\s+["'`]?([^"'`\s.(]+)["'`]?\s*\(\s*["'`]?(\w+)["'`]?\s*\))/gi;
let refMatch;
while ((refMatch = referencesPattern.exec(stmt)) !== null) {
// Extract source and target info based on which pattern matched
const sourceColumn = refMatch[1] || refMatch[4]; // Column name from either pattern
const targetTable = refMatch[2] || refMatch[5]; // Referenced table from either pattern
const targetColumn = refMatch[3] || refMatch[6]; // Referenced column from either pattern
const targetSchema = 'public'; // Default to public schema
// Skip if any part is invalid
if (!sourceColumn || !targetTable || !targetColumn) {
continue;
}
// Create a unique key to track this relationship
const relationshipKey = `${sourceTable}.${sourceColumn}-${targetTable}.${targetColumn}`;
// Skip if we've already added this relationship
if (addedRelationships.has(relationshipKey)) {
continue;
}
// Get table IDs
const sourceTableKey = `${sourceSchema}.${sourceTable}`;
const targetTableKey = `${targetSchema}.${targetTable}`;
const sourceTableId = tableMap[sourceTableKey];
const targetTableId = tableMap[targetTableKey];
// Skip if either table ID is missing
if (!sourceTableId || !targetTableId) continue;
// Check if this is a one-to-one relationship
const isUnique =
stmt
.toLowerCase()
.includes(
`${sourceColumn.toLowerCase()} integer primary key`
) ||
stmt
.toLowerCase()
.includes(
`${sourceColumn.toLowerCase()} int primary key`
) ||
stmt
.toLowerCase()
.includes(
`"${sourceColumn.toLowerCase()}" integer primary key`
) ||
stmt
.toLowerCase()
.includes(
`"${sourceColumn.toLowerCase()}" int primary key`
);
// For one-to-one relationships, both sides are 'one'
const sourceCardinality = isUnique ? 'one' : 'many';
const targetCardinality = 'one'; // Referenced PK is always one
// Add the relationship
relationships.push({
name: `FK_${sourceTable}_${sourceColumn}_${targetTable}`,
sourceTable,
sourceSchema,
sourceColumn,
targetTable,
targetSchema,
targetColumn,
sourceTableId,
targetTableId,
sourceCardinality,
targetCardinality,
});
addedRelationships.add(relationshipKey);
}
}
}
function getDefaultValueString(
columnDef: ColumnDefinition
): string | undefined {
let defVal = columnDef.default_val;
// Unwrap {type: 'default', value: ...}
if (
defVal &&
typeof defVal === 'object' &&
defVal.type === 'default' &&
'value' in defVal
) {
defVal = defVal.value;
}
if (defVal === undefined || defVal === null) return undefined;
let value: string | undefined;
switch (typeof defVal) {
case 'string':
value = defVal;
break;
case 'number':
value = String(defVal);
break;
case 'boolean':
value = defVal ? 'TRUE' : 'FALSE';
break;
case 'object':
if ('value' in defVal && typeof defVal.value === 'string') {
value = defVal.value;
} else if ('raw' in defVal && typeof defVal.raw === 'string') {
value = defVal.raw;
} else if (defVal.type === 'bool') {
value = defVal.value ? 'TRUE' : 'FALSE';
} else if (defVal.type === 'function' && defVal.name) {
// Handle nested structure: { name: { name: [{ value: ... }] } }
const fnName = defVal.name;
if (
fnName &&
typeof fnName === 'object' &&
Array.isArray(fnName.name) &&
fnName.name.length > 0 &&
fnName.name[0].value
) {
value = fnName.name[0].value.toUpperCase();
} else if (typeof fnName === 'string') {
value = fnName.toUpperCase();
} else {
value = 'UNKNOWN_FUNCTION';
}
} else {
const built = buildSQLFromAST(defVal);
value =
typeof built === 'string' ? built : JSON.stringify(built);
}
break;
default:
value = undefined;
}
return value;
}
// PostgreSQL-specific parsing logic
export async function fromPostgres(
sqlContent: string
@@ -33,6 +245,7 @@ export async function fromPostgres(
const tables: SQLTable[] = [];
const relationships: SQLForeignKey[] = [];
const tableMap: Record<string, string> = {}; // Maps table name to its ID
const addedRelationships = new Set<string>(); // Initialize set to track added FKs
try {
const { Parser } = await import('node-sql-parser');
@@ -44,7 +257,42 @@ export async function fromPostgres(
throw new Error('Failed to parse SQL DDL - AST is not an array');
}
// Process each CREATE TABLE statement
// Process each CREATE TABLE statement first to build tableMap
ast.forEach((stmt: SQLAstNode) => {
if (stmt.type === 'create' && stmt.keyword === 'table') {
const createTableStmt = stmt as CreateTableStatement;
let tableName = '';
let schemaName = '';
if (
createTableStmt.table &&
typeof createTableStmt.table === 'object'
) {
if (
Array.isArray(createTableStmt.table) &&
createTableStmt.table.length > 0
) {
const tableObj = createTableStmt.table[0];
tableName = tableObj.table || '';
schemaName = tableObj.schema || tableObj.db || '';
} else {
const tableObj =
createTableStmt.table as TableReference;
tableName = tableObj.table || '';
schemaName = tableObj.schema || tableObj.db || '';
}
}
if (!tableName) return;
if (!schemaName) schemaName = 'public';
const tableId = generateId();
const tableKey = `${schemaName}.${tableName}`;
tableMap[tableKey] = tableId;
}
});
// Now process tables and relationships
ast.forEach((stmt: SQLAstNode) => {
if (stmt.type === 'create' && stmt.keyword === 'table') {
// Extract table name and schema
@@ -114,8 +362,22 @@ export async function fromPostgres(
const columnName = extractColumnName(
columnDef.column
);
const dataType =
columnDef.definition?.dataType || '';
const rawDataType =
columnDef.definition?.dataType?.toUpperCase() ||
'';
let finalDataType = rawDataType;
let isSerialType = false;
if (rawDataType === 'SERIAL') {
finalDataType = 'INTEGER';
isSerialType = true;
} else if (rawDataType === 'BIGSERIAL') {
finalDataType = 'BIGINT';
isSerialType = true;
} else if (rawDataType === 'SMALLSERIAL') {
finalDataType = 'SMALLINT';
isSerialType = true;
}
// Handle the column definition and add to columns array
if (columnName) {
@@ -128,23 +390,24 @@ export async function fromPostgres(
columns.push({
name: columnName,
type: dataType,
nullable:
columnDef.nullable?.type !==
'not null',
primaryKey: isPrimaryKey,
type: finalDataType,
nullable: isSerialType
? false
: columnDef.nullable?.type !==
'not null',
primaryKey:
isPrimaryKey || isSerialType,
unique: columnDef.unique === 'unique',
typeArgs: getTypeArgs(
columnDef.definition
),
default: columnDef.default_val
? buildSQLFromAST(
columnDef.default_val
)
: undefined,
default: isSerialType
? undefined
: getDefaultValueString(columnDef),
increment:
isSerialType ||
columnDef.auto_increment ===
'auto_increment',
'auto_increment',
});
}
} else if (def.resource === 'constraint') {
@@ -489,6 +752,8 @@ export async function fromPostgres(
reference.on_update,
deleteAction:
reference.on_delete,
sourceCardinality: 'many',
targetCardinality: 'one',
};
relationships.push(fk);
@@ -822,6 +1087,8 @@ export async function fromPostgres(
targetTableId,
updateAction,
deleteAction,
sourceCardinality: 'many',
targetCardinality: 'one',
};
relationships.push(fk);
@@ -840,39 +1107,35 @@ export async function fromPostgres(
}
});
// Update table IDs in relationships and fix missing target table IDs
relationships.forEach((rel) => {
// Ensure schemas are set to 'public' if empty
if (!rel.sourceSchema) rel.sourceSchema = 'public';
if (!rel.targetSchema) rel.targetSchema = 'public';
// Only check/fix sourceTableId if not already set
if (!rel.sourceTableId) {
rel.sourceTableId =
getTableIdWithSchemaSupport(
tableMap,
rel.sourceTable,
rel.sourceSchema
) || '';
}
// Check/fix targetTableId if not already set
if (!rel.targetTableId) {
rel.targetTableId =
getTableIdWithSchemaSupport(
tableMap,
rel.targetTable,
rel.targetSchema
) || '';
}
});
// Filter out relationships with missing source table IDs or target table IDs
const validRelationships = relationships.filter(
(rel) => rel.sourceTableId && rel.targetTableId
// Use regex as fallback to find additional foreign keys that the parser may have missed
findForeignKeysUsingRegex(
sqlContent,
tableMap,
relationships,
addedRelationships
);
return { tables, relationships: validRelationships };
// Filter out any duplicate relationships that might have been added
const uniqueRelationships = relationships.filter((rel, index) => {
const key = `${rel.sourceTable}.${rel.sourceColumn}-${rel.targetTable}.${rel.targetColumn}`;
return (
index ===
relationships.findIndex(
(r) =>
`${r.sourceTable}.${r.sourceColumn}-${r.targetTable}.${r.targetColumn}` ===
key
)
);
});
// Sort relationships for consistent output
uniqueRelationships.sort((a, b) => {
const keyA = `${a.sourceTable}.${a.sourceColumn}-${a.targetTable}.${a.targetColumn}`;
const keyB = `${b.sourceTable}.${b.sourceColumn}-${b.targetTable}.${b.targetColumn}`;
return keyA.localeCompare(keyB);
});
return { tables, relationships: uniqueRelationships };
} catch (error: unknown) {
throw new Error(
`Error parsing PostgreSQL SQL: ${(error as Error).message}`

View File

@@ -32,12 +32,43 @@ export async function fromSQLite(sqlContent: string): Promise<SQLParserResult> {
const tableMap: Record<string, string> = {}; // Maps table name to its ID
try {
// SPECIAL HANDLING: Direct line-by-line parser for SQLite DDL
// This ensures we preserve the exact data types from the original DDL
const directlyParsedTables = parseCreateTableStatements(sqlContent);
// Check if we successfully parsed tables directly
if (directlyParsedTables.length > 0) {
// Map the direct parsing results to the expected SQLParserResult format
directlyParsedTables.forEach((table) => {
const tableId = getTableIdWithSchemaSupport(table.name);
tableMap[table.name] = tableId;
// Add the table with its columns
tables.push({
id: tableId,
name: table.name,
columns: table.columns,
indexes: [],
order: tables.length,
});
});
// Process foreign keys using the regex approach
findForeignKeysUsingRegex(sqlContent, tableMap, relationships);
// Return the result
return { tables, relationships };
}
// Preprocess SQL to handle SQLite quoted identifiers
const preprocessedSQL = preprocessSQLiteDDL(sqlContent);
// Parse the SQL DDL statements
const { Parser } = await import('node-sql-parser');
const parser = new Parser();
const ast = parser.astify(
sqlContent,
preprocessedSQL,
parserOpts
) as unknown as SQLASTNode[];
@@ -87,6 +118,141 @@ export async function fromSQLite(sqlContent: string): Promise<SQLParserResult> {
}
}
/**
* Parse SQLite CREATE TABLE statements directly to preserve exact type information
*/
function parseCreateTableStatements(sqlContent: string): {
name: string;
columns: SQLColumn[];
}[] {
const tables: {
name: string;
columns: SQLColumn[];
}[] = [];
// Split SQL content into lines
const lines = sqlContent.split('\n');
let currentTable: { name: string; columns: SQLColumn[] } | null = null;
let inCreateTable = false;
// Process each line
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
// Skip empty lines and comments
if (!line || line.startsWith('--')) {
continue;
}
// Check for CREATE TABLE statement
if (line.toUpperCase().startsWith('CREATE TABLE')) {
// Extract table name
const tableNameMatch =
/CREATE\s+TABLE\s+(?:if\s+not\s+exists\s+)?["'`]?(\w+)["'`]?/i.exec(
line
);
if (tableNameMatch && tableNameMatch[1]) {
inCreateTable = true;
currentTable = {
name: tableNameMatch[1],
columns: [],
};
}
}
// Check for end of CREATE TABLE statement
else if (inCreateTable && line.includes(');')) {
if (currentTable) {
tables.push(currentTable);
}
inCreateTable = false;
currentTable = null;
}
// Process column definitions inside CREATE TABLE
else if (inCreateTable && currentTable && line.includes('"')) {
// Column line pattern optimized for user's DDL format
const columnPattern = /\s*["'`](\w+)["'`]\s+([A-Za-z0-9_]+)(.+)?/i;
const match = columnPattern.exec(line);
if (match) {
const columnName = match[1];
const rawType = match[2].toUpperCase();
const restOfLine = match[3] || '';
// Determine column properties
const isPrimaryKey = restOfLine
.toUpperCase()
.includes('PRIMARY KEY');
const isNotNull = restOfLine.toUpperCase().includes('NOT NULL');
const isUnique = restOfLine.toUpperCase().includes('UNIQUE');
// Extract default value
let defaultValue = '';
const defaultMatch = /DEFAULT\s+([^,\s)]+)/i.exec(restOfLine);
if (defaultMatch) {
defaultValue = defaultMatch[1];
}
// Map to appropriate SQLite storage class
let columnType = rawType;
if (rawType === 'INTEGER' || rawType === 'INT') {
columnType = 'INTEGER';
} else if (
['REAL', 'FLOAT', 'DOUBLE', 'NUMERIC', 'DECIMAL'].includes(
rawType
)
) {
columnType = 'REAL';
} else if (rawType === 'BLOB' || rawType === 'BINARY') {
columnType = 'BLOB';
} else if (
['TIMESTAMP', 'DATETIME', 'DATE'].includes(rawType)
) {
columnType = 'TIMESTAMP';
} else {
columnType = 'TEXT';
}
// Add column to the table
currentTable.columns.push({
name: columnName,
type: columnType,
nullable: !isNotNull,
primaryKey: isPrimaryKey,
unique: isUnique || isPrimaryKey,
default: defaultValue,
increment: isPrimaryKey && columnType === 'INTEGER',
});
}
}
}
return tables;
}
/**
* Preprocess SQLite DDL to handle specific syntax issues that might cause parsing problems
*/
function preprocessSQLiteDDL(sqlContent: string): string {
// Replace quoted identifiers with their unquoted equivalents
let processedSQL = sqlContent;
// Handle column type declarations with quotes around them
// For example: "id" "TEXT" PRIMARY KEY -> "id" TEXT PRIMARY KEY
processedSQL = processedSQL.replace(
/(['"`])(\w+)(['"`])\s+(['"`])(\w+)(['"`])/g,
(_match, q1, col, q2, ...rest) => {
// Extract the type from rest parameters
// match, q1, col, q2, q3, type, q4
const type = rest[1];
// Preserve the quotes around column name, but remove quotes around type
return `${q1}${col}${q2} ${type}`;
}
);
return processedSQL;
}
/**
* Process a CREATE TABLE statement to extract table and column information
*/
@@ -139,6 +305,7 @@ function processCreateTableStatement(
createTableStmt.create_definitions &&
Array.isArray(createTableStmt.create_definitions)
) {
// First pass - collect column information from the SQL
createTableStmt.create_definitions.forEach((def) => {
if ('column' in def) {
// Process column definition
@@ -158,7 +325,34 @@ function processCreateTableStatement(
};
if (columnDef.dataType) {
typeName = columnDef.dataType.dataType || 'text';
// Get the raw data type string and clean it up
typeName =
columnDef.dataType.dataType?.toUpperCase() || 'TEXT';
// Set the exact type according to SQLite's type system
// SQLite has 5 storage classes: NULL, INTEGER, REAL, TEXT, and BLOB
if (typeName === 'INTEGER' || typeName === 'INT') {
typeName = 'INTEGER';
} else if (
typeName === 'REAL' ||
typeName === 'FLOAT' ||
typeName === 'DOUBLE' ||
typeName === 'NUMERIC' ||
typeName === 'DECIMAL'
) {
typeName = 'REAL';
} else if (typeName === 'BLOB') {
typeName = 'BLOB';
} else if (
typeName === 'TIMESTAMP' ||
typeName === 'DATETIME' ||
typeName === 'DATE'
) {
typeName = 'TIMESTAMP'; // Preserve TIMESTAMP as a special type
} else {
typeName = 'TEXT'; // Default SQLite type
}
const args = getTypeArgs(columnDef.dataType);
typeArgs.length = args.size > 0 ? args.size : undefined;
typeArgs.precision = args.precision;

View File

@@ -9,6 +9,25 @@ import { genericDataTypes } from '@/lib/data/data-types/generic-data-types';
import { randomColor } from '@/lib/colors';
import { DatabaseType } from '@/lib/domain/database-type';
// Simple function to replace Spanish special characters
export const sanitizeDBML = (content: string): string => {
return content
.replace(/[áàäâ]/g, 'a')
.replace(/[éèëê]/g, 'e')
.replace(/[íìïî]/g, 'i')
.replace(/[óòöô]/g, 'o')
.replace(/[úùüû]/g, 'u')
.replace(/[ñ]/g, 'n')
.replace(/[ç]/g, 'c')
.replace(/Á/g, 'A')
.replace(/É/g, 'E')
.replace(/Í/g, 'I')
.replace(/Ó/g, 'O')
.replace(/Ú/g, 'U')
.replace(/Ñ/g, 'N')
.replace(/Ç/g, 'C');
};
interface DBMLTypeArgs {
length?: number;
precision?: number;
@@ -108,7 +127,9 @@ export const importDBMLToDiagram = async (
): Promise<Diagram> => {
try {
const parser = new Parser();
const parsedData = parser.parse(dbmlContent, 'dbml');
// Sanitize DBML content to remove special characters
const sanitizedContent = sanitizeDBML(dbmlContent);
const parsedData = parser.parse(sanitizedContent, 'dbml');
const dbmlData = parsedData.schemas[0];
// Extract only the necessary data from the parsed DBML

23
src/lib/domain/area.ts Normal file
View File

@@ -0,0 +1,23 @@
import { z } from 'zod';
export interface Area {
id: string;
name: string;
x: number;
y: number;
width: number;
height: number;
color: string;
order?: number;
}
export const areaSchema: z.ZodType<Area> = z.object({
id: z.string(),
name: z.string(),
x: z.number(),
y: z.number(),
width: z.number(),
height: z.number(),
color: z.string(),
order: z.number().optional(),
});

View File

@@ -14,6 +14,7 @@ export interface DBField {
primaryKey: boolean;
unique: boolean;
nullable: boolean;
increment?: boolean;
createdAt: number;
characterMaximumLength?: string;
precision?: number;
@@ -30,6 +31,7 @@ export const dbFieldSchema: z.ZodType<DBField> = z.object({
primaryKey: z.boolean(),
unique: z.boolean(),
nullable: z.boolean(),
increment: z.boolean().optional(),
createdAt: z.number(),
characterMaximumLength: z.string().optional(),
precision: z.number().optional(),

View File

@@ -25,6 +25,7 @@ import {
import { DatabaseType } from './database-type';
import type { DatabaseMetadata } from '../data/import-metadata/metadata-types/database-metadata';
import { z } from 'zod';
import { getTableDimensions } from '@/pages/editor-page/canvas/canvas-utils';
export interface DBTable {
id: string;
@@ -41,6 +42,7 @@ export interface DBTable {
width?: number;
comments?: string;
order?: number;
expanded?: boolean;
}
export const dbTableSchema: z.ZodType<DBTable> = z.object({
@@ -58,6 +60,7 @@ export const dbTableSchema: z.ZodType<DBTable> = z.object({
width: z.number().optional(),
comments: z.string().optional(),
order: z.number().optional(),
expanded: z.boolean().optional(),
});
export const shouldShowTablesBySchemaFilter = (
@@ -175,8 +178,8 @@ export const adjustTablePositions = ({
const relationships = deepCopy(inputRelationships);
const adjustPositionsForTables = (tablesToAdjust: DBTable[]) => {
const tableWidth = 200;
const tableHeight = 300;
const defaultTableWidth = 200;
const defaultTableHeight = 300;
const gapX = 100;
const gapY = 100;
const startX = 100;
@@ -205,6 +208,20 @@ export const adjustTablePositions = ({
const positionedTables = new Set<string>();
const tablePositions = new Map<string, { x: number; y: number }>();
const getTableWidthAndHeight = (
tableId: string
): {
width: number;
height: number;
} => {
const table = tablesToAdjust.find((t) => t.id === tableId);
if (!table)
return { width: defaultTableWidth, height: defaultTableHeight };
return getTableDimensions(table);
};
const isOverlapping = (
x: number,
y: number,
@@ -212,9 +229,11 @@ export const adjustTablePositions = ({
): boolean => {
for (const [tableId, pos] of tablePositions) {
if (tableId === currentTableId) continue;
const { width, height } = getTableWidthAndHeight(tableId);
if (
Math.abs(x - pos.x) < tableWidth + gapX &&
Math.abs(y - pos.y) < tableHeight + gapY
Math.abs(x - pos.x) < width + gapX &&
Math.abs(y - pos.y) < height + gapY
) {
return true;
}
@@ -227,7 +246,8 @@ export const adjustTablePositions = ({
baseY: number,
tableId: string
): { x: number; y: number } => {
const spiralStep = Math.max(tableWidth, tableHeight) / 2;
const { width, height } = getTableWidthAndHeight(tableId);
const spiralStep = Math.max(width, height) / 2;
let angle = 0;
let radius = 0;
let iterations = 0;
@@ -279,10 +299,21 @@ export const adjustTablePositions = ({
(t) => t.id === connectedTableId
);
if (connectedTable) {
const { width: tableWidth, height: tableHeight } =
getTableWidthAndHeight(table.id);
const {
width: connectedTableWidth,
height: connectedTableHeight,
} = getTableWidthAndHeight(connectedTableId);
const avgWidth = (tableWidth + connectedTableWidth) / 2;
const avgHeight =
(tableHeight + connectedTableHeight) / 2;
const newX =
x + Math.cos(angle) * (tableWidth + gapX * 2);
x + Math.cos(angle) * (avgWidth + gapX * 2);
const newY =
y + Math.sin(angle) * (tableHeight + gapY * 2);
y + Math.sin(angle) * (avgHeight + gapY * 2);
positionTable(connectedTable, newX, newY);
angle += angleStep;
}
@@ -295,6 +326,9 @@ export const adjustTablePositions = ({
if (!positionedTables.has(table.id)) {
const row = Math.floor(index / 6);
const col = index % 6;
const { width: tableWidth, height: tableHeight } =
getTableWidthAndHeight(table.id);
const x = startX + col * (tableWidth + gapX * 2);
const y = startY + row * (tableHeight + gapY * 2);
positionTable(table, x, y);

View File

@@ -19,6 +19,7 @@ import {
dbTableSchema,
} from './db-table';
import { generateDiagramId } from '@/lib/utils';
import { areaSchema, type Area } from './area';
export interface Diagram {
id: string;
name: string;
@@ -27,6 +28,7 @@ export interface Diagram {
tables?: DBTable[];
relationships?: DBRelationship[];
dependencies?: DBDependency[];
areas?: Area[];
createdAt: Date;
updatedAt: Date;
}
@@ -39,6 +41,7 @@ export const diagramSchema: z.ZodType<Diagram> = z.object({
tables: z.array(dbTableSchema).optional(),
relationships: z.array(dbRelationshipSchema).optional(),
dependencies: z.array(dbDependencySchema).optional(),
areas: z.array(areaSchema).optional(),
createdAt: z.date(),
updatedAt: z.date(),
});

View File

@@ -101,3 +101,25 @@ export const sha256 = async (message: string): Promise<string> => {
return hashHex;
};
export function mergeRefs<T>(
...inputRefs: (React.Ref<T> | undefined)[]
): React.Ref<T> | React.RefCallback<T> {
const filteredInputRefs = inputRefs.filter(Boolean);
if (filteredInputRefs.length <= 1) {
const firstRef = filteredInputRefs[0];
return firstRef || null;
}
return function mergedRefs(ref) {
for (const inputRef of filteredInputRefs) {
if (typeof inputRef === 'function') {
inputRef(ref);
} else if (inputRef) {
(inputRef as React.MutableRefObject<T | null>).current = ref;
}
}
};
}

View File

@@ -0,0 +1,138 @@
import React, { useCallback, useState } from 'react';
import type { NodeProps, Node } from '@xyflow/react';
import { NodeResizer } from '@xyflow/react';
import type { Area } from '@/lib/domain/area';
import { useChartDB } from '@/hooks/use-chartdb';
import { Input } from '@/components/input/input';
import { useClickAway, useKeyPressEvent } from 'react-use';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { Check, GripVertical } from 'lucide-react';
import { Button } from '@/components/button/button';
import { useLayout } from '@/hooks/use-layout';
export type AreaNodeType = Node<
{
area: Area;
},
'area'
>;
export const AreaNode: React.FC<NodeProps<AreaNodeType>> = React.memo(
({ selected, dragging, data: { area } }) => {
const { updateArea, readonly } = useChartDB();
const { t } = useTranslation();
const [editMode, setEditMode] = useState(false);
const [areaName, setAreaName] = useState(area.name);
const inputRef = React.useRef<HTMLInputElement>(null);
const { openAreaFromSidebar, selectSidebarSection } = useLayout();
const focused = !!selected && !dragging;
const editAreaName = useCallback(() => {
if (!editMode) return;
if (areaName.trim()) {
updateArea(area.id, { name: areaName.trim() });
}
setEditMode(false);
}, [areaName, area.id, updateArea, editMode]);
const abortEdit = useCallback(() => {
setEditMode(false);
setAreaName(area.name);
}, [area.name]);
const openAreaInEditor = useCallback(() => {
selectSidebarSection('areas');
openAreaFromSidebar(area.id);
}, [selectSidebarSection, openAreaFromSidebar, area.id]);
useClickAway(inputRef, editAreaName);
useKeyPressEvent('Enter', editAreaName);
useKeyPressEvent('Escape', abortEdit);
const enterEditMode = (e: React.MouseEvent) => {
e.stopPropagation();
setEditMode(true);
};
return (
<div
className={cn(
'flex h-full flex-col rounded-md border-2 shadow-sm',
selected ? 'border-pink-600' : 'border-transparent'
)}
style={{
backgroundColor: `${area.color}15`,
borderColor: selected ? undefined : area.color,
}}
onClick={(e) => {
if (e.detail === 2) {
openAreaInEditor();
}
}}
>
<NodeResizer
isVisible={focused}
lineClassName="!border-4 !border-transparent"
handleClassName="!h-3 !w-3 !rounded-full !bg-pink-600"
/>
<div className="group flex h-8 items-center justify-between rounded-t-md px-2">
<div className="flex w-full items-center gap-1">
<GripVertical className="size-4 text-slate-700 opacity-60 dark:text-slate-300" />
{editMode && !readonly ? (
<div className="flex w-full items-center">
<Input
ref={inputRef}
autoFocus
type="text"
placeholder={area.name}
value={areaName}
onClick={(e) => e.stopPropagation()}
onChange={(e) =>
setAreaName(e.target.value)
}
className="h-6 bg-white/70 focus-visible:ring-0 dark:bg-slate-900/70"
/>
<Button
variant="ghost"
className="ml-1 size-6 p-0 hover:bg-white/20"
onClick={editAreaName}
>
<Check className="size-3.5 text-slate-700 dark:text-slate-300" />
</Button>
</div>
) : !readonly ? (
<Tooltip>
<TooltipTrigger asChild>
<div
className="text-editable max-w-[200px] cursor-text truncate px-1 py-0.5 text-base font-semibold text-slate-700 dark:text-slate-300"
onDoubleClick={enterEditMode}
>
{area.name}
</div>
</TooltipTrigger>
<TooltipContent>
{t('tool_tips.double_click_to_edit')}
</TooltipContent>
</Tooltip>
) : (
<div className="truncate px-1 py-0.5 text-base font-semibold text-slate-700 dark:text-slate-300">
{area.name}
</div>
)}
</div>
</div>
<div className="flex-1" />
</div>
);
}
);
AreaNode.displayName = 'AreaNode';

View File

@@ -10,11 +10,13 @@ import { useDialog } from '@/hooks/use-dialog';
import { useReactFlow } from '@xyflow/react';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Table, Workflow, Group } from 'lucide-react';
export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const { createTable, filteredSchemas, schemas, readonly } = useChartDB();
const { createTable, filteredSchemas, schemas, readonly, createArea } =
useChartDB();
const { openCreateRelationshipDialog, openTableSchemaDialog } = useDialog();
const { screenToFlowPosition } = useReactFlow();
const { t } = useTranslation();
@@ -61,6 +63,21 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
]
);
const createAreaHandler = useCallback(
(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const position = screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
createArea({
x: position.x,
y: position.y,
});
},
[createArea, screenToFlowPosition]
);
const createRelationshipHandler = useCallback(() => {
openCreateRelationshipDialog();
}, [openCreateRelationshipDialog]);
@@ -73,11 +90,26 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
<ContextMenu>
<ContextMenuTrigger>{children}</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem onClick={createTableHandler}>
<ContextMenuItem
onClick={createTableHandler}
className="flex justify-between gap-4"
>
{t('canvas_context_menu.new_table')}
<Table className="size-3.5" />
</ContextMenuItem>
<ContextMenuItem onClick={createRelationshipHandler}>
<ContextMenuItem
onClick={createRelationshipHandler}
className="flex justify-between gap-4"
>
{t('canvas_context_menu.new_relationship')}
<Workflow className="size-3.5" />
</ContextMenuItem>
<ContextMenuItem
onClick={createAreaHandler}
className="flex justify-between gap-4"
>
{t('canvas_context_menu.new_area')}
<Group className="size-3.5" />
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>

View File

@@ -1,5 +1,9 @@
import type { Cardinality } from '@/lib/domain/db-relationship';
import { MIN_TABLE_SIZE, type TableNodeType } from './table-node/table-node';
import {
MIN_TABLE_SIZE,
TABLE_MINIMIZED_FIELDS,
type TableNodeType,
} from './table-node/table-node';
import { addEdge, createGraph, removeEdge, type Graph } from '@/lib/graph';
import type { DBTable } from '@/lib/domain/db-table';
@@ -27,9 +31,8 @@ const calcRect = ({
table?.width ??
MIN_TABLE_SIZE;
const height = node
? (node?.measured?.height ??
calcTableHeight(node?.data.table.fields.length ?? 0))
: calcTableHeight(table?.fields.length ?? 0);
? (node?.measured?.height ?? calcTableHeight(node.data.table))
: calcTableHeight(table);
return {
id,
@@ -108,17 +111,35 @@ export const findOverlappingTables = ({
return graph;
};
export const calcTableHeight = (fieldCount: number): number => {
const fieldHeight = 32; // h-8 per field
export const calcTableHeight = (table?: DBTable): number => {
if (!table) {
return 300;
}
return Math.min(fieldCount, 11) * fieldHeight + 48;
const FIELD_HEIGHT = 32; // h-8 per field
const TABLE_FOOTER_HEIGHT = 32; // h-8 for show more button
const TABLE_HEADER_HEIGHT = 42;
// Calculate how many fields are visible
const fieldCount = table.fields.length;
let visibleFieldCount = fieldCount;
// If not expanded, use minimum of field count and TABLE_MINIMIZED_FIELDS
if (!table.expanded) {
visibleFieldCount = Math.min(fieldCount, TABLE_MINIMIZED_FIELDS);
}
// Calculate height based on visible fields
const fieldsHeight = visibleFieldCount * FIELD_HEIGHT;
const showMoreButtonHeight =
fieldCount > TABLE_MINIMIZED_FIELDS ? TABLE_FOOTER_HEIGHT : 0;
return TABLE_HEADER_HEIGHT + fieldsHeight + showMoreButtonHeight;
};
export const getTableDimensions = (
table: DBTable
): { width: number; height: number } => {
const fieldCount = table.fields.length;
const height = calcTableHeight(fieldCount);
const height = calcTableHeight(table);
const width = table.width || MIN_TABLE_SIZE;
return { width, height };
};

View File

@@ -12,6 +12,9 @@ import type {
NodeDimensionChange,
OnEdgesChange,
OnNodesChange,
NodeTypes,
EdgeTypes,
NodeChange,
} from '@xyflow/react';
import {
ReactFlow,
@@ -28,8 +31,8 @@ import '@xyflow/react/dist/style.css';
import equal from 'fast-deep-equal';
import type { TableNodeType } from './table-node/table-node';
import { MIN_TABLE_SIZE, TableNode } from './table-node/table-node';
import type { RelationshipEdgeType } from './relationship-edge';
import { RelationshipEdge } from './relationship-edge';
import type { RelationshipEdgeType } from './relationship-edge/relationship-edge';
import { RelationshipEdge } from './relationship-edge/relationship-edge';
import { useChartDB } from '@/hooks/use-chartdb';
import {
LEFT_HANDLE_ID_PREFIX,
@@ -64,8 +67,8 @@ import type { Graph } from '@/lib/graph';
import { removeVertex } from '@/lib/graph';
import type { ChartDBEvent } from '@/context/chartdb-context/chartdb-context';
import { cn, debounce, getOperatingSystem } from '@/lib/utils';
import type { DependencyEdgeType } from './dependency-edge';
import { DependencyEdge } from './dependency-edge';
import type { DependencyEdgeType } from './dependency-edge/dependency-edge';
import { DependencyEdge } from './dependency-edge/dependency-edge';
import {
BOTTOM_SOURCE_HANDLE_ID_PREFIX,
TARGET_DEP_PREFIX,
@@ -74,16 +77,26 @@ import {
import { DatabaseType } from '@/lib/domain/database-type';
import { useAlert } from '@/context/alert-context/alert-context';
import { useCanvas } from '@/hooks/use-canvas';
import type { AreaNodeType } from './area-node/area-node';
import { AreaNode } from './area-node/area-node';
import type { Area } from '@/lib/domain/area';
export type EdgeType = RelationshipEdgeType | DependencyEdgeType;
export type NodeType = TableNodeType | AreaNodeType;
type AddEdgeParams = Parameters<typeof addEdge<EdgeType>>[0];
const edgeTypes = {
const edgeTypes: EdgeTypes = {
'relationship-edge': RelationshipEdge,
'dependency-edge': DependencyEdge,
};
const nodeTypes: NodeTypes = {
table: TableNode,
area: AreaNode,
};
const initialEdges: EdgeType[] = [];
const tableToTableNode = (
@@ -101,6 +114,16 @@ const tableToTableNode = (
hidden: !shouldShowTablesBySchemaFilter(table, filteredSchemas),
});
const areaToAreaNode = (area: Area): AreaNodeType => ({
id: area.id,
type: 'area',
position: { x: area.x, y: area.y },
data: { area },
width: area.width,
height: area.height,
zIndex: -10,
});
export interface CanvasProps {
initialTables: DBTable[];
}
@@ -115,6 +138,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
const { t } = useTranslation();
const {
tables,
areas,
relationships,
createRelationship,
createDependency,
@@ -127,6 +151,8 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
events,
dependencies,
readonly,
removeArea,
updateArea,
} = useChartDB();
const { showSidePanel } = useLayout();
const { effectiveTheme } = useTheme();
@@ -134,7 +160,6 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
useLocalConfig();
const { showAlert } = useAlert();
const { isMd: isDesktop } = useBreakpoint('md');
const nodeTypes = useMemo(() => ({ table: TableNode }), []);
const [highlightOverlappingTables, setHighlightOverlappingTables] =
useState(false);
const { reorderTables, fitView, setOverlapGraph, overlapGraph } =
@@ -142,7 +167,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
const [isInitialLoadingNodes, setIsInitialLoadingNodes] = useState(true);
const [nodes, setNodes, onNodesChange] = useNodesState<TableNodeType>(
const [nodes, setNodes, onNodesChange] = useNodesState<NodeType>(
initialTables.map((table) => tableToTableNode(table, filteredSchemas))
);
const [edges, setEdges, onEdgesChange] =
@@ -299,8 +324,8 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
}, [selectedRelationshipIds, selectedTableIds, setEdges, getEdges]);
useEffect(() => {
setNodes(
tables.map((table) => {
setNodes([
...tables.map((table) => {
const isOverlapping =
(overlapGraph.graph.get(table.id) ?? []).length > 0;
const node = tableToTableNode(table, filteredSchemas);
@@ -313,10 +338,12 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
highlightOverlappingTables,
},
};
})
);
}),
...areas.map(areaToAreaNode),
]);
}, [
tables,
areas,
setNodes,
filteredSchemas,
overlapGraph.lastUpdated,
@@ -463,17 +490,43 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
let newOverlappingGraph: Graph<string> = overlapGraph;
for (const change of positionChanges) {
const node = getNode(change.id) as NodeType;
if (!node) {
continue;
}
if (node.type !== 'table') {
continue;
}
newOverlappingGraph = findTableOverlapping(
{ node: getNode(change.id) as TableNodeType },
{ nodes: nodes.filter((node) => !node.hidden) },
{ node: node as TableNodeType },
{
nodes: nodes.filter(
(node) => !node.hidden && node.type === 'table'
) as TableNodeType[],
},
newOverlappingGraph
);
}
for (const change of sizeChanges) {
const node = getNode(change.id) as NodeType;
if (!node) {
continue;
}
if (node.type !== 'table') {
continue;
}
newOverlappingGraph = findTableOverlapping(
{ node: getNode(change.id) as TableNodeType },
{ nodes: nodes.filter((node) => !node.hidden) },
{ node: node as TableNodeType },
{
nodes: nodes.filter(
(node) => !node.hidden && node.type === 'table'
) as TableNodeType[],
},
newOverlappingGraph
);
}
@@ -489,7 +542,52 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
200
);
const onNodesChangeHandler: OnNodesChange<TableNodeType> = useCallback(
const findRelevantNodesChanges = useCallback(
(changes: NodeChange<NodeType>[], type: NodeType['type']) => {
const relevantChanges = changes.filter((change) => {
if (
change.type === 'position' ||
change.type === 'dimensions' ||
change.type === 'remove'
) {
const node = getNode(change.id);
if (!node) {
return false;
}
if (node.type !== type) {
return false;
}
return true;
}
return false;
});
const positionChanges: NodePositionChange[] =
relevantChanges.filter(
(change) => change.type === 'position' && !change.dragging
) as NodePositionChange[];
const removeChanges: NodeRemoveChange[] = relevantChanges.filter(
(change) => change.type === 'remove'
) as NodeRemoveChange[];
const sizeChanges: NodeDimensionChange[] = relevantChanges.filter(
(change) => change.type === 'dimensions' && change.resizing
) as NodeDimensionChange[];
return {
positionChanges,
removeChanges,
sizeChanges,
};
},
[getNode]
);
const onNodesChangeHandler: OnNodesChange<NodeType> = useCallback(
(changes) => {
let changesToApply = changes;
@@ -499,17 +597,9 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
);
}
const positionChanges: NodePositionChange[] = changesToApply.filter(
(change) => change.type === 'position' && !change.dragging
) as NodePositionChange[];
const removeChanges: NodeRemoveChange[] = changesToApply.filter(
(change) => change.type === 'remove'
) as NodeRemoveChange[];
const sizeChanges: NodeDimensionChange[] = changesToApply.filter(
(change) => change.type === 'dimensions' && change.resizing
) as NodeDimensionChange[];
// Handle table changes
const { positionChanges, removeChanges, sizeChanges } =
findRelevantNodesChanges(changesToApply, 'table');
if (
positionChanges.length > 0 ||
@@ -560,12 +650,52 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
sizeChanges,
});
// Handle area changes
const {
positionChanges: areaPositionChanges,
removeChanges: areaRemoveChanges,
sizeChanges: areaSizeChanges,
} = findRelevantNodesChanges(changesToApply, 'area');
if (
areaPositionChanges.length > 0 ||
areaRemoveChanges.length > 0 ||
areaSizeChanges.length > 0
) {
[...areaPositionChanges, ...areaSizeChanges].forEach(
(change) => {
const updateData: Partial<Area> = {};
if (change.type === 'position') {
updateData.x = change.position?.x;
updateData.y = change.position?.y;
}
if (change.type === 'dimensions') {
updateData.width = change.dimensions?.width;
updateData.height = change.dimensions?.height;
}
if (Object.keys(updateData).length > 0) {
updateArea(change.id, updateData);
}
}
);
areaRemoveChanges.forEach((change) => {
removeArea(change.id);
});
}
return onNodesChange(changesToApply);
},
[
onNodesChange,
updateTablesState,
updateOverlappingGraphOnChangesDebounced,
findRelevantNodesChanges,
updateArea,
removeArea,
readonly,
]
);
@@ -577,7 +707,11 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
for (const table of event.data.tables) {
newOverlappingGraph = findTableOverlapping(
{ node: getNode(table.id) as TableNodeType },
{ nodes: nodes.filter((node) => !node.hidden) },
{
nodes: nodes.filter(
(node) => !node.hidden && node.type === 'table'
) as TableNodeType[],
},
overlapGraph
);
}
@@ -610,7 +744,11 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
measured,
},
},
{ nodes: nodes.filter((node) => !node.hidden) },
{
nodes: nodes.filter(
(node) => !node.hidden && node.type === 'table'
) as TableNodeType[],
},
overlapGraph
);
setOverlapGraph(newOverlappingGraph);
@@ -622,7 +760,10 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
const measured = {
...(node.measured ?? {}),
height: calcTableHeight(event.data.fields.length),
height: calcTableHeight({
...node.data.table,
fields: event.data.fields,
}),
};
newOverlappingGraph = findTableOverlapping(
@@ -632,7 +773,11 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
measured,
},
},
{ nodes: nodes.filter((node) => !node.hidden) },
{
nodes: nodes.filter(
(node) => !node.hidden && node.type === 'table'
) as TableNodeType[],
},
overlapGraph
);
setOverlapGraph(newOverlappingGraph);
@@ -684,6 +829,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
<CanvasContextMenu>
<div className="relative flex h-full" id="canvas">
<ReactFlow
onlyRenderVisibleElements
colorMode={effectiveTheme}
className="canvas-cursor-default nodes-animated"
nodes={nodes}

View File

@@ -2,11 +2,11 @@ import React, { useCallback, useMemo } from 'react';
import type { Edge, EdgeProps } from '@xyflow/react';
import { getSmoothStepPath, Position, useReactFlow } from '@xyflow/react';
import type { DBRelationship } from '@/lib/domain/db-relationship';
import { RIGHT_HANDLE_ID_PREFIX } from './table-node/table-node-field';
import { RIGHT_HANDLE_ID_PREFIX } from '../table-node/table-node-field';
import { useChartDB } from '@/hooks/use-chartdb';
import { useLayout } from '@/hooks/use-layout';
import { cn } from '@/lib/utils';
import { getCardinalityMarkerId } from './canvas-utils';
import { getCardinalityMarkerId } from '../canvas-utils';
import { useDiff } from '@/context/diff-context/use-diff';
export type RelationshipEdgeType = Edge<

View File

@@ -19,7 +19,7 @@ import type { DBTable } from '@/lib/domain/db-table';
import { TableNodeField } from './table-node-field';
import { useLayout } from '@/hooks/use-layout';
import { useChartDB } from '@/hooks/use-chartdb';
import type { RelationshipEdgeType } from '../relationship-edge';
import type { RelationshipEdgeType } from '../relationship-edge/relationship-edge';
import type { DBField } from '@/lib/domain/db-field';
import { useTranslation } from 'react-i18next';
import { TableNodeContextMenu } from './table-node-context-menu';
@@ -60,11 +60,12 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
const { updateTable, relationships, readonly } = useChartDB();
const edges = useStore((store) => store.edges) as EdgeType[];
const { openTableFromSidebar, selectSidebarSection } = useLayout();
const [expanded, setExpanded] = useState(false);
const [expanded, setExpanded] = useState(table.expanded ?? false);
const { t } = useTranslation();
const [editMode, setEditMode] = useState(false);
const [tableName, setTableName] = useState(table.name);
const inputRef = React.useRef<HTMLInputElement>(null);
const [isHovering, setIsHovering] = useState(false);
const {
getTableNewName,
@@ -115,7 +116,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
edge.type === 'relationship-edge'
) as RelationshipEdgeType[];
const focused = !!selected && !dragging;
const focused = (!!selected && !dragging) || isHovering;
const openTableInEditor = () => {
selectSidebarSection('tables');
@@ -137,9 +138,13 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
});
}, [table.id, updateTable]);
const toggleExpand = () => {
setExpanded(!expanded);
};
const toggleExpand = useCallback(() => {
setExpanded((prev) => {
const value = !prev;
updateTable(table.id, { expanded: value });
return value;
});
}, [table.id, updateTable]);
const isMustDisplayedField = useCallback(
(field: DBField) => {
@@ -239,6 +244,8 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
openTableInEditor();
}
}}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
>
<NodeResizer
isVisible={focused}
@@ -252,7 +259,6 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
table={table}
focused={focused}
/>
{/* Badge added here */}
<TableNodeStatus
status={
isDiffNewTable

View File

@@ -10,7 +10,7 @@ import {
SidebarMenuButton,
SidebarMenuItem,
} from '@/components/sidebar/sidebar';
import { Twitter, BookOpen } from 'lucide-react';
import { Twitter, BookOpen, Group } from 'lucide-react';
import { SquareStack, Table, Workflow } from 'lucide-react';
import { useLayout } from '@/hooks/use-layout';
import { useTranslation } from 'react-i18next';
@@ -19,6 +19,7 @@ import { useBreakpoint } from '@/hooks/use-breakpoint';
import ChartDBLogo from '@/assets/logo-light.png';
import ChartDBDarkLogo from '@/assets/logo-dark.png';
import { useTheme } from '@/hooks/use-theme';
import { useChartDB } from '@/hooks/use-chartdb';
export interface SidebarItem {
title: string;
@@ -36,8 +37,10 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
const { t } = useTranslation();
const { isMd: isDesktop } = useBreakpoint('md');
const { effectiveTheme } = useTheme();
const items: SidebarItem[] = useMemo(
() => [
const { dependencies } = useChartDB();
const items: SidebarItem[] = useMemo(() => {
const baseItems = [
{
title: t('side_panel.tables_section.tables'),
icon: Table,
@@ -57,6 +60,18 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
active: selectedSidebarSection === 'relationships',
},
{
title: t('side_panel.areas_section.areas'),
icon: Group,
onClick: () => {
showSidePanel();
selectSidebarSection('areas');
},
active: selectedSidebarSection === 'areas',
},
];
if (dependencies && dependencies.length > 0) {
baseItems.splice(2, 0, {
title: t('side_panel.dependencies_section.dependencies'),
icon: SquareStack,
onClick: () => {
@@ -64,10 +79,17 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
selectSidebarSection('dependencies');
},
active: selectedSidebarSection === 'dependencies',
},
],
[selectSidebarSection, selectedSidebarSection, t, showSidePanel]
);
});
}
return baseItems;
}, [
selectSidebarSection,
selectedSidebarSection,
t,
showSidePanel,
dependencies,
]);
const footerItems: SidebarItem[] = useMemo(
() => [

View File

@@ -0,0 +1,254 @@
import React, { useCallback } from 'react';
import {
GripVertical,
Pencil,
Check,
Trash2,
EllipsisVertical,
CircleDotDashed,
} from 'lucide-react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import type { Area } from '@/lib/domain/area';
import { Input } from '@/components/input/input';
import { useChartDB } from '@/hooks/use-chartdb';
import { useClickAway, useKeyPressEvent } from 'react-use';
import { useTranslation } from 'react-i18next';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { ColorPicker } from '@/components/color-picker/color-picker';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/dropdown-menu/dropdown-menu';
import { ListItemHeaderButton } from '@/pages/editor-page/side-panel/list-item-header-button/list-item-header-button';
import { mergeRefs } from '@/lib/utils';
import { useReactFlow } from '@xyflow/react';
import { useLayout } from '@/hooks/use-layout';
import { useBreakpoint } from '@/hooks/use-breakpoint';
export interface AreaListItemProps {
area: Area;
}
export const AreaListItem = React.forwardRef<HTMLDivElement, AreaListItemProps>(
({ area }, forwardedRef) => {
const { updateArea, removeArea } = useChartDB();
const { t } = useTranslation();
const { fitView, setNodes } = useReactFlow();
const { hideSidePanel } = useLayout();
const { isMd: isDesktop } = useBreakpoint('md');
const [editMode, setEditMode] = React.useState(false);
const [areaName, setAreaName] = React.useState(area.name);
const inputRef = React.useRef<HTMLInputElement>(null);
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({
id: area.id,
});
// Merge the forwarded ref with the sortable ref
const combinedRef = mergeRefs<HTMLDivElement>(forwardedRef, setNodeRef);
const style = {
transform: CSS.Translate.toString(transform),
transition,
};
const saveAreaName = useCallback(() => {
if (!editMode) return;
if (areaName.trim()) {
updateArea(area.id, { name: areaName.trim() });
}
setEditMode(false);
}, [areaName, area.id, updateArea, editMode]);
const abortEdit = useCallback(() => {
setEditMode(false);
setAreaName(area.name);
}, [area.name]);
const enterEditMode = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
setEditMode(true);
}, []);
const handleDelete = useCallback(() => {
removeArea(area.id);
}, [area.id, removeArea]);
const handleColorChange = useCallback(
(color: string) => {
updateArea(area.id, { color });
},
[area.id, updateArea]
);
const focusOnArea = useCallback(
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.stopPropagation();
setNodes((nodes) =>
nodes.map((node) =>
node.id == area.id
? {
...node,
selected: true,
}
: {
...node,
selected: false,
}
)
);
fitView({
duration: 500,
maxZoom: 1,
minZoom: 1,
nodes: [
{
id: area.id,
},
],
});
if (!isDesktop) {
hideSidePanel();
}
},
[fitView, area.id, setNodes, hideSidePanel, isDesktop]
);
useClickAway(inputRef, saveAreaName);
useKeyPressEvent('Enter', saveAreaName);
useKeyPressEvent('Escape', abortEdit);
const renderDropDownMenu = useCallback(
() => (
<DropdownMenu>
<DropdownMenuTrigger>
<ListItemHeaderButton>
<EllipsisVertical />
</ListItemHeaderButton>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-fit min-w-40">
<DropdownMenuLabel>
{t(
'side_panel.areas_section.area.area_actions.title'
)}
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className="flex justify-between gap-4"
onClick={(e) => {
e.stopPropagation();
enterEditMode(e);
}}
>
{t(
'side_panel.areas_section.area.area_actions.edit_name'
)}
<Pencil className="size-3.5" />
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onClick={handleDelete}
className="flex justify-between !text-red-700"
>
{t(
'side_panel.areas_section.area.area_actions.delete_area'
)}
<Trash2 className="size-3.5 text-red-700" />
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
),
[enterEditMode, handleDelete, t]
);
return (
<div
className="w-full rounded-md border border-border hover:bg-accent/5"
ref={combinedRef}
style={{
...style,
borderLeftWidth: '6px',
borderLeftColor: area.color,
}}
{...attributes}
>
<div className="group flex h-11 items-center justify-between gap-1 overflow-hidden p-2">
<div
className="flex cursor-move items-center justify-center"
{...listeners}
>
<GripVertical className="size-4 text-muted-foreground" />
</div>
<div className="flex min-w-0 flex-1">
{editMode ? (
<Input
ref={inputRef}
autoFocus
type="text"
placeholder={area.name}
value={areaName}
onClick={(e) => e.stopPropagation()}
onChange={(e) => setAreaName(e.target.value)}
className="h-7 w-full focus-visible:ring-0"
/>
) : (
<Tooltip>
<TooltipTrigger asChild>
<div
onDoubleClick={enterEditMode}
className="text-editable truncate px-2 py-0.5 text-sm font-medium"
>
{area.name}
</div>
</TooltipTrigger>
<TooltipContent>
{t('tool_tips.double_click_to_edit')}
</TooltipContent>
</Tooltip>
)}
</div>
<div className="flex items-center gap-1">
{!editMode ? (
<div className="flex flex-row-reverse items-center gap-1">
{renderDropDownMenu()}
<ColorPicker
color={area.color}
onChange={handleColorChange}
/>
<div className="hidden md:group-hover:flex">
<ListItemHeaderButton onClick={focusOnArea}>
<CircleDotDashed />
</ListItemHeaderButton>
</div>
</div>
) : (
<ListItemHeaderButton onClick={saveAreaName}>
<Check />
</ListItemHeaderButton>
)}
</div>
</div>
</div>
);
}
);
AreaListItem.displayName = 'AreaListItem';

View File

@@ -0,0 +1,123 @@
import React, { useCallback, useMemo } from 'react';
import { AreaListItem } from './area-list-item/area-list-item';
import type { Area } from '@/lib/domain/area';
import { useLayout } from '@/hooks/use-layout';
import {
closestCenter,
DndContext,
type DragEndEvent,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import {
arrayMove,
SortableContext,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { useChartDB } from '@/hooks/use-chartdb.ts';
export interface AreaListProps {
areas: Area[];
}
export const AreaList: React.FC<AreaListProps> = ({ areas }) => {
const { updateArea } = useChartDB();
const { openedAreaInSidebar } = useLayout();
const lastSelectedArea = React.useRef<string | null>(null);
const refs = useMemo(
() =>
areas.reduce(
(acc, area) => {
acc[area.id] = React.createRef();
return acc;
},
{} as Record<string, React.RefObject<HTMLDivElement>>
),
[areas]
);
const scrollToArea = useCallback(
(id: string) =>
refs[id]?.current?.scrollIntoView({
behavior: 'smooth',
block: 'start',
}),
[refs]
);
const sensors = useSensors(useSensor(PointerSensor));
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (active?.id !== over?.id && !!over && !!active) {
const oldIndex = areas.findIndex((area) => area.id === active.id);
const newIndex = areas.findIndex((area) => area.id === over.id);
const newAreasOrder = arrayMove<Area>(areas, oldIndex, newIndex);
newAreasOrder.forEach((area, index) => {
updateArea(area.id, { order: index });
});
}
};
const handleScrollToArea = useCallback(() => {
if (
openedAreaInSidebar &&
lastSelectedArea.current !== openedAreaInSidebar
) {
lastSelectedArea.current = openedAreaInSidebar;
scrollToArea(openedAreaInSidebar);
}
}, [scrollToArea, openedAreaInSidebar]);
React.useEffect(() => {
handleScrollToArea();
}, [openedAreaInSidebar, handleScrollToArea]);
return (
<div className="flex w-full flex-col gap-1">
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext
items={areas}
strategy={verticalListSortingStrategy}
>
{areas
.sort((area1: Area, area2: Area) => {
if (area1.order && area2.order === undefined) {
return -1;
}
if (area1.order === undefined && area2.order) {
return 1;
}
if (
area1.order !== undefined &&
area2.order !== undefined
) {
return area1.order - area2.order;
}
// if both areas don't have order, sort by name
return area1.name.localeCompare(area2.name);
})
.map((area) => (
<AreaListItem
key={area.id}
area={area}
ref={refs[area.id]}
/>
))}
</SortableContext>
</DndContext>
</div>
);
};

View File

@@ -0,0 +1,134 @@
import React, { useCallback, useMemo } from 'react';
import { AreaList } from './areas-list/areas-list';
import { Button } from '@/components/button/button';
import { Group, X } from 'lucide-react';
import { Input } from '@/components/input/input';
import type { Area } from '@/lib/domain/area';
import { useChartDB } from '@/hooks/use-chartdb';
import { useLayout } from '@/hooks/use-layout';
import { EmptyState } from '@/components/empty-state/empty-state';
import { ScrollArea } from '@/components/scroll-area/scroll-area';
import { useTranslation } from 'react-i18next';
import { useViewport } from '@xyflow/react';
import { useHotkeys } from 'react-hotkeys-hook';
import { getOperatingSystem } from '@/lib/utils';
export interface AreasSectionProps {}
export const AreasSection: React.FC<AreasSectionProps> = () => {
const { createArea, areas } = useChartDB();
const viewport = useViewport();
const { t } = useTranslation();
const { openAreaFromSidebar } = useLayout();
const [filterText, setFilterText] = React.useState('');
const filterInputRef = React.useRef<HTMLInputElement>(null);
const filteredAreas = useMemo(() => {
const filterAreaName: (area: Area) => boolean = (area) =>
!filterText?.trim?.() ||
area.name.toLowerCase().includes(filterText.toLowerCase());
return areas.filter(filterAreaName);
}, [areas, filterText]);
const createAreaWithLocation = useCallback(async () => {
const padding = 80;
const centerX = -viewport.x / viewport.zoom + padding / viewport.zoom;
const centerY = -viewport.y / viewport.zoom + padding / viewport.zoom;
const area = await createArea({
x: centerX,
y: centerY,
});
if (openAreaFromSidebar) {
openAreaFromSidebar(area.id);
}
}, [
createArea,
openAreaFromSidebar,
viewport.x,
viewport.y,
viewport.zoom,
]);
const handleCreateArea = useCallback(async () => {
setFilterText('');
createAreaWithLocation();
}, [createAreaWithLocation, setFilterText]);
const handleClearFilter = useCallback(() => {
setFilterText('');
}, []);
const operatingSystem = useMemo(() => getOperatingSystem(), []);
useHotkeys(
operatingSystem === 'mac' ? 'meta+f' : 'ctrl+f',
() => {
filterInputRef.current?.focus();
},
{
preventDefault: true,
},
[filterInputRef]
);
return (
<section
className="flex flex-1 flex-col overflow-hidden px-2"
data-vaul-no-drag
>
<div className="flex items-center justify-between gap-4 py-1">
<div className="flex-1">
<Input
ref={filterInputRef}
type="text"
placeholder={t('side_panel.areas_section.filter')}
className="h-8 w-full focus-visible:ring-0"
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
/>
</div>
<Button
variant="secondary"
className="h-8 p-2 text-xs"
onClick={handleCreateArea}
>
<Group className="h-4" />
{t('side_panel.areas_section.add_area')}
</Button>
</div>
<div className="flex flex-1 flex-col overflow-hidden">
<ScrollArea className="h-full">
{areas.length === 0 ? (
<EmptyState
title={t(
'side_panel.areas_section.empty_state.title'
)}
description={t(
'side_panel.areas_section.empty_state.description'
)}
className="mt-20"
/>
) : filterText && filteredAreas.length === 0 ? (
<div className="mt-10 flex flex-col items-center gap-2">
<div className="text-sm text-muted-foreground">
{t('side_panel.areas_section.no_results')}
</div>
<Button
variant="outline"
size="sm"
onClick={handleClearFilter}
className="gap-1"
>
<X className="size-3.5" />
{t('side_panel.areas_section.clear')}
</Button>
</div>
) : (
<AreaList areas={filteredAreas} />
)}
</ScrollArea>
</div>
</section>
);
};

View File

@@ -17,6 +17,7 @@ import { SelectBox } from '@/components/select-box/select-box';
import { useChartDB } from '@/hooks/use-chartdb';
import { DependenciesSection } from './dependencies-section/dependencies-section';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { AreasSection } from './areas-section/areas-section';
export interface SidePanelProps {}
@@ -113,6 +114,9 @@ export const SidePanel: React.FC<SidePanelProps> = () => {
'side_panel.dependencies_section.dependencies'
)}
</SelectItem>
<SelectItem value="areas">
{t('side_panel.areas_section.areas')}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
@@ -122,8 +126,10 @@ export const SidePanel: React.FC<SidePanelProps> = () => {
<TablesSection />
) : selectedSidebarSection === 'relationships' ? (
<RelationshipsSection />
) : (
) : selectedSidebarSection === 'dependencies' ? (
<DependenciesSection />
) : (
<AreasSection />
)}
</aside>
);

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React, { useMemo, useState, useEffect } from 'react';
import type { DBTable } from '@/lib/domain/db-table';
import { useChartDB } from '@/hooks/use-chartdb';
import { useTheme } from '@/hooks/use-theme';
@@ -10,6 +10,8 @@ import type { Diagram } from '@/lib/domain/diagram';
import { useToast } from '@/components/toast/use-toast';
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
import { DatabaseType } from '@/lib/domain/database-type';
import { ArrowLeftRight } from 'lucide-react';
import { type DBField } from '@/lib/domain/db-field';
export interface TableDBMLProps {
filteredTables: DBTable[];
@@ -37,74 +39,484 @@ const databaseTypeToImportFormat = (
}
};
// Fix problematic field names in the diagram before passing to SQL generator
const fixProblematicFieldNames = (diagram: Diagram): Diagram => {
const fixedTables =
diagram.tables?.map((table) => {
// Deep clone the table to avoid modifying the original
const newTable = { ...table };
// Fix field names if this is the "relation" table
if (table.name === 'relation') {
newTable.fields = table.fields.map((field) => {
// Create a new field to avoid modifying the original
const newField = { ...field };
// Fix the 'from' and 'to' fields which are SQL keywords
if (field.name === 'from') {
newField.name = 'source';
} else if (field.name === 'to') {
newField.name = 'target';
}
return newField;
});
}
return newTable;
}) || [];
// Update relationships to point to the renamed fields
const fixedRelationships =
diagram.relationships?.map((rel) => {
const relationTable = diagram.tables?.find(
(t) => t.name === 'relation'
);
if (!relationTable) return rel;
const newRel = { ...rel };
// Fix relationships that were pointing to the 'from' field
const fromField = relationTable.fields.find(
(f) => f.name === 'from'
);
if (fromField && rel.targetFieldId === fromField.id) {
// We need to look up the renamed field in our fixed tables
const fixedRelationTable = fixedTables.find(
(t) => t.name === 'relation'
);
const sourceField = fixedRelationTable?.fields.find(
(f) => f.name === 'source'
);
if (sourceField) {
newRel.targetFieldId = sourceField.id;
}
}
// Fix relationships that were pointing to the 'to' field
const toField = relationTable.fields.find((f) => f.name === 'to');
if (toField && rel.targetFieldId === toField.id) {
// We need to look up the renamed field in our fixed tables
const fixedRelationTable = fixedTables.find(
(t) => t.name === 'relation'
);
const targetField = fixedRelationTable?.fields.find(
(f) => f.name === 'target'
);
if (targetField) {
newRel.targetFieldId = targetField.id;
}
}
return newRel;
}) || [];
// Return a new diagram with the fixes
return {
...diagram,
tables: fixedTables,
relationships: fixedRelationships,
};
};
// Function to sanitize SQL before passing to the importer
const sanitizeSQLforDBML = (sql: string): string => {
// Replace special characters in identifiers
let sanitized = sql;
// Handle duplicate constraint names
const constraintNames = new Set<string>();
let constraintCounter = 0;
sanitized = sanitized.replace(
/ADD CONSTRAINT (\w+) FOREIGN KEY/g,
(match, name) => {
if (constraintNames.has(name)) {
return `ADD CONSTRAINT ${name}_${++constraintCounter} FOREIGN KEY`;
} else {
constraintNames.add(name);
return match;
}
}
);
// Replace any remaining problematic characters
sanitized = sanitized.replace(/\?\?/g, '__');
return sanitized;
};
// Post-process DBML to convert separate Ref statements to inline refs
const convertToInlineRefs = (dbml: string): string => {
// Extract all Ref statements - Corrected pattern
const refPattern =
/Ref\s+"([^"]+)"\s*:\s*"([^"]+)"\."([^"]+)"\s*([<>*])\s*"([^"]+)"\."([^"]+)"/g;
const refs: Array<{
refName: string;
sourceTable: string;
sourceField: string;
direction: string;
targetTable: string;
targetField: string;
}> = [];
let match;
while ((match = refPattern.exec(dbml)) !== null) {
refs.push({
refName: match[1], // Reference name
sourceTable: match[2], // Source table
sourceField: match[3], // Source field
direction: match[4], // Direction (<, >)
targetTable: match[5], // Target table
targetField: match[6], // Target field
});
}
// Extract all table definitions - Corrected pattern and handling
const tables: {
[key: string]: { start: number; end: number; content: string };
} = {};
const tablePattern = /Table\s+"([^"]+)"\s*{([^}]*)}/g; // Simpler pattern, assuming content doesn't have {}
let tableMatch;
while ((tableMatch = tablePattern.exec(dbml)) !== null) {
const tableName = tableMatch[1];
tables[tableName] = {
start: tableMatch.index,
end: tableMatch.index + tableMatch[0].length,
content: tableMatch[2],
};
}
if (refs.length === 0 || Object.keys(tables).length === 0) {
return dbml; // Return original if parsing failed
}
// Create a map for faster table lookup
const tableMap = new Map(Object.entries(tables));
// 1. Add inline refs to table contents
refs.forEach((ref) => {
let targetTableName, fieldNameToModify, inlineRefSyntax;
if (ref.direction === '<') {
targetTableName = ref.targetTable;
fieldNameToModify = ref.targetField;
inlineRefSyntax = `[ref: < "${ref.sourceTable}"."${ref.sourceField}"]`;
} else {
targetTableName = ref.sourceTable;
fieldNameToModify = ref.sourceField;
inlineRefSyntax = `[ref: > "${ref.targetTable}"."${ref.targetField}"]`;
}
const tableData = tableMap.get(targetTableName);
if (tableData) {
const fieldPattern = new RegExp(
`("(${fieldNameToModify})"[^\n]*?)([ \t]*[[].*?[]])?([ \t]*//.*)?$`,
'm'
);
let newContent = tableData.content;
newContent = newContent.replace(
fieldPattern,
(
lineMatch,
fieldPart,
_fieldName,
existingAttributes,
commentPart
) => {
// Avoid adding duplicate refs
if (lineMatch.includes('[ref:')) {
return lineMatch;
}
return `${fieldPart.trim()} ${inlineRefSyntax}${existingAttributes || ''}${commentPart || ''}`;
}
);
// Update the table content if modified
if (newContent !== tableData.content) {
tableData.content = newContent;
tableMap.set(targetTableName, tableData);
}
}
});
// 2. Reconstruct DBML with modified tables
let reconstructedDbml = '';
let lastIndex = 0;
const sortedTables = Object.entries(tables).sort(
([, a], [, b]) => a.start - b.start
);
for (const [tableName, tableData] of sortedTables) {
reconstructedDbml += dbml.substring(lastIndex, tableData.start);
reconstructedDbml += `Table "${tableName}" {${tableData.content}}`;
lastIndex = tableData.end;
}
reconstructedDbml += dbml.substring(lastIndex);
// 3. Remove original Ref lines
const finalLines = reconstructedDbml
.split('\n')
.filter((line) => !line.trim().startsWith('Ref '));
const finalDbml = finalLines.join('\n').trim();
return finalDbml;
};
// Function to check for SQL keywords (add more if needed)
const isSQLKeyword = (name: string): boolean => {
const keywords = new Set(['CASE', 'ORDER', 'GROUP', 'FROM', 'TO', 'USER']); // Add common keywords
return keywords.has(name.toUpperCase());
};
// Fix DBML formatting to ensure consistent display of char and varchar types
const normalizeCharTypeFormat = (dbml: string): string => {
// Replace "char (N)" with "char(N)" to match varchar's formatting
return dbml
.replace(/"char "/g, 'char')
.replace(/"char \(([0-9]+)\)"/g, 'char($1)')
.replace(/"character \(([0-9]+)\)"/g, 'character($1)');
};
export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
const { currentDiagram } = useChartDB();
const { effectiveTheme } = useTheme();
const { toast } = useToast();
const [dbmlFormat, setDbmlFormat] = useState<'inline' | 'standard'>(
'standard'
);
const generateDBML = useMemo(() => {
const filteredDiagram: Diagram = {
...currentDiagram,
tables: filteredTables,
relationships:
currentDiagram.relationships?.filter((rel) => {
const sourceTable = filteredTables.find(
(t) => t.id === rel.sourceTableId
);
const targetTable = filteredTables.find(
(t) => t.id === rel.targetTableId
);
// --- Effect for handling empty field name warnings ---
useEffect(() => {
let foundInvalidFields = false;
const invalidTableNames = new Set<string>();
return sourceTable && targetTable;
}) ?? [],
} satisfies Diagram;
const filteredDiagramWithoutSpaces: Diagram = {
...filteredDiagram,
tables:
filteredDiagram.tables?.map((table) => ({
...table,
name: table.name.replace(/\s/g, '_'),
fields: table.fields.map((field) => ({
...field,
name: field.name.replace(/\s/g, '_'),
})),
indexes: table.indexes?.map((index) => ({
...index,
name: index.name.replace(/\s/g, '_'),
})),
})) ?? [],
} satisfies Diagram;
const baseScript = exportBaseSQL({
diagram: filteredDiagramWithoutSpaces,
targetDatabaseType: currentDiagram.databaseType,
isDBMLFlow: true,
filteredTables.forEach((table) => {
table.fields.forEach((field) => {
if (field.name === '') {
foundInvalidFields = true;
invalidTableNames.add(table.name);
}
});
});
try {
const importFormat = databaseTypeToImportFormat(
currentDiagram.databaseType
);
return importer.import(baseScript, importFormat);
} catch (e) {
console.error(e);
if (foundInvalidFields) {
const tableNamesString = Array.from(invalidTableNames).join(', ');
toast({
title: 'Error',
description:
'Failed to generate DBML. We would appreciate if you could report this issue!',
variant: 'destructive',
title: 'Warning',
description: `Some fields had empty names in tables: [${tableNamesString}] and were excluded from the DBML export.`,
variant: 'default',
});
}
}, [filteredTables, toast]); // Depend on filteredTables and toast
// Generate both standard and inline DBML formats
const { standardDbml, inlineDbml } = useMemo(() => {
// Filter out fields with empty names
const sanitizedTables = filteredTables.map((table) => {
const validFields = table.fields.filter(
(field) => field.name !== ''
);
return {
...table,
fields: validFields,
};
});
// Create the base filtered diagram structure
const filteredDiagram: Diagram = {
...currentDiagram,
tables: sanitizedTables,
relationships:
currentDiagram.relationships?.filter((rel) => {
const sourceTable = sanitizedTables.find(
(t) => t.id === rel.sourceTableId
);
const targetTable = sanitizedTables.find(
(t) => t.id === rel.targetTableId
);
const sourceFieldExists = sourceTable?.fields.some(
(f) => f.id === rel.sourceFieldId
);
const targetFieldExists = targetTable?.fields.some(
(f) => f.id === rel.targetFieldId
);
return (
sourceTable &&
targetTable &&
sourceFieldExists &&
targetFieldExists
);
}) ?? [],
};
// Sanitize field names ('from'/'to' in 'relation' table)
const cleanDiagram = fixProblematicFieldNames(filteredDiagram);
// --- Final sanitization and renaming pass ---
// Track tables renamed due to SQL keyword conflicts
const sqlRenamedTables = new Map<string, string>();
// Track fields renamed due to SQL keyword conflicts
const fieldRenames: Array<{
table: string;
originalName: string;
newName: string;
}> = [];
const finalDiagramForExport: Diagram = {
...cleanDiagram,
tables:
cleanDiagram.tables?.map((table) => {
const originalName = table.name;
// Sanitize table name
let safeTableName = originalName.replace(/[^\w]/g, '_');
// Rename if SQL keyword
if (isSQLKeyword(safeTableName)) {
const newName = `${safeTableName}_table`;
sqlRenamedTables.set(newName, originalName);
safeTableName = newName;
}
const fieldNameCounts = new Map<string, number>();
const processedFields = table.fields.map((field) => {
const originalSafeName = field.name.replace(
/[^\w]/g,
'_'
);
let finalSafeName = originalSafeName;
const count =
fieldNameCounts.get(originalSafeName) || 0;
if (count > 0) {
finalSafeName = `${originalSafeName}_${count + 1}`; // Rename duplicate
}
fieldNameCounts.set(originalSafeName, count + 1);
// Create a copy and remove comments
const sanitizedField: DBField = {
...field,
name: finalSafeName,
};
delete sanitizedField.comments;
// Rename if SQL keyword
if (isSQLKeyword(finalSafeName)) {
const newFieldName = `${finalSafeName}_field`;
fieldRenames.push({
table: safeTableName,
originalName: finalSafeName,
newName: newFieldName,
});
sanitizedField.name = newFieldName;
}
return sanitizedField;
});
return {
...table,
name: safeTableName,
fields: processedFields, // Use fields with renamed duplicates
indexes: (table.indexes || []).map((index) => ({
...index,
name: index.name
? index.name.replace(/[^\w]/g, '_')
: `idx_${Math.random().toString(36).substring(2, 8)}`,
})),
};
}) ?? [],
relationships:
cleanDiagram.relationships?.map((rel, index) => ({
...rel,
name: `fk_${index}_${rel.name ? rel.name.replace(/[^\w]/g, '_') : Math.random().toString(36).substring(2, 8)}`,
})) ?? [],
} as Diagram;
let standard = '';
let inline = '';
let baseScript = ''; // Define baseScript outside try
try {
baseScript = exportBaseSQL({
diagram: finalDiagramForExport, // Use final diagram
targetDatabaseType: currentDiagram.databaseType,
isDBMLFlow: true,
});
return '';
baseScript = sanitizeSQLforDBML(baseScript);
// Append COMMENTS for tables renamed due to SQL keywords
sqlRenamedTables.forEach((originalName, newName) => {
const escapedOriginal = originalName.replace(/'/g, "\\'");
baseScript += `\nCOMMENT ON TABLE "${newName}" IS 'Original name was "${escapedOriginal}" (renamed due to SQL keyword conflict).';`;
});
// Append COMMENTS for fields renamed due to SQL keyword conflicts
fieldRenames.forEach(({ table, originalName, newName }) => {
const escapedOriginal = originalName.replace(/'/g, "\\'");
baseScript += `\nCOMMENT ON COLUMN "${table}"."${newName}" IS 'Original name was "${escapedOriginal}" (renamed due to SQL keyword conflict).';`;
});
standard = normalizeCharTypeFormat(
importer.import(
baseScript,
databaseTypeToImportFormat(currentDiagram.databaseType)
)
);
inline = normalizeCharTypeFormat(convertToInlineRefs(standard));
} catch (error: unknown) {
console.error(
'Error during DBML generation process:',
error,
'Input SQL was:',
baseScript // Log the SQL that caused the error
);
const errorMessage = `// Error generating DBML: ${error instanceof Error ? error.message : 'Unknown error'}`;
standard = errorMessage;
inline = errorMessage;
// Handle different error types for toast
if (error instanceof Error) {
toast({
title: 'DBML Export Error',
description: `Could not generate DBML: ${error.message.substring(0, 100)}${error.message.length > 100 ? '...' : ''}`,
variant: 'destructive',
});
} else {
toast({
title: 'DBML Export Error',
description:
'Could not generate DBML due to an unknown error',
variant: 'destructive',
});
}
}
}, [currentDiagram, filteredTables, toast]);
return { standardDbml: standard, inlineDbml: inline };
}, [currentDiagram, filteredTables, toast]); // Keep toast dependency for now, although direct call is removed
// Determine which DBML string to display
const dbmlToDisplay = dbmlFormat === 'inline' ? inlineDbml : standardDbml;
// Toggle function
const toggleFormat = () => {
setDbmlFormat((prev) => (prev === 'inline' ? 'standard' : 'inline'));
};
return (
<CodeSnippet
code={generateDBML}
code={dbmlToDisplay}
className="my-0.5"
actions={[
{
label: `Show ${dbmlFormat === 'inline' ? 'Standard' : 'Inline'} Refs`,
icon: ArrowLeftRight,
onClick: toggleFormat,
},
]}
editorProps={{
height: '100%',
defaultLanguage: 'dbml',

Some files were not shown because too many files have changed in this diff Show More