mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-02 04:53:27 +00:00
Compare commits
21 Commits
jf/fix-fie
...
jf/prevent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e90887e9ee | ||
|
|
ad1e59bdd2 | ||
|
|
6282a555bb | ||
|
|
4f1d3295c0 | ||
|
|
5936500ca0 | ||
|
|
43fc1d7fc2 | ||
|
|
8dfa7cc62e | ||
|
|
23e93bfd01 | ||
|
|
16f9f4671e | ||
|
|
0c300e5e72 | ||
|
|
b9a1e78b53 | ||
|
|
337f7cdab4 | ||
|
|
1b0390f0b7 | ||
|
|
bc52933b58 | ||
|
|
2fdad2344c | ||
|
|
0c7eaa2df2 | ||
|
|
a5f8e56b3c | ||
|
|
8ffde62c1a | ||
|
|
39247b77a2 | ||
|
|
984b2aeee2 | ||
|
|
eed104be5b |
63
CHANGELOG.md
63
CHANGELOG.md
@@ -1,5 +1,68 @@
|
||||
# Changelog
|
||||
|
||||
## [1.14.0](https://github.com/chartdb/chartdb/compare/v1.13.2...v1.14.0) (2025-08-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add floating "Show All" button when tables are out of view ([#787](https://github.com/chartdb/chartdb/issues/787)) ([bda150d](https://github.com/chartdb/chartdb/commit/bda150d4b6d6fb90beb423efba69349d21a037a5))
|
||||
* add table selection for large database imports ([#776](https://github.com/chartdb/chartdb/issues/776)) ([0d9f57a](https://github.com/chartdb/chartdb/commit/0d9f57a9c969a67e350d6bf25e07c3a9ef5bba39))
|
||||
* **canvas:** Add filter tables on canvas ([#774](https://github.com/chartdb/chartdb/issues/774)) ([dfbcf05](https://github.com/chartdb/chartdb/commit/dfbcf05b2f595f5b7b77dd61abf77e6e07acaf8f))
|
||||
* **custom-types:** add highlight fields option for custom types ([#726](https://github.com/chartdb/chartdb/issues/726)) ([7e0483f](https://github.com/chartdb/chartdb/commit/7e0483f1a5512a6a737baf61caf7513e043f2e96))
|
||||
* **datatypes:** Add decimal / numeric attribute support + organize field row ([#715](https://github.com/chartdb/chartdb/issues/715)) ([778f85d](https://github.com/chartdb/chartdb/commit/778f85d49214232a39710e47bb5d4ec41b75d427))
|
||||
* **dbml:** Edit Diagram Directly from DBML ([#819](https://github.com/chartdb/chartdb/issues/819)) ([1b0390f](https://github.com/chartdb/chartdb/commit/1b0390f0b7652fe415540b7942cf53ec87143f08))
|
||||
* **default value:** add default value option to table field settings ([#770](https://github.com/chartdb/chartdb/issues/770)) ([c9ea7da](https://github.com/chartdb/chartdb/commit/c9ea7da0923ff991cb936235674d9a52b8186137))
|
||||
* enhance primary key and unique field handling logic ([#817](https://github.com/chartdb/chartdb/issues/817)) ([39247b7](https://github.com/chartdb/chartdb/commit/39247b77a299caa4f29ea434af3028155c6d37ed))
|
||||
* implement area grouping with parent-child relationships ([#762](https://github.com/chartdb/chartdb/issues/762)) ([b35e175](https://github.com/chartdb/chartdb/commit/b35e17526b3c9b918928ae5f3f89711ea7b2529c))
|
||||
* **schema:** support create new schema ([#801](https://github.com/chartdb/chartdb/issues/801)) ([867903c](https://github.com/chartdb/chartdb/commit/867903cd5f24d96ce1fe718dc9b562e2f2b75276))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add open and create diagram to side menu ([#757](https://github.com/chartdb/chartdb/issues/757)) ([67f5ac3](https://github.com/chartdb/chartdb/commit/67f5ac303ebf5ada97d5c80fb08a2815ca205a91))
|
||||
* add PostgreSQL tests and fix parsing SQL ([#760](https://github.com/chartdb/chartdb/issues/760)) ([5d33740](https://github.com/chartdb/chartdb/commit/5d337409d64d1078b538350016982a98e684c06c))
|
||||
* area resizers size ([#830](https://github.com/chartdb/chartdb/issues/830)) ([23e93bf](https://github.com/chartdb/chartdb/commit/23e93bfd01d741dd3d11aa5c479cef97e1a86fa6))
|
||||
* **area:** redo/undo after dragging an area with tables ([#767](https://github.com/chartdb/chartdb/issues/767)) ([6af94af](https://github.com/chartdb/chartdb/commit/6af94afc56cf8987b8fc9e3f0a9bfa966de35408))
|
||||
* **canvas filter:** improve scroller on canvas filter ([#799](https://github.com/chartdb/chartdb/issues/799)) ([6bea827](https://github.com/chartdb/chartdb/commit/6bea82729362a8c7b73dc089ddd9e52bae176aa2))
|
||||
* **canvas:** fix filter eye button ([#780](https://github.com/chartdb/chartdb/issues/780)) ([b7dbe54](https://github.com/chartdb/chartdb/commit/b7dbe54c83c75cfe3c556f7a162055dcfe2de23d))
|
||||
* clone of custom types ([#804](https://github.com/chartdb/chartdb/issues/804)) ([b30162d](https://github.com/chartdb/chartdb/commit/b30162d98bc659a61aae023cdeaead4ce25c7ae9))
|
||||
* **cockroachdb:** support schema creation for cockroachdb ([#803](https://github.com/chartdb/chartdb/issues/803)) ([dba372d](https://github.com/chartdb/chartdb/commit/dba372d25a8c642baf8600d05aa154882729d446))
|
||||
* **dbml actions:** set dbml tooltips side ([#798](https://github.com/chartdb/chartdb/issues/798)) ([a119854](https://github.com/chartdb/chartdb/commit/a119854da7c935eb595984ea9398e04136ce60c4))
|
||||
* **dbml editor:** move tooltips button to be on the right ([#797](https://github.com/chartdb/chartdb/issues/797)) ([bfbfd7b](https://github.com/chartdb/chartdb/commit/bfbfd7b843f96c894b1966ad95393b866c927466))
|
||||
* **dbml export:** fix handle tables with same name under different schemas ([#807](https://github.com/chartdb/chartdb/issues/807)) ([18e9142](https://github.com/chartdb/chartdb/commit/18e914242faccd6376fe5a7cd5a4478667f065ee))
|
||||
* **dbml export:** handle tables with same name under different schemas ([#806](https://github.com/chartdb/chartdb/issues/806)) ([e68837a](https://github.com/chartdb/chartdb/commit/e68837a34aa635fb6fc02c7f1289495e5c448242))
|
||||
* **dbml field comments:** support export field comments in dbml ([#796](https://github.com/chartdb/chartdb/issues/796)) ([0ca7008](https://github.com/chartdb/chartdb/commit/0ca700873577bbfbf1dd3f8088c258fc89b10c53))
|
||||
* **dbml import:** fix dbml import types + schemas ([#808](https://github.com/chartdb/chartdb/issues/808)) ([00bd535](https://github.com/chartdb/chartdb/commit/00bd535b3c62d26d25a6276d52beb10e26afad76))
|
||||
* **dbml-export:** merge field attributes into single brackets and fix schema syntax ([#790](https://github.com/chartdb/chartdb/issues/790)) ([309ee9c](https://github.com/chartdb/chartdb/commit/309ee9cb0ff1f5a68ed183e3919e1a11a8410909))
|
||||
* **dbml-import:** handle unsupported DBML features and add comprehensive tests ([#766](https://github.com/chartdb/chartdb/issues/766)) ([22d46e1](https://github.com/chartdb/chartdb/commit/22d46e1e90729730cc25dd6961bfe8c3d2ae0c98))
|
||||
* **dbml:** dbml indentation ([#829](https://github.com/chartdb/chartdb/issues/829)) ([16f9f46](https://github.com/chartdb/chartdb/commit/16f9f4671e011eb66ba9594bed47570eda3eed66))
|
||||
* **dbml:** dbml note syntax ([#826](https://github.com/chartdb/chartdb/issues/826)) ([337f7cd](https://github.com/chartdb/chartdb/commit/337f7cdab4759d15cb4d25a8c0e9394e99ba33d4))
|
||||
* **dbml:** fix dbml output format ([#815](https://github.com/chartdb/chartdb/issues/815)) ([eed104b](https://github.com/chartdb/chartdb/commit/eed104be5ba2b7d9940ffac38e7877722ad764fc))
|
||||
* **dbml:** fix schemas with same table names ([#828](https://github.com/chartdb/chartdb/issues/828)) ([0c300e5](https://github.com/chartdb/chartdb/commit/0c300e5e72cc5ff22cac42f8dbaed167061157c6))
|
||||
* **dbml:** import dbml notes (table + fields) ([#827](https://github.com/chartdb/chartdb/issues/827)) ([b9a1e78](https://github.com/chartdb/chartdb/commit/b9a1e78b53c932c0b1a12ee38b62494a5c2f9348))
|
||||
* **dbml:** support multiple relationships on same field in inline DBML ([#822](https://github.com/chartdb/chartdb/issues/822)) ([a5f8e56](https://github.com/chartdb/chartdb/commit/a5f8e56b3ca97b851b6953481644d3a3ff7ce882))
|
||||
* **dbml:** support spaces in names ([#794](https://github.com/chartdb/chartdb/issues/794)) ([8f27f10](https://github.com/chartdb/chartdb/commit/8f27f10dec96af400dc2c12a30b22b3a346803a9))
|
||||
* fix hotkeys on form elements ([#778](https://github.com/chartdb/chartdb/issues/778)) ([43d1dff](https://github.com/chartdb/chartdb/commit/43d1dfff71f2b960358a79b0112b78d11df91fb7))
|
||||
* fix screen freeze after schema select ([#800](https://github.com/chartdb/chartdb/issues/800)) ([8aeb1df](https://github.com/chartdb/chartdb/commit/8aeb1df0ad353c49e91243453f24bfa5921a89ab))
|
||||
* **i18n:** add Croatian (hr) language support ([#802](https://github.com/chartdb/chartdb/issues/802)) ([2eb48e7](https://github.com/chartdb/chartdb/commit/2eb48e75d303d622f51327d22502a6f78e7fb32d))
|
||||
* improve SQL export formatting and add schema-aware FK grouping ([#783](https://github.com/chartdb/chartdb/issues/783)) ([6df588f](https://github.com/chartdb/chartdb/commit/6df588f40e6e7066da6125413b94466429d48767))
|
||||
* lost in canvas button animation ([#793](https://github.com/chartdb/chartdb/issues/793)) ([a93ec2c](https://github.com/chartdb/chartdb/commit/a93ec2cab906d0e4431d8d1668adcf2dbfc3c80f))
|
||||
* **readonly:** fix zoom out on readonly ([#818](https://github.com/chartdb/chartdb/issues/818)) ([8ffde62](https://github.com/chartdb/chartdb/commit/8ffde62c1a00893c4bf6b4dd39068df530375416))
|
||||
* remove error lag after autofix ([#764](https://github.com/chartdb/chartdb/issues/764)) ([bf32c08](https://github.com/chartdb/chartdb/commit/bf32c08d37c02ee6d7946a41633bb97b2271fcb7))
|
||||
* remove unnecessary import ([#791](https://github.com/chartdb/chartdb/issues/791)) ([87836e5](https://github.com/chartdb/chartdb/commit/87836e53d145b825f9c4f80abca72f418df50e6c))
|
||||
* **scroll:** disable scroll x behavior ([#795](https://github.com/chartdb/chartdb/issues/795)) ([4bc71c5](https://github.com/chartdb/chartdb/commit/4bc71c52ff5c462800d8530b72a5aadb7d7f85ed))
|
||||
* set focus on filter search ([#775](https://github.com/chartdb/chartdb/issues/775)) ([9949a46](https://github.com/chartdb/chartdb/commit/9949a46ee3ba7f46a2ea7f2c0d7101cc9336df4f))
|
||||
* solve issue with multiple render of tables ([#823](https://github.com/chartdb/chartdb/issues/823)) ([0c7eaa2](https://github.com/chartdb/chartdb/commit/0c7eaa2df20cfb6994b7e6251c760a2d4581c879))
|
||||
* **sql-export:** escape newlines and quotes in multi-line comments ([#765](https://github.com/chartdb/chartdb/issues/765)) ([f7f9290](https://github.com/chartdb/chartdb/commit/f7f92903def84a94ac0c66f625f96a6681383945))
|
||||
* **sql-server:** improvment for sql-server import via sql script ([#789](https://github.com/chartdb/chartdb/issues/789)) ([79b8855](https://github.com/chartdb/chartdb/commit/79b885502e3385e996a52093a3ccd5f6e469993a))
|
||||
* **table-node:** fix comment icon on field ([#786](https://github.com/chartdb/chartdb/issues/786)) ([745bdee](https://github.com/chartdb/chartdb/commit/745bdee86d07f1e9c3a2d24237c48c25b9a8eeea))
|
||||
* **table-node:** improve field spacing ([#785](https://github.com/chartdb/chartdb/issues/785)) ([08eb9cc](https://github.com/chartdb/chartdb/commit/08eb9cc55f0077f53afea6f9ce720341e1a583c2))
|
||||
* **table-select:** add loading indication for import ([#782](https://github.com/chartdb/chartdb/issues/782)) ([b46ed58](https://github.com/chartdb/chartdb/commit/b46ed58dff1ec74579fb1544dba46b0f77730c52))
|
||||
* **ui:** reduce spacing between primary key icon and short field types ([#816](https://github.com/chartdb/chartdb/issues/816)) ([984b2ae](https://github.com/chartdb/chartdb/commit/984b2aeee22c43cb9bda77df2c22087973079af4))
|
||||
* update MariaDB database import smart query ([#792](https://github.com/chartdb/chartdb/issues/792)) ([386e40a](https://github.com/chartdb/chartdb/commit/386e40a0bf93d9aef1486bb1e729d8f485e675eb))
|
||||
* update multiple schemas toast to require user action ([#771](https://github.com/chartdb/chartdb/issues/771)) ([f56fab9](https://github.com/chartdb/chartdb/commit/f56fab9876fb9fc46c6c708231324a90d8a7851d))
|
||||
* update relationship when table width changes via expand/shrink ([#825](https://github.com/chartdb/chartdb/issues/825)) ([bc52933](https://github.com/chartdb/chartdb/commit/bc52933b58bfe6bc73779d9401128254cbf497d5))
|
||||
|
||||
## [1.13.2](https://github.com/chartdb/chartdb/compare/v1.13.1...v1.13.2) (2025-07-06)
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="max-image-preview:large" />
|
||||
<title>ChartDB - Create & Visualize Database Schema Diagrams</title>
|
||||
<link rel="canonical" href="https://chartdb.io" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
|
||||
22
package-lock.json
generated
22
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "chartdb",
|
||||
"version": "1.13.2",
|
||||
"version": "1.14.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "chartdb",
|
||||
"version": "1.13.2",
|
||||
"version": "1.14.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^0.0.51",
|
||||
"@dbml/core": "^3.9.5",
|
||||
@@ -35,7 +35,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@xyflow/react": "^12.3.1",
|
||||
"@xyflow/react": "^12.8.2",
|
||||
"ahooks": "^3.8.1",
|
||||
"ai": "^3.3.14",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -4603,12 +4603,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xyflow/react": {
|
||||
"version": "12.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.4.2.tgz",
|
||||
"integrity": "sha512-AFJKVc/fCPtgSOnRst3xdYJwiEcUN9lDY7EO/YiRvFHYCJGgfzg+jpvZjkTOnBLGyrMJre9378pRxAc3fsR06A==",
|
||||
"version": "12.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.8.2.tgz",
|
||||
"integrity": "sha512-VifLpxOy74ck283NQOtBn1e8igmB7xo7ADDKxyBHkKd8IKpyr16TgaYOhzqVwNMdB4NT+m++zfkic530L+gEXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@xyflow/system": "0.0.50",
|
||||
"@xyflow/system": "0.0.66",
|
||||
"classcat": "^5.0.3",
|
||||
"zustand": "^4.4.0"
|
||||
},
|
||||
@@ -4618,16 +4618,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@xyflow/system": {
|
||||
"version": "0.0.50",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.50.tgz",
|
||||
"integrity": "sha512-HVUZd4LlY88XAaldFh2nwVxDOcdIBxGpQ5txzwfJPf+CAjj2BfYug1fHs2p4yS7YO8H6A3EFJQovBE8YuHkAdg==",
|
||||
"version": "0.0.66",
|
||||
"resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.66.tgz",
|
||||
"integrity": "sha512-TTxESDwPsATnuDMUeYYtKe4wt9v8bRO29dgYBhR8HyhSCzipnAdIL/1CDfFd+WqS1srVreo24u6zZeVIDk4r3Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-drag": "^3.0.7",
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
"@types/d3-selection": "^3.0.10",
|
||||
"@types/d3-transition": "^3.0.8",
|
||||
"@types/d3-zoom": "^3.0.8",
|
||||
"d3-drag": "^3.0.0",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-selection": "^3.0.0",
|
||||
"d3-zoom": "^3.0.0"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chartdb",
|
||||
"private": true,
|
||||
"version": "1.13.2",
|
||||
"version": "1.14.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -43,7 +43,7 @@
|
||||
"@radix-ui/react-toggle-group": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@xyflow/react": "^12.3.1",
|
||||
"@xyflow/react": "^12.8.2",
|
||||
"ahooks": "^3.8.1",
|
||||
"ai": "^3.3.14",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface CodeSnippetAction {
|
||||
label: string;
|
||||
icon: LucideIcon;
|
||||
onClick: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface CodeSnippetProps {
|
||||
@@ -44,6 +45,7 @@ export interface CodeSnippetProps {
|
||||
editorProps?: React.ComponentProps<EditorType>;
|
||||
actions?: CodeSnippetAction[];
|
||||
actionsTooltipSide?: 'top' | 'right' | 'bottom' | 'left';
|
||||
allowCopy?: boolean;
|
||||
}
|
||||
|
||||
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
@@ -58,6 +60,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
editorProps,
|
||||
actions,
|
||||
actionsTooltipSide,
|
||||
allowCopy = true,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const monaco = useMonaco();
|
||||
@@ -131,33 +134,37 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
<Suspense fallback={<Spinner />}>
|
||||
{isComplete ? (
|
||||
<div className="absolute right-1 top-1 z-10 flex flex-col gap-1">
|
||||
<Tooltip
|
||||
onOpenChange={setTooltipOpen}
|
||||
open={isCopied || tooltipOpen}
|
||||
>
|
||||
<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 side={actionsTooltipSide}>
|
||||
{t(
|
||||
isCopied
|
||||
? 'copied'
|
||||
: 'copy_to_clipboard'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{allowCopy ? (
|
||||
<Tooltip
|
||||
onOpenChange={setTooltipOpen}
|
||||
open={isCopied || tooltipOpen}
|
||||
>
|
||||
<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
|
||||
side={actionsTooltipSide}
|
||||
>
|
||||
{t(
|
||||
isCopied
|
||||
? 'copied'
|
||||
: 'copy_to_clipboard'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
|
||||
{actions &&
|
||||
actions.length > 0 &&
|
||||
@@ -166,7 +173,10 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
className="h-fit p-1.5"
|
||||
className={cn(
|
||||
'h-fit p-1.5',
|
||||
action.className
|
||||
)}
|
||||
variant="outline"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
|
||||
51
src/components/code-snippet/dbml/utils.ts
Normal file
51
src/components/code-snippet/dbml/utils.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { DBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
|
||||
import * as monaco from 'monaco-editor';
|
||||
|
||||
export const highlightErrorLine = ({
|
||||
error,
|
||||
model,
|
||||
editorDecorationsCollection,
|
||||
}: {
|
||||
error: DBMLError;
|
||||
model?: monaco.editor.ITextModel | null;
|
||||
editorDecorationsCollection:
|
||||
| monaco.editor.IEditorDecorationsCollection
|
||||
| undefined;
|
||||
}) => {
|
||||
if (!model) return;
|
||||
if (!editorDecorationsCollection) return;
|
||||
|
||||
const decorations = [
|
||||
{
|
||||
range: new monaco.Range(
|
||||
error.line,
|
||||
1,
|
||||
error.line,
|
||||
model.getLineMaxColumn(error.line)
|
||||
),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
className: 'dbml-error-line',
|
||||
glyphMarginClassName: 'dbml-error-glyph',
|
||||
hoverMessage: { value: error.message },
|
||||
overviewRuler: {
|
||||
color: '#ff0000',
|
||||
position: monaco.editor.OverviewRulerLane.Right,
|
||||
darkColor: '#ff0000',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
editorDecorationsCollection?.set(decorations);
|
||||
};
|
||||
|
||||
export const clearErrorHighlight = (
|
||||
editorDecorationsCollection:
|
||||
| monaco.editor.IEditorDecorationsCollection
|
||||
| undefined
|
||||
) => {
|
||||
if (editorDecorationsCollection) {
|
||||
editorDecorationsCollection.clear();
|
||||
}
|
||||
};
|
||||
@@ -37,11 +37,14 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
|
||||
const datatypePattern = dataTypesNames.join('|');
|
||||
|
||||
monaco.languages.setMonarchTokensProvider('dbml', {
|
||||
keywords: ['Table', 'Ref', 'Indexes'],
|
||||
keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum'],
|
||||
datatypes: dataTypesNames,
|
||||
tokenizer: {
|
||||
root: [
|
||||
[/\b(Table|Ref|Indexes)\b/, 'keyword'],
|
||||
[
|
||||
/\b([Tt][Aa][Bb][Ll][Ee]|[Ee][Nn][Uu][Mm]|[Rr][Ee][Ff]|[Ii][Nn][Dd][Ee][Xx][Ee][Ss]|[Nn][Oo][Tt][Ee])\b/,
|
||||
'keyword',
|
||||
],
|
||||
[/\[.*?\]/, 'annotation'],
|
||||
[/'''/, 'string', '@tripleQuoteString'],
|
||||
[/".*?"/, 'string'],
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import React, { type ReactNode, useCallback, useState } from 'react';
|
||||
import { canvasContext } from './canvas-context';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import {
|
||||
adjustTablePositions,
|
||||
shouldShowTablesBySchemaFilter,
|
||||
} from '@/lib/domain/db-table';
|
||||
import { adjustTablePositions } from '@/lib/domain/db-table';
|
||||
import { useReactFlow } from '@xyflow/react';
|
||||
import { findOverlappingTables } from '@/pages/editor-page/canvas/canvas-utils';
|
||||
import type { Graph } from '@/lib/graph';
|
||||
import { createGraph } from '@/lib/graph';
|
||||
import { useDiagramFilter } from '../diagram-filter-context/use-diagram-filter';
|
||||
import { filterTable } from '@/lib/domain/diagram-filter/filter';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
|
||||
interface CanvasProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
||||
const { tables, relationships, updateTablesState, filteredSchemas } =
|
||||
const { tables, relationships, updateTablesState, databaseType } =
|
||||
useChartDB();
|
||||
const { filter } = useDiagramFilter();
|
||||
const { fitView } = useReactFlow();
|
||||
const [overlapGraph, setOverlapGraph] =
|
||||
useState<Graph<string>>(createGraph());
|
||||
@@ -32,9 +33,18 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
||||
const newTables = adjustTablePositions({
|
||||
relationships,
|
||||
tables: tables.filter((table) =>
|
||||
shouldShowTablesBySchemaFilter(table, filteredSchemas)
|
||||
filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
})
|
||||
),
|
||||
mode: 'all', // Use 'all' mode for manual reordering
|
||||
mode: 'all',
|
||||
});
|
||||
|
||||
const updatedOverlapGraph = findOverlappingTables({
|
||||
@@ -69,7 +79,14 @@ export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
||||
});
|
||||
}, 500);
|
||||
},
|
||||
[filteredSchemas, relationships, tables, updateTablesState, fitView]
|
||||
[
|
||||
filter,
|
||||
relationships,
|
||||
tables,
|
||||
updateTablesState,
|
||||
fitView,
|
||||
databaseType,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -81,9 +81,6 @@ export interface ChartDBContext {
|
||||
highlightedCustomType?: DBCustomType;
|
||||
highlightCustomTypeId: (id?: string) => void;
|
||||
|
||||
filteredSchemas?: string[];
|
||||
filterSchemas: (schemaIds: string[]) => void;
|
||||
|
||||
// General operations
|
||||
updateDiagramId: (id: string) => Promise<void>;
|
||||
updateDiagramName: (
|
||||
@@ -95,6 +92,10 @@ export interface ChartDBContext {
|
||||
updateDiagramUpdatedAt: () => Promise<void>;
|
||||
clearDiagramData: () => Promise<void>;
|
||||
deleteDiagram: () => Promise<void>;
|
||||
updateDiagramData: (
|
||||
diagram: Diagram,
|
||||
options?: { forceUpdateStorage?: boolean }
|
||||
) => Promise<void>;
|
||||
|
||||
// Database type operations
|
||||
updateDatabaseType: (databaseType: DatabaseType) => Promise<void>;
|
||||
@@ -280,11 +281,6 @@ export interface ChartDBContext {
|
||||
customType: Partial<DBCustomType>,
|
||||
options?: { updateHistory: boolean }
|
||||
) => Promise<void>;
|
||||
|
||||
// Filters
|
||||
hiddenTableIds?: string[];
|
||||
addHiddenTableId: (tableId: string) => Promise<void>;
|
||||
removeHiddenTableId: (tableId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const chartDBContext = createContext<ChartDBContext>({
|
||||
@@ -298,8 +294,6 @@ export const chartDBContext = createContext<ChartDBContext>({
|
||||
customTypes: [],
|
||||
schemas: [],
|
||||
highlightCustomTypeId: emptyFn,
|
||||
filteredSchemas: [],
|
||||
filterSchemas: emptyFn,
|
||||
currentDiagram: {
|
||||
id: '',
|
||||
name: '',
|
||||
@@ -317,6 +311,7 @@ export const chartDBContext = createContext<ChartDBContext>({
|
||||
loadDiagramFromData: emptyFn,
|
||||
clearDiagramData: emptyFn,
|
||||
deleteDiagram: emptyFn,
|
||||
updateDiagramData: emptyFn,
|
||||
|
||||
// Database type operations
|
||||
updateDatabaseType: emptyFn,
|
||||
@@ -381,9 +376,4 @@ export const chartDBContext = createContext<ChartDBContext>({
|
||||
removeCustomType: emptyFn,
|
||||
removeCustomTypes: emptyFn,
|
||||
updateCustomType: emptyFn,
|
||||
|
||||
// Filters
|
||||
hiddenTableIds: [],
|
||||
addHiddenTableId: emptyFn,
|
||||
removeHiddenTableId: emptyFn,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import { deepCopy, generateId } from '@/lib/utils';
|
||||
import { randomColor } from '@/lib/colors';
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
databasesWithSchemas,
|
||||
schemaNameToSchemaId,
|
||||
} from '@/lib/domain/db-schema';
|
||||
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';
|
||||
@@ -29,7 +28,6 @@ import {
|
||||
DBCustomTypeKind,
|
||||
type DBCustomType,
|
||||
} from '@/lib/domain/db-custom-type';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
|
||||
export interface ChartDBProviderProps {
|
||||
diagram?: Diagram;
|
||||
@@ -40,16 +38,12 @@ export const ChartDBProvider: React.FC<
|
||||
React.PropsWithChildren<ChartDBProviderProps>
|
||||
> = ({ children, diagram, readonly: readonlyProp }) => {
|
||||
const { hasDiff } = useDiff();
|
||||
let db = useStorage();
|
||||
const dbStorage = useStorage();
|
||||
let db = dbStorage;
|
||||
const events = useEventEmitter<ChartDBEvent>();
|
||||
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
||||
const { addUndoAction, resetRedoStack, resetUndoStack } =
|
||||
useRedoUndoStack();
|
||||
const {
|
||||
getHiddenTablesForDiagram,
|
||||
hideTableForDiagram,
|
||||
unhideTableForDiagram,
|
||||
} = useConfig();
|
||||
|
||||
const [diagramId, setDiagramId] = useState('');
|
||||
const [diagramName, setDiagramName] = useState('');
|
||||
const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
|
||||
@@ -71,7 +65,7 @@ export const ChartDBProvider: React.FC<
|
||||
const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
|
||||
diagram?.customTypes ?? []
|
||||
);
|
||||
const [hiddenTableIds, setHiddenTableIds] = useState<string[]>([]);
|
||||
|
||||
const { events: diffEvents } = useDiff();
|
||||
|
||||
const [highlightedCustomTypeId, setHighlightedCustomTypeId] =
|
||||
@@ -95,14 +89,6 @@ export const ChartDBProvider: React.FC<
|
||||
|
||||
diffEvents.useSubscription(diffCalculatedHandler);
|
||||
|
||||
// Sync hiddenTableIds with config
|
||||
useEffect(() => {
|
||||
if (diagramId) {
|
||||
const hiddenTables = getHiddenTablesForDiagram(diagramId);
|
||||
setHiddenTableIds(hiddenTables);
|
||||
}
|
||||
}, [diagramId, getHiddenTablesForDiagram]);
|
||||
|
||||
const defaultSchemaName = defaultSchemas[databaseType];
|
||||
|
||||
const readonly = useMemo(
|
||||
@@ -140,34 +126,6 @@ export const ChartDBProvider: React.FC<
|
||||
[tables, defaultSchemaName, databaseType]
|
||||
);
|
||||
|
||||
const filterSchemas: ChartDBContext['filterSchemas'] = useCallback(
|
||||
(schemaIds) => {
|
||||
setSchemasFilter((prev) => ({
|
||||
...prev,
|
||||
[diagramId]: schemaIds,
|
||||
}));
|
||||
},
|
||||
[diagramId, setSchemasFilter]
|
||||
);
|
||||
|
||||
const filteredSchemas: ChartDBContext['filteredSchemas'] = useMemo(() => {
|
||||
if (schemas.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const schemasFilterFromCache =
|
||||
(schemasFilter[diagramId] ?? []).length === 0
|
||||
? undefined // in case of empty filter, skip cache
|
||||
: schemasFilter[diagramId];
|
||||
|
||||
return (
|
||||
schemasFilterFromCache ?? [
|
||||
schemas.find((s) => s.name === defaultSchemaName)?.id ??
|
||||
schemas[0]?.id,
|
||||
]
|
||||
);
|
||||
}, [schemasFilter, diagramId, schemas, defaultSchemaName]);
|
||||
|
||||
const currentDiagram: Diagram = useMemo(
|
||||
() => ({
|
||||
id: diagramId,
|
||||
@@ -1124,12 +1082,15 @@ export const ChartDBProvider: React.FC<
|
||||
|
||||
const sourceFieldName = sourceField?.name ?? '';
|
||||
|
||||
const targetTable = getTable(targetTableId);
|
||||
const targetTableSchema = targetTable?.schema;
|
||||
|
||||
const relationship: DBRelationship = {
|
||||
id: generateId(),
|
||||
name: `${sourceTableName}_${sourceFieldName}_fk`,
|
||||
sourceSchema: sourceTable?.schema,
|
||||
sourceTableId,
|
||||
targetSchema: sourceTable?.schema,
|
||||
targetSchema: targetTableSchema,
|
||||
targetTableId,
|
||||
sourceFieldId,
|
||||
targetFieldId,
|
||||
@@ -1585,6 +1546,16 @@ export const ChartDBProvider: React.FC<
|
||||
]
|
||||
);
|
||||
|
||||
const updateDiagramData: ChartDBContext['updateDiagramData'] = useCallback(
|
||||
async (diagram, options) => {
|
||||
const st = options?.forceUpdateStorage ? dbStorage : db;
|
||||
await st.deleteDiagram(diagram.id);
|
||||
await st.addDiagram({ diagram });
|
||||
loadDiagramFromData(diagram);
|
||||
},
|
||||
[db, dbStorage, loadDiagramFromData]
|
||||
);
|
||||
|
||||
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
|
||||
async (diagramId: string) => {
|
||||
const diagram = await db.getDiagram(diagramId, {
|
||||
@@ -1748,29 +1719,6 @@ export const ChartDBProvider: React.FC<
|
||||
]
|
||||
);
|
||||
|
||||
const addHiddenTableId: ChartDBContext['addHiddenTableId'] = useCallback(
|
||||
async (tableId: string) => {
|
||||
if (!hiddenTableIds.includes(tableId)) {
|
||||
setHiddenTableIds((prev) => [...prev, tableId]);
|
||||
await hideTableForDiagram(diagramId, tableId);
|
||||
}
|
||||
},
|
||||
[hiddenTableIds, diagramId, hideTableForDiagram]
|
||||
);
|
||||
|
||||
const removeHiddenTableId: ChartDBContext['removeHiddenTableId'] =
|
||||
useCallback(
|
||||
async (tableId: string) => {
|
||||
if (hiddenTableIds.includes(tableId)) {
|
||||
setHiddenTableIds((prev) =>
|
||||
prev.filter((id) => id !== tableId)
|
||||
);
|
||||
await unhideTableForDiagram(diagramId, tableId);
|
||||
}
|
||||
},
|
||||
[hiddenTableIds, diagramId, unhideTableForDiagram]
|
||||
);
|
||||
|
||||
return (
|
||||
<chartDBContext.Provider
|
||||
value={{
|
||||
@@ -1783,10 +1731,9 @@ export const ChartDBProvider: React.FC<
|
||||
areas,
|
||||
currentDiagram,
|
||||
schemas,
|
||||
filteredSchemas,
|
||||
events,
|
||||
readonly,
|
||||
filterSchemas,
|
||||
updateDiagramData,
|
||||
updateDiagramId,
|
||||
updateDiagramName,
|
||||
loadDiagram,
|
||||
@@ -1843,9 +1790,6 @@ export const ChartDBProvider: React.FC<
|
||||
removeCustomType,
|
||||
removeCustomTypes,
|
||||
updateCustomType,
|
||||
hiddenTableIds,
|
||||
addHiddenTableId,
|
||||
removeHiddenTableId,
|
||||
highlightCustomTypeId,
|
||||
highlightedCustomType,
|
||||
}}
|
||||
|
||||
@@ -8,23 +8,9 @@ export interface ConfigContext {
|
||||
config?: Partial<ChartDBConfig>;
|
||||
updateFn?: (config: ChartDBConfig) => ChartDBConfig;
|
||||
}) => Promise<void>;
|
||||
getHiddenTablesForDiagram: (diagramId: string) => string[];
|
||||
setHiddenTablesForDiagram: (
|
||||
diagramId: string,
|
||||
hiddenTableIds: string[]
|
||||
) => Promise<void>;
|
||||
hideTableForDiagram: (diagramId: string, tableId: string) => Promise<void>;
|
||||
unhideTableForDiagram: (
|
||||
diagramId: string,
|
||||
tableId: string
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export const ConfigContext = createContext<ConfigContext>({
|
||||
config: undefined,
|
||||
updateConfig: emptyFn,
|
||||
getHiddenTablesForDiagram: () => [],
|
||||
setHiddenTablesForDiagram: emptyFn,
|
||||
hideTableForDiagram: emptyFn,
|
||||
unhideTableForDiagram: emptyFn,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ConfigContext } from './config-context';
|
||||
|
||||
import { useStorage } from '@/hooks/use-storage';
|
||||
@@ -8,7 +8,7 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { getConfig, updateConfig: updateDataConfig } = useStorage();
|
||||
const [config, setConfig] = React.useState<ChartDBConfig | undefined>();
|
||||
const [config, setConfig] = useState<ChartDBConfig | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
@@ -44,84 +44,11 @@ export const ConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
return promise;
|
||||
};
|
||||
|
||||
const getHiddenTablesForDiagram = (diagramId: string): string[] => {
|
||||
return config?.hiddenTablesByDiagram?.[diagramId] ?? [];
|
||||
};
|
||||
|
||||
const setHiddenTablesForDiagram = async (
|
||||
diagramId: string,
|
||||
hiddenTableIds: string[]
|
||||
): Promise<void> => {
|
||||
return updateConfig({
|
||||
updateFn: (currentConfig) => ({
|
||||
...currentConfig,
|
||||
hiddenTablesByDiagram: {
|
||||
...currentConfig.hiddenTablesByDiagram,
|
||||
[diagramId]: hiddenTableIds,
|
||||
},
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
const hideTableForDiagram = async (
|
||||
diagramId: string,
|
||||
tableId: string
|
||||
): Promise<void> => {
|
||||
return updateConfig({
|
||||
updateFn: (currentConfig) => {
|
||||
const currentHiddenTables =
|
||||
currentConfig.hiddenTablesByDiagram?.[diagramId] ?? [];
|
||||
if (currentHiddenTables.includes(tableId)) {
|
||||
return currentConfig; // Already hidden, no change needed
|
||||
}
|
||||
|
||||
return {
|
||||
...currentConfig,
|
||||
hiddenTablesByDiagram: {
|
||||
...currentConfig.hiddenTablesByDiagram,
|
||||
[diagramId]: [...currentHiddenTables, tableId],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const unhideTableForDiagram = async (
|
||||
diagramId: string,
|
||||
tableId: string
|
||||
): Promise<void> => {
|
||||
return updateConfig({
|
||||
updateFn: (currentConfig) => {
|
||||
const currentHiddenTables =
|
||||
currentConfig.hiddenTablesByDiagram?.[diagramId] ?? [];
|
||||
const filteredTables = currentHiddenTables.filter(
|
||||
(id) => id !== tableId
|
||||
);
|
||||
|
||||
if (filteredTables.length === currentHiddenTables.length) {
|
||||
return currentConfig; // Not hidden, no change needed
|
||||
}
|
||||
|
||||
return {
|
||||
...currentConfig,
|
||||
hiddenTablesByDiagram: {
|
||||
...currentConfig.hiddenTablesByDiagram,
|
||||
[diagramId]: filteredTables,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigContext.Provider
|
||||
value={{
|
||||
config,
|
||||
updateConfig,
|
||||
getHiddenTablesForDiagram,
|
||||
setHiddenTablesForDiagram,
|
||||
hideTableForDiagram,
|
||||
unhideTableForDiagram,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { DBSchema } from '@/lib/domain';
|
||||
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
import { emptyFn } from '@/lib/utils';
|
||||
import { createContext } from 'react';
|
||||
|
||||
export interface DiagramFilterContext {
|
||||
filter?: DiagramFilter;
|
||||
|
||||
hasActiveFilter: boolean;
|
||||
schemasDisplayed: DBSchema[];
|
||||
|
||||
// schemas
|
||||
schemaIdsFilter?: string[];
|
||||
addSchemaIdsFilter: (...ids: string[]) => void;
|
||||
removeSchemaIdsFilter: (...ids: string[]) => void;
|
||||
clearSchemaIdsFilter: () => void;
|
||||
|
||||
// tables
|
||||
tableIdsFilter?: string[];
|
||||
addTableIdsFilter: (...ids: string[]) => void;
|
||||
removeTableIdsFilter: (...ids: string[]) => void;
|
||||
clearTableIdsFilter: () => void;
|
||||
setTableIdsFilterEmpty: () => void;
|
||||
|
||||
// reset
|
||||
resetFilter: () => void;
|
||||
|
||||
// smart filters
|
||||
toggleSchemaFilter: (schemaId: string) => void;
|
||||
toggleTableFilter: (tableId: string) => void;
|
||||
addSchemaIfFiltered: (schemaId: string) => void;
|
||||
}
|
||||
|
||||
export const diagramFilterContext = createContext<DiagramFilterContext>({
|
||||
hasActiveFilter: false,
|
||||
addSchemaIdsFilter: emptyFn,
|
||||
addTableIdsFilter: emptyFn,
|
||||
clearSchemaIdsFilter: emptyFn,
|
||||
clearTableIdsFilter: emptyFn,
|
||||
setTableIdsFilterEmpty: emptyFn,
|
||||
removeSchemaIdsFilter: emptyFn,
|
||||
removeTableIdsFilter: emptyFn,
|
||||
resetFilter: emptyFn,
|
||||
toggleSchemaFilter: emptyFn,
|
||||
toggleTableFilter: emptyFn,
|
||||
addSchemaIfFiltered: emptyFn,
|
||||
schemasDisplayed: [],
|
||||
});
|
||||
442
src/context/diagram-filter-context/diagram-filter-provider.tsx
Normal file
442
src/context/diagram-filter-context/diagram-filter-provider.tsx
Normal file
@@ -0,0 +1,442 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { DiagramFilterContext } from './diagram-filter-context';
|
||||
import { diagramFilterContext } from './diagram-filter-context';
|
||||
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
import { reduceFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
import { useStorage } from '@/hooks/use-storage';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { filterSchema, filterTable } from '@/lib/domain/diagram-filter/filter';
|
||||
import { schemaNameToSchemaId } from '@/lib/domain';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
|
||||
export const DiagramFilterProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { diagramId, tables, schemas, databaseType } = useChartDB();
|
||||
const { getDiagramFilter, updateDiagramFilter } = useStorage();
|
||||
const [filter, setFilter] = useState<DiagramFilter>({});
|
||||
|
||||
const allSchemasIds = useMemo(() => {
|
||||
return schemas.map((schema) => schema.id);
|
||||
}, [schemas]);
|
||||
|
||||
const allTables = useMemo(() => {
|
||||
return tables.map((table) => ({
|
||||
id: table.id,
|
||||
schemaId: table.schema
|
||||
? schemaNameToSchemaId(table.schema)
|
||||
: defaultSchemas[databaseType],
|
||||
schema: table.schema,
|
||||
}));
|
||||
}, [tables, databaseType]);
|
||||
|
||||
const diagramIdOfLoadedFilter = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (diagramId && diagramId === diagramIdOfLoadedFilter.current) {
|
||||
updateDiagramFilter(diagramId, filter);
|
||||
}
|
||||
}, [diagramId, filter, updateDiagramFilter]);
|
||||
|
||||
// Reset filter when diagram changes
|
||||
useEffect(() => {
|
||||
if (diagramIdOfLoadedFilter.current === diagramId) {
|
||||
// If the diagramId hasn't changed, do not reset the filter
|
||||
return;
|
||||
}
|
||||
|
||||
const loadFilterFromStorage = async (diagramId: string) => {
|
||||
if (diagramId) {
|
||||
const storedFilter = await getDiagramFilter(diagramId);
|
||||
setFilter(storedFilter ?? {});
|
||||
}
|
||||
};
|
||||
|
||||
setFilter({});
|
||||
|
||||
if (diagramId) {
|
||||
loadFilterFromStorage(diagramId);
|
||||
diagramIdOfLoadedFilter.current = diagramId;
|
||||
}
|
||||
}, [diagramId, getDiagramFilter]);
|
||||
|
||||
// Schema methods
|
||||
const addSchemaIds: DiagramFilterContext['addSchemaIdsFilter'] =
|
||||
useCallback((...ids: string[]) => {
|
||||
setFilter(
|
||||
(prev) =>
|
||||
({
|
||||
...prev,
|
||||
schemaIds: [
|
||||
...new Set([...(prev.schemaIds || []), ...ids]),
|
||||
],
|
||||
}) satisfies DiagramFilter
|
||||
);
|
||||
}, []);
|
||||
|
||||
const removeSchemaIds: DiagramFilterContext['removeSchemaIdsFilter'] =
|
||||
useCallback((...ids: string[]) => {
|
||||
setFilter(
|
||||
(prev) =>
|
||||
({
|
||||
...prev,
|
||||
schemaIds: prev.schemaIds?.filter(
|
||||
(id) => !ids.includes(id)
|
||||
),
|
||||
}) satisfies DiagramFilter
|
||||
);
|
||||
}, []);
|
||||
|
||||
const clearSchemaIds: DiagramFilterContext['clearSchemaIdsFilter'] =
|
||||
useCallback(() => {
|
||||
setFilter(
|
||||
(prev) =>
|
||||
({
|
||||
...prev,
|
||||
schemaIds: undefined,
|
||||
}) satisfies DiagramFilter
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Table methods
|
||||
const addTableIds: DiagramFilterContext['addTableIdsFilter'] = useCallback(
|
||||
(...ids: string[]) => {
|
||||
setFilter(
|
||||
(prev) =>
|
||||
({
|
||||
...prev,
|
||||
tableIds: [
|
||||
...new Set([...(prev.tableIds || []), ...ids]),
|
||||
],
|
||||
}) satisfies DiagramFilter
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const removeTableIds: DiagramFilterContext['removeTableIdsFilter'] =
|
||||
useCallback((...ids: string[]) => {
|
||||
setFilter(
|
||||
(prev) =>
|
||||
({
|
||||
...prev,
|
||||
tableIds: prev.tableIds?.filter(
|
||||
(id) => !ids.includes(id)
|
||||
),
|
||||
}) satisfies DiagramFilter
|
||||
);
|
||||
}, []);
|
||||
|
||||
const clearTableIds: DiagramFilterContext['clearTableIdsFilter'] =
|
||||
useCallback(() => {
|
||||
setFilter(
|
||||
(prev) =>
|
||||
({
|
||||
...prev,
|
||||
tableIds: undefined,
|
||||
}) satisfies DiagramFilter
|
||||
);
|
||||
}, []);
|
||||
|
||||
const setTableIdsEmpty: DiagramFilterContext['setTableIdsFilterEmpty'] =
|
||||
useCallback(() => {
|
||||
setFilter(
|
||||
(prev) =>
|
||||
({
|
||||
...prev,
|
||||
tableIds: [],
|
||||
}) satisfies DiagramFilter
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Reset filter
|
||||
const resetFilter: DiagramFilterContext['resetFilter'] = useCallback(() => {
|
||||
setFilter({});
|
||||
}, []);
|
||||
|
||||
const toggleSchemaFilter: DiagramFilterContext['toggleSchemaFilter'] =
|
||||
useCallback(
|
||||
(schemaId: string) => {
|
||||
setFilter((prev) => {
|
||||
const currentSchemaIds = prev.schemaIds;
|
||||
|
||||
// Check if schema is currently visible
|
||||
const isSchemaVisible = filterSchema({
|
||||
schemaId,
|
||||
schemaIdsFilter: currentSchemaIds,
|
||||
});
|
||||
|
||||
let newSchemaIds: string[] | undefined;
|
||||
let newTableIds: string[] | undefined = prev.tableIds;
|
||||
|
||||
if (isSchemaVisible) {
|
||||
// Schema is visible, make it not visible
|
||||
if (!currentSchemaIds) {
|
||||
// All schemas are visible, create filter with all except this one
|
||||
newSchemaIds = allSchemasIds.filter(
|
||||
(id) => id !== schemaId
|
||||
);
|
||||
} else {
|
||||
// Remove this schema from the filter
|
||||
newSchemaIds = currentSchemaIds.filter(
|
||||
(id) => id !== schemaId
|
||||
);
|
||||
}
|
||||
|
||||
// Remove tables from this schema from tableIds if present
|
||||
if (prev.tableIds) {
|
||||
const schemaTableIds = allTables
|
||||
.filter((table) => table.schemaId === schemaId)
|
||||
.map((table) => table.id);
|
||||
newTableIds = prev.tableIds.filter(
|
||||
(id) => !schemaTableIds.includes(id)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Schema is not visible, make it visible
|
||||
newSchemaIds = [
|
||||
...new Set([...(currentSchemaIds || []), schemaId]),
|
||||
];
|
||||
|
||||
// Add tables from this schema to tableIds if tableIds is defined
|
||||
if (prev.tableIds) {
|
||||
const schemaTableIds = allTables
|
||||
.filter((table) => table.schemaId === schemaId)
|
||||
.map((table) => table.id);
|
||||
newTableIds = [
|
||||
...new Set([
|
||||
...prev.tableIds,
|
||||
...schemaTableIds,
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Use reduceFilter to optimize and handle edge cases
|
||||
return reduceFilter(
|
||||
{
|
||||
schemaIds: newSchemaIds,
|
||||
tableIds: newTableIds,
|
||||
},
|
||||
allTables
|
||||
);
|
||||
});
|
||||
},
|
||||
[allSchemasIds, allTables]
|
||||
);
|
||||
|
||||
const toggleTableFilterForNoSchema = useCallback(
|
||||
(tableId: string) => {
|
||||
setFilter((prev) => {
|
||||
const currentTableIds = prev.tableIds;
|
||||
|
||||
// Check if table is currently visible
|
||||
const isTableVisible = filterTable({
|
||||
table: { id: tableId, schema: undefined },
|
||||
filter: prev,
|
||||
options: { defaultSchema: undefined },
|
||||
});
|
||||
|
||||
let newTableIds: string[] | undefined;
|
||||
|
||||
if (isTableVisible) {
|
||||
// Table is visible, make it not visible
|
||||
if (!currentTableIds) {
|
||||
// All tables are visible, create filter with all except this one
|
||||
newTableIds = allTables
|
||||
.filter((t) => t.id !== tableId)
|
||||
.map((t) => t.id);
|
||||
} else {
|
||||
// Remove this table from the filter
|
||||
newTableIds = currentTableIds.filter(
|
||||
(id) => id !== tableId
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Table is not visible, make it visible
|
||||
newTableIds = [
|
||||
...new Set([...(currentTableIds || []), tableId]),
|
||||
];
|
||||
}
|
||||
|
||||
// Use reduceFilter to optimize and handle edge cases
|
||||
return reduceFilter(
|
||||
{
|
||||
schemaIds: undefined,
|
||||
tableIds: newTableIds,
|
||||
},
|
||||
allTables
|
||||
);
|
||||
});
|
||||
},
|
||||
[allTables]
|
||||
);
|
||||
|
||||
const toggleTableFilter: DiagramFilterContext['toggleTableFilter'] =
|
||||
useCallback(
|
||||
(tableId: string) => {
|
||||
if (!defaultSchemas[databaseType]) {
|
||||
// No schemas, toggle table filter without schema context
|
||||
toggleTableFilterForNoSchema(tableId);
|
||||
return;
|
||||
}
|
||||
|
||||
setFilter((prev) => {
|
||||
// Find the table in the tables list
|
||||
const tableInfo = allTables.find((t) => t.id === tableId);
|
||||
|
||||
if (!tableInfo) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
// Check if table is currently visible using filterTable
|
||||
const isTableVisible = filterTable({
|
||||
table: {
|
||||
id: tableInfo.id,
|
||||
schema: tableInfo.schema,
|
||||
},
|
||||
filter: prev,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
});
|
||||
|
||||
let newSchemaIds = prev.schemaIds;
|
||||
let newTableIds = prev.tableIds;
|
||||
|
||||
if (isTableVisible) {
|
||||
// Table is visible, make it not visible
|
||||
|
||||
// If the table is visible due to its schema being in schemaIds
|
||||
if (
|
||||
tableInfo?.schemaId &&
|
||||
prev.schemaIds?.includes(tableInfo.schemaId)
|
||||
) {
|
||||
// Remove the schema from schemaIds and add all other tables from that schema to tableIds
|
||||
newSchemaIds = prev.schemaIds.filter(
|
||||
(id) => id !== tableInfo.schemaId
|
||||
);
|
||||
|
||||
// Get all other tables from this schema (except the one being toggled)
|
||||
const otherTablesFromSchema = allTables
|
||||
.filter(
|
||||
(t) =>
|
||||
t.schemaId === tableInfo.schemaId &&
|
||||
t.id !== tableId
|
||||
)
|
||||
.map((t) => t.id);
|
||||
|
||||
// Add these tables to tableIds
|
||||
newTableIds = [
|
||||
...(prev.tableIds || []),
|
||||
...otherTablesFromSchema,
|
||||
];
|
||||
} else if (prev.tableIds?.includes(tableId)) {
|
||||
// Table is visible because it's in tableIds, remove it
|
||||
newTableIds = prev.tableIds.filter(
|
||||
(id) => id !== tableId
|
||||
);
|
||||
} else if (!prev.tableIds && !prev.schemaIds) {
|
||||
// No filters = all visible, create filter with all tables except this one
|
||||
newTableIds = allTables
|
||||
.filter((t) => t.id !== tableId)
|
||||
.map((t) => t.id);
|
||||
}
|
||||
} else {
|
||||
// Table is not visible, make it visible by adding to tableIds
|
||||
newTableIds = [...(prev.tableIds || []), tableId];
|
||||
}
|
||||
|
||||
// Use reduceFilter to optimize and handle edge cases
|
||||
return reduceFilter(
|
||||
{
|
||||
schemaIds: newSchemaIds,
|
||||
tableIds: newTableIds,
|
||||
},
|
||||
allTables
|
||||
);
|
||||
});
|
||||
},
|
||||
[allTables, databaseType, toggleTableFilterForNoSchema]
|
||||
);
|
||||
|
||||
const addSchemaIfFiltered: DiagramFilterContext['addSchemaIfFiltered'] =
|
||||
useCallback(
|
||||
(schemaId: string) => {
|
||||
setFilter((prev) => {
|
||||
const currentSchemaIds = prev.schemaIds;
|
||||
if (!currentSchemaIds) {
|
||||
// No schemas are filtered
|
||||
return prev;
|
||||
}
|
||||
|
||||
// If schema is already filtered, do nothing
|
||||
if (currentSchemaIds.includes(schemaId)) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
// Add schema to the filter
|
||||
const newSchemaIds = [...currentSchemaIds, schemaId];
|
||||
|
||||
if (newSchemaIds.length === allSchemasIds.length) {
|
||||
// All schemas are now filtered, set to undefined
|
||||
return {
|
||||
...prev,
|
||||
schemaIds: undefined,
|
||||
} satisfies DiagramFilter;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
schemaIds: newSchemaIds,
|
||||
} satisfies DiagramFilter;
|
||||
});
|
||||
},
|
||||
[allSchemasIds.length]
|
||||
);
|
||||
|
||||
const hasActiveFilter: boolean = useMemo(() => {
|
||||
return !!filter.schemaIds || !!filter.tableIds;
|
||||
}, [filter]);
|
||||
|
||||
const schemasDisplayed: DiagramFilterContext['schemasDisplayed'] =
|
||||
useMemo(() => {
|
||||
if (!filter.schemaIds) {
|
||||
return schemas;
|
||||
}
|
||||
|
||||
return schemas.filter((schema) =>
|
||||
filter.schemaIds?.includes(schema.id)
|
||||
);
|
||||
}, [filter.schemaIds, schemas]);
|
||||
|
||||
const value: DiagramFilterContext = {
|
||||
filter,
|
||||
schemaIdsFilter: filter.schemaIds,
|
||||
addSchemaIdsFilter: addSchemaIds,
|
||||
removeSchemaIdsFilter: removeSchemaIds,
|
||||
clearSchemaIdsFilter: clearSchemaIds,
|
||||
setTableIdsFilterEmpty: setTableIdsEmpty,
|
||||
tableIdsFilter: filter.tableIds,
|
||||
addTableIdsFilter: addTableIds,
|
||||
removeTableIdsFilter: removeTableIds,
|
||||
clearTableIdsFilter: clearTableIds,
|
||||
resetFilter,
|
||||
toggleSchemaFilter,
|
||||
toggleTableFilter,
|
||||
addSchemaIfFiltered,
|
||||
hasActiveFilter,
|
||||
schemasDisplayed,
|
||||
};
|
||||
|
||||
return (
|
||||
<diagramFilterContext.Provider value={value}>
|
||||
{children}
|
||||
</diagramFilterContext.Provider>
|
||||
);
|
||||
};
|
||||
4
src/context/diagram-filter-context/use-diagram-filter.ts
Normal file
4
src/context/diagram-filter-context/use-diagram-filter.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useContext } from 'react';
|
||||
import { diagramFilterContext } from './diagram-filter-context';
|
||||
|
||||
export const useDiagramFilter = () => useContext(diagramFilterContext);
|
||||
@@ -32,14 +32,20 @@ export interface DiffContext {
|
||||
originalDiagram: Diagram | null;
|
||||
diffMap: DiffMap;
|
||||
hasDiff: boolean;
|
||||
isSummaryOnly: boolean;
|
||||
|
||||
calculateDiff: ({
|
||||
diagram,
|
||||
newDiagram,
|
||||
options,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
newDiagram: Diagram;
|
||||
options?: {
|
||||
summaryOnly?: boolean;
|
||||
};
|
||||
}) => void;
|
||||
resetDiff: () => void;
|
||||
|
||||
// table diff
|
||||
checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
|
||||
@@ -60,6 +66,15 @@ export interface DiffContext {
|
||||
checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
|
||||
getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
|
||||
getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
|
||||
getFieldNewPrimaryKey: ({ fieldId }: { fieldId: string }) => boolean | null;
|
||||
getFieldNewNullable: ({ fieldId }: { fieldId: string }) => boolean | null;
|
||||
getFieldNewCharacterMaximumLength: ({
|
||||
fieldId,
|
||||
}: {
|
||||
fieldId: string;
|
||||
}) => string | null;
|
||||
getFieldNewScale: ({ fieldId }: { fieldId: string }) => number | null;
|
||||
getFieldNewPrecision: ({ fieldId }: { fieldId: string }) => number | null;
|
||||
|
||||
// relationship diff
|
||||
checkIfNewRelationship: ({
|
||||
|
||||
@@ -32,6 +32,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
const [fieldsChanged, setFieldsChanged] = React.useState<
|
||||
Map<string, boolean>
|
||||
>(new Map<string, boolean>());
|
||||
const [isSummaryOnly, setIsSummaryOnly] = React.useState<boolean>(false);
|
||||
|
||||
const events = useEventEmitter<DiffEvent>();
|
||||
|
||||
@@ -127,7 +128,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
);
|
||||
|
||||
const calculateDiff: DiffContext['calculateDiff'] = useCallback(
|
||||
({ diagram, newDiagram: newDiagramArg }) => {
|
||||
({ diagram, newDiagram: newDiagramArg, options }) => {
|
||||
const {
|
||||
diffMap: newDiffs,
|
||||
changedTables: newChangedTables,
|
||||
@@ -139,6 +140,7 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setFieldsChanged(newChangedFields);
|
||||
setNewDiagram(newDiagramArg);
|
||||
setOriginalDiagram(diagram);
|
||||
setIsSummaryOnly(options?.summaryOnly ?? false);
|
||||
|
||||
events.emit({
|
||||
action: 'diff_calculated',
|
||||
@@ -305,6 +307,117 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getFieldNewPrimaryKey = useCallback<
|
||||
DiffContext['getFieldNewPrimaryKey']
|
||||
>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
attribute: 'primaryKey',
|
||||
});
|
||||
|
||||
if (diffMap.has(fieldKey)) {
|
||||
const diff = diffMap.get(fieldKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as boolean;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getFieldNewNullable = useCallback<DiffContext['getFieldNewNullable']>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
attribute: 'nullable',
|
||||
});
|
||||
|
||||
if (diffMap.has(fieldKey)) {
|
||||
const diff = diffMap.get(fieldKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as boolean;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getFieldNewCharacterMaximumLength = useCallback<
|
||||
DiffContext['getFieldNewCharacterMaximumLength']
|
||||
>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
attribute: 'characterMaximumLength',
|
||||
});
|
||||
|
||||
if (diffMap.has(fieldKey)) {
|
||||
const diff = diffMap.get(fieldKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as string;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getFieldNewScale = useCallback<DiffContext['getFieldNewScale']>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
attribute: 'scale',
|
||||
});
|
||||
|
||||
if (diffMap.has(fieldKey)) {
|
||||
const diff = diffMap.get(fieldKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as number;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getFieldNewPrecision = useCallback<
|
||||
DiffContext['getFieldNewPrecision']
|
||||
>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
attribute: 'precision',
|
||||
});
|
||||
|
||||
if (diffMap.has(fieldKey)) {
|
||||
const diff = diffMap.get(fieldKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as number;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfNewRelationship = useCallback<
|
||||
DiffContext['checkIfNewRelationship']
|
||||
>(
|
||||
@@ -339,6 +452,15 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const resetDiff = useCallback<DiffContext['resetDiff']>(() => {
|
||||
setDiffMap(new Map<string, ChartDBDiff>());
|
||||
setTablesChanged(new Map<string, boolean>());
|
||||
setFieldsChanged(new Map<string, boolean>());
|
||||
setNewDiagram(null);
|
||||
setOriginalDiagram(null);
|
||||
setIsSummaryOnly(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<diffContext.Provider
|
||||
value={{
|
||||
@@ -346,8 +468,10 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
originalDiagram,
|
||||
diffMap,
|
||||
hasDiff: diffMap.size > 0,
|
||||
isSummaryOnly,
|
||||
|
||||
calculateDiff,
|
||||
resetDiff,
|
||||
|
||||
// table diff
|
||||
getTableNewName,
|
||||
@@ -362,6 +486,11 @@ export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
checkIfNewField,
|
||||
getFieldNewName,
|
||||
getFieldNewType,
|
||||
getFieldNewPrimaryKey,
|
||||
getFieldNewNullable,
|
||||
getFieldNewCharacterMaximumLength,
|
||||
getFieldNewScale,
|
||||
getFieldNewPrecision,
|
||||
|
||||
// relationship diff
|
||||
checkIfNewRelationship,
|
||||
|
||||
@@ -36,10 +36,6 @@ export interface LayoutContext {
|
||||
hideSidePanel: () => void;
|
||||
showSidePanel: () => void;
|
||||
toggleSidePanel: () => void;
|
||||
|
||||
isSelectSchemaOpen: boolean;
|
||||
openSelectSchema: () => void;
|
||||
closeSelectSchema: () => void;
|
||||
}
|
||||
|
||||
export const layoutContext = createContext<LayoutContext>({
|
||||
@@ -70,8 +66,4 @@ export const layoutContext = createContext<LayoutContext>({
|
||||
hideSidePanel: emptyFn,
|
||||
showSidePanel: emptyFn,
|
||||
toggleSidePanel: emptyFn,
|
||||
|
||||
isSelectSchemaOpen: false,
|
||||
openSelectSchema: emptyFn,
|
||||
closeSelectSchema: emptyFn,
|
||||
});
|
||||
|
||||
@@ -23,8 +23,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
||||
React.useState<SidebarSection>('tables');
|
||||
const [isSidePanelShowed, setIsSidePanelShowed] =
|
||||
React.useState<boolean>(isDesktop);
|
||||
const [isSelectSchemaOpen, setIsSelectSchemaOpen] =
|
||||
React.useState<boolean>(false);
|
||||
|
||||
const closeAllTablesInSidebar: LayoutContext['closeAllTablesInSidebar'] =
|
||||
() => setOpenedTableInSidebar('');
|
||||
@@ -88,11 +86,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setOpenedTableInSidebar(customTypeId);
|
||||
};
|
||||
|
||||
const openSelectSchema: LayoutContext['openSelectSchema'] = () =>
|
||||
setIsSelectSchemaOpen(true);
|
||||
|
||||
const closeSelectSchema: LayoutContext['closeSelectSchema'] = () =>
|
||||
setIsSelectSchemaOpen(false);
|
||||
return (
|
||||
<layoutContext.Provider
|
||||
value={{
|
||||
@@ -108,9 +101,6 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
||||
hideSidePanel,
|
||||
showSidePanel,
|
||||
toggleSidePanel,
|
||||
isSelectSchemaOpen,
|
||||
openSelectSchema,
|
||||
closeSelectSchema,
|
||||
openedDependencyInSidebar,
|
||||
openDependencyFromSidebar,
|
||||
closeAllDependenciesInSidebar,
|
||||
|
||||
@@ -4,8 +4,6 @@ import type { Theme } from '../theme-context/theme-context';
|
||||
|
||||
export type ScrollAction = 'pan' | 'zoom';
|
||||
|
||||
export type SchemasFilter = Record<string, string[]>;
|
||||
|
||||
export interface LocalConfigContext {
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
@@ -13,20 +11,12 @@ export interface LocalConfigContext {
|
||||
scrollAction: ScrollAction;
|
||||
setScrollAction: (action: ScrollAction) => void;
|
||||
|
||||
schemasFilter: SchemasFilter;
|
||||
setSchemasFilter: React.Dispatch<React.SetStateAction<SchemasFilter>>;
|
||||
|
||||
showCardinality: boolean;
|
||||
setShowCardinality: (showCardinality: boolean) => void;
|
||||
|
||||
showFieldAttributes: boolean;
|
||||
setShowFieldAttributes: (showFieldAttributes: boolean) => void;
|
||||
|
||||
hideMultiSchemaNotification: boolean;
|
||||
setHideMultiSchemaNotification: (
|
||||
hideMultiSchemaNotification: boolean
|
||||
) => void;
|
||||
|
||||
githubRepoOpened: boolean;
|
||||
setGithubRepoOpened: (githubRepoOpened: boolean) => void;
|
||||
|
||||
@@ -47,18 +37,12 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
||||
scrollAction: 'pan',
|
||||
setScrollAction: emptyFn,
|
||||
|
||||
schemasFilter: {},
|
||||
setSchemasFilter: emptyFn,
|
||||
|
||||
showCardinality: true,
|
||||
setShowCardinality: emptyFn,
|
||||
|
||||
showFieldAttributes: true,
|
||||
setShowFieldAttributes: emptyFn,
|
||||
|
||||
hideMultiSchemaNotification: false,
|
||||
setHideMultiSchemaNotification: emptyFn,
|
||||
|
||||
githubRepoOpened: false,
|
||||
setGithubRepoOpened: emptyFn,
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import type { SchemasFilter, ScrollAction } from './local-config-context';
|
||||
import type { ScrollAction } from './local-config-context';
|
||||
import { LocalConfigContext } from './local-config-context';
|
||||
import type { Theme } from '../theme-context/theme-context';
|
||||
|
||||
const themeKey = 'theme';
|
||||
const scrollActionKey = 'scroll_action';
|
||||
const schemasFilterKey = 'schemas_filter';
|
||||
const showCardinalityKey = 'show_cardinality';
|
||||
const showFieldAttributesKey = 'show_field_attributes';
|
||||
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
|
||||
const githubRepoOpenedKey = 'github_repo_opened';
|
||||
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
||||
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
|
||||
@@ -25,12 +23,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
(localStorage.getItem(scrollActionKey) as ScrollAction) || 'pan'
|
||||
);
|
||||
|
||||
const [schemasFilter, setSchemasFilter] = React.useState<SchemasFilter>(
|
||||
JSON.parse(
|
||||
localStorage.getItem(schemasFilterKey) || '{}'
|
||||
) as SchemasFilter
|
||||
);
|
||||
|
||||
const [showCardinality, setShowCardinality] = React.useState<boolean>(
|
||||
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
|
||||
);
|
||||
@@ -40,12 +32,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
(localStorage.getItem(showFieldAttributesKey) || 'true') === 'true'
|
||||
);
|
||||
|
||||
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
|
||||
React.useState<boolean>(
|
||||
(localStorage.getItem(hideMultiSchemaNotificationKey) ||
|
||||
'false') === 'true'
|
||||
);
|
||||
|
||||
const [githubRepoOpened, setGithubRepoOpened] = React.useState<boolean>(
|
||||
(localStorage.getItem(githubRepoOpenedKey) || 'false') === 'true'
|
||||
);
|
||||
@@ -77,13 +63,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
|
||||
}, [githubRepoOpened]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(
|
||||
hideMultiSchemaNotificationKey,
|
||||
hideMultiSchemaNotification.toString()
|
||||
);
|
||||
}, [hideMultiSchemaNotification]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(themeKey, theme);
|
||||
}, [theme]);
|
||||
@@ -92,10 +71,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
localStorage.setItem(scrollActionKey, scrollAction);
|
||||
}, [scrollAction]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(schemasFilterKey, JSON.stringify(schemasFilter));
|
||||
}, [schemasFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(showCardinalityKey, showCardinality.toString());
|
||||
}, [showCardinality]);
|
||||
@@ -121,14 +96,10 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setTheme,
|
||||
scrollAction,
|
||||
setScrollAction,
|
||||
schemasFilter,
|
||||
setSchemasFilter,
|
||||
showCardinality,
|
||||
setShowCardinality,
|
||||
showFieldAttributes,
|
||||
setShowFieldAttributes,
|
||||
hideMultiSchemaNotification,
|
||||
setHideMultiSchemaNotification,
|
||||
setGithubRepoOpened,
|
||||
githubRepoOpened,
|
||||
starUsDialogLastOpen,
|
||||
|
||||
@@ -7,12 +7,21 @@ import type { ChartDBConfig } from '@/lib/domain/config';
|
||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||
import type { Area } from '@/lib/domain/area';
|
||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
|
||||
export interface StorageContext {
|
||||
// Config operations
|
||||
getConfig: () => Promise<ChartDBConfig | undefined>;
|
||||
updateConfig: (config: Partial<ChartDBConfig>) => Promise<void>;
|
||||
|
||||
// Diagram filter operations
|
||||
getDiagramFilter: (diagramId: string) => Promise<DiagramFilter | undefined>;
|
||||
updateDiagramFilter: (
|
||||
diagramId: string,
|
||||
filter: DiagramFilter
|
||||
) => Promise<void>;
|
||||
deleteDiagramFilter: (diagramId: string) => Promise<void>;
|
||||
|
||||
// Diagram operations
|
||||
addDiagram: (params: { diagram: Diagram }) => Promise<void>;
|
||||
listDiagrams: (options?: {
|
||||
@@ -132,6 +141,10 @@ export const storageInitialValue: StorageContext = {
|
||||
getConfig: emptyFn,
|
||||
updateConfig: emptyFn,
|
||||
|
||||
getDiagramFilter: emptyFn,
|
||||
updateDiagramFilter: emptyFn,
|
||||
deleteDiagramFilter: emptyFn,
|
||||
|
||||
addDiagram: emptyFn,
|
||||
listDiagrams: emptyFn,
|
||||
getDiagram: emptyFn,
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { ChartDBConfig } from '@/lib/domain/config';
|
||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||
import type { Area } from '@/lib/domain/area';
|
||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
|
||||
export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
@@ -44,6 +45,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
ChartDBConfig & { id: number },
|
||||
'id' // primary key "id" (for the typings only)
|
||||
>;
|
||||
diagram_filters: EntityTable<
|
||||
DiagramFilter & { diagramId: string },
|
||||
'diagramId' // primary key "id" (for the typings only)
|
||||
>;
|
||||
};
|
||||
|
||||
// Schema declaration:
|
||||
@@ -190,6 +195,27 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
config: '++id, defaultDiagramId',
|
||||
});
|
||||
|
||||
dexieDB
|
||||
.version(12)
|
||||
.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',
|
||||
db_custom_types:
|
||||
'++id, diagramId, schema, type, kind, values, fields',
|
||||
config: '++id, defaultDiagramId',
|
||||
diagram_filters: 'diagramId, tableIds, schemasIds',
|
||||
})
|
||||
.upgrade((tx) => {
|
||||
tx.table('config').clear();
|
||||
});
|
||||
|
||||
dexieDB.on('ready', async () => {
|
||||
const config = await dexieDB.config.get(1);
|
||||
|
||||
@@ -217,6 +243,32 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
[db]
|
||||
);
|
||||
|
||||
const getDiagramFilter: StorageContext['getDiagramFilter'] = useCallback(
|
||||
async (diagramId: string): Promise<DiagramFilter | undefined> => {
|
||||
return await db.diagram_filters.get({ diagramId });
|
||||
},
|
||||
[db]
|
||||
);
|
||||
|
||||
const updateDiagramFilter: StorageContext['updateDiagramFilter'] =
|
||||
useCallback(
|
||||
async (diagramId, filter): Promise<void> => {
|
||||
await db.diagram_filters.put({
|
||||
diagramId,
|
||||
...filter,
|
||||
});
|
||||
},
|
||||
[db]
|
||||
);
|
||||
|
||||
const deleteDiagramFilter: StorageContext['deleteDiagramFilter'] =
|
||||
useCallback(
|
||||
async (diagramId: string): Promise<void> => {
|
||||
await db.diagram_filters.where({ diagramId }).delete();
|
||||
},
|
||||
[db]
|
||||
);
|
||||
|
||||
const addTable: StorageContext['addTable'] = useCallback(
|
||||
async ({ diagramId, table }) => {
|
||||
await db.db_tables.add({
|
||||
@@ -756,6 +808,9 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
deleteCustomType,
|
||||
listCustomTypes,
|
||||
deleteDiagramCustomTypes,
|
||||
getDiagramFilter,
|
||||
updateDiagramFilter,
|
||||
deleteDiagramFilter,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -20,12 +20,18 @@ import {
|
||||
} from '@/lib/data/export-metadata/export-sql-script';
|
||||
import { databaseTypeToLabelMap } from '@/lib/databases';
|
||||
import { DatabaseType } from '@/lib/domain/database-type';
|
||||
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
|
||||
import { Annoyed, Sparkles } from 'lucide-react';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||
import {
|
||||
filterDependency,
|
||||
filterRelationship,
|
||||
filterTable,
|
||||
} from '@/lib/domain/diagram-filter/filter';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
|
||||
export interface ExportSQLDialogProps extends BaseDialogProps {
|
||||
targetDatabaseType: DatabaseType;
|
||||
@@ -36,7 +42,8 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
|
||||
targetDatabaseType,
|
||||
}) => {
|
||||
const { closeExportSQLDialog } = useDialog();
|
||||
const { currentDiagram, filteredSchemas } = useChartDB();
|
||||
const { currentDiagram } = useChartDB();
|
||||
const { filter } = useDiagramFilter();
|
||||
const { t } = useTranslation();
|
||||
const [script, setScript] = React.useState<string>();
|
||||
const [error, setError] = React.useState<boolean>(false);
|
||||
@@ -48,7 +55,16 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
|
||||
const filteredDiagram: Diagram = {
|
||||
...currentDiagram,
|
||||
tables: currentDiagram.tables?.filter((table) =>
|
||||
shouldShowTablesBySchemaFilter(table, filteredSchemas)
|
||||
filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[targetDatabaseType],
|
||||
},
|
||||
})
|
||||
),
|
||||
relationships: currentDiagram.relationships?.filter((rel) => {
|
||||
const sourceTable = currentDiagram.tables?.find(
|
||||
@@ -60,11 +76,20 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
|
||||
return (
|
||||
sourceTable &&
|
||||
targetTable &&
|
||||
shouldShowTablesBySchemaFilter(
|
||||
sourceTable,
|
||||
filteredSchemas
|
||||
) &&
|
||||
shouldShowTablesBySchemaFilter(targetTable, filteredSchemas)
|
||||
filterRelationship({
|
||||
tableA: {
|
||||
id: sourceTable.id,
|
||||
schema: sourceTable.schema,
|
||||
},
|
||||
tableB: {
|
||||
id: targetTable.id,
|
||||
schema: targetTable.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[targetDatabaseType],
|
||||
},
|
||||
})
|
||||
);
|
||||
}),
|
||||
dependencies: currentDiagram.dependencies?.filter((dep) => {
|
||||
@@ -77,11 +102,20 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
|
||||
return (
|
||||
table &&
|
||||
dependentTable &&
|
||||
shouldShowTablesBySchemaFilter(table, filteredSchemas) &&
|
||||
shouldShowTablesBySchemaFilter(
|
||||
dependentTable,
|
||||
filteredSchemas
|
||||
)
|
||||
filterDependency({
|
||||
tableA: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
tableB: {
|
||||
id: dependentTable.id,
|
||||
schema: dependentTable.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[targetDatabaseType],
|
||||
},
|
||||
})
|
||||
);
|
||||
}),
|
||||
};
|
||||
@@ -101,7 +135,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
|
||||
signal: abortControllerRef.current?.signal,
|
||||
});
|
||||
}
|
||||
}, [targetDatabaseType, currentDiagram, filteredSchemas]);
|
||||
}, [targetDatabaseType, currentDiagram, filter]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialog.open) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import React, {
|
||||
Suspense,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import * as monaco from 'monaco-editor';
|
||||
import type * as monaco from 'monaco-editor';
|
||||
import { useDialog } from '@/hooks/use-dialog';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -36,45 +36,11 @@ import type { DBTable } from '@/lib/domain/db-table';
|
||||
import { useToast } from '@/components/toast/use-toast';
|
||||
import { Spinner } from '@/components/spinner/spinner';
|
||||
import { debounce } from '@/lib/utils';
|
||||
|
||||
interface DBMLError {
|
||||
message: string;
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
function parseDBMLError(error: unknown): DBMLError | null {
|
||||
try {
|
||||
if (typeof error === 'string') {
|
||||
const parsed = JSON.parse(error);
|
||||
if (parsed.diags?.[0]) {
|
||||
const diag = parsed.diags[0];
|
||||
return {
|
||||
message: diag.message,
|
||||
line: diag.location.start.line,
|
||||
column: diag.location.start.column,
|
||||
};
|
||||
}
|
||||
} else if (error && typeof error === 'object' && 'diags' in error) {
|
||||
const parsed = error as {
|
||||
diags: Array<{
|
||||
message: string;
|
||||
location: { start: { line: number; column: number } };
|
||||
}>;
|
||||
};
|
||||
if (parsed.diags?.[0]) {
|
||||
return {
|
||||
message: parsed.diags[0].message,
|
||||
line: parsed.diags[0].location.start.line,
|
||||
column: parsed.diags[0].location.start.column,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing DBML error:', e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
|
||||
import {
|
||||
clearErrorHighlight,
|
||||
highlightErrorLine,
|
||||
} from '@/components/code-snippet/dbml/utils';
|
||||
|
||||
export interface ImportDBMLDialogProps extends BaseDialogProps {
|
||||
withCreateEmptyDiagram?: boolean;
|
||||
@@ -150,39 +116,8 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
}
|
||||
}, [reorder, reorderTables]);
|
||||
|
||||
const highlightErrorLine = useCallback((error: DBMLError) => {
|
||||
if (!editorRef.current) return;
|
||||
|
||||
const model = editorRef.current.getModel();
|
||||
if (!model) return;
|
||||
|
||||
const decorations = [
|
||||
{
|
||||
range: new monaco.Range(
|
||||
error.line,
|
||||
1,
|
||||
error.line,
|
||||
model.getLineMaxColumn(error.line)
|
||||
),
|
||||
options: {
|
||||
isWholeLine: true,
|
||||
className: 'dbml-error-line',
|
||||
glyphMarginClassName: 'dbml-error-glyph',
|
||||
hoverMessage: { value: error.message },
|
||||
overviewRuler: {
|
||||
color: '#ff0000',
|
||||
position: monaco.editor.OverviewRulerLane.Right,
|
||||
darkColor: '#ff0000',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
decorationsCollection.current?.set(decorations);
|
||||
}, []);
|
||||
|
||||
const clearDecorations = useCallback(() => {
|
||||
decorationsCollection.current?.clear();
|
||||
clearErrorHighlight(decorationsCollection.current);
|
||||
}, []);
|
||||
|
||||
const validateDBML = useCallback(
|
||||
@@ -205,7 +140,12 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
t('import_dbml_dialog.error.description') +
|
||||
` (1 error found - in line ${parsedError.line})`
|
||||
);
|
||||
highlightErrorLine(parsedError);
|
||||
highlightErrorLine({
|
||||
error: parsedError,
|
||||
model: editorRef.current?.getModel(),
|
||||
editorDecorationsCollection:
|
||||
decorationsCollection.current,
|
||||
});
|
||||
} else {
|
||||
setErrorMessage(
|
||||
e instanceof Error ? e.message : JSON.stringify(e)
|
||||
@@ -213,7 +153,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
}
|
||||
}
|
||||
},
|
||||
[clearDecorations, highlightErrorLine, t]
|
||||
[clearDecorations, t]
|
||||
);
|
||||
|
||||
const debouncedValidateRef = useRef<((value: string) => void) | null>(null);
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
import { Label } from '@/components/label/label';
|
||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||
|
||||
export interface TableSchemaDialogProps extends BaseDialogProps {
|
||||
table?: DBTable;
|
||||
@@ -44,7 +45,8 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
allowSchemaCreation = false,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { databaseType, filteredSchemas, filterSchemas } = useChartDB();
|
||||
const { databaseType } = useChartDB();
|
||||
const { addSchemaIfFiltered } = useDiagramFilter();
|
||||
const [selectedSchemaId, setSelectedSchemaId] = useState<string>(
|
||||
table?.schema
|
||||
? schemaNameToSchemaId(table.schema)
|
||||
@@ -112,18 +114,14 @@ export const TableSchemaDialog: React.FC<TableSchemaDialogProps> = ({
|
||||
onConfirm({ schema });
|
||||
}
|
||||
|
||||
filterSchemas([
|
||||
...(filteredSchemas ?? schemas.map((s) => s.id)),
|
||||
createdSchemaId,
|
||||
]);
|
||||
addSchemaIfFiltered(createdSchemaId);
|
||||
}, [
|
||||
onConfirm,
|
||||
selectedSchemaId,
|
||||
schemas,
|
||||
isCreatingNew,
|
||||
newSchemaName,
|
||||
filteredSchemas,
|
||||
filterSchemas,
|
||||
addSchemaIfFiltered,
|
||||
]);
|
||||
|
||||
const schemaOptions: SelectBoxOption[] = useMemo(
|
||||
|
||||
@@ -155,3 +155,29 @@
|
||||
background-size: 650%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Edit button emphasis animation */
|
||||
@keyframes dbml_edit-button-emphasis {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7);
|
||||
background-color: rgba(59, 130, 246, 0);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
|
||||
background-color: rgba(59, 130, 246, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.dbml-edit-button-emphasis {
|
||||
animation: dbml_edit-button-emphasis 0.6s ease-in-out;
|
||||
animation-iteration-count: 1;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@@ -72,15 +72,6 @@ export const ar: LanguageTranslation = {
|
||||
cancel: 'إلغاء',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'مخططات متعددة',
|
||||
description:
|
||||
'{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'لا شيء',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'فشل النسخ',
|
||||
@@ -115,10 +106,6 @@ export const ar: LanguageTranslation = {
|
||||
copied: '!تم النسخ',
|
||||
|
||||
side_panel: {
|
||||
schema: ':المخطط',
|
||||
filter_by_schema: 'تصفية حسب المخطط',
|
||||
search_schema: '...بحث في المخطط',
|
||||
no_schemas_found: '.لم يتم العثور على مخططات',
|
||||
view_all_options: '...عرض جميع الخيارات',
|
||||
tables_section: {
|
||||
tables: 'الجداول',
|
||||
|
||||
@@ -73,15 +73,6 @@ export const bn: LanguageTranslation = {
|
||||
cancel: 'বাতিল করুন',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'বহু স্কিমা',
|
||||
description:
|
||||
'{{schemasCount}} স্কিমা এই ডায়াগ্রামে রয়েছে। বর্তমানে প্রদর্শিত: {{formattedSchemas}}।',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'কিছুই না',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'কপি ব্যর্থ হয়েছে',
|
||||
@@ -116,10 +107,6 @@ export const bn: LanguageTranslation = {
|
||||
copied: 'অনুলিপি সম্পন্ন!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'স্কিমা:',
|
||||
filter_by_schema: 'স্কিমা দ্বারা ফিল্টার করুন',
|
||||
search_schema: 'স্কিমা খুঁজুন...',
|
||||
no_schemas_found: 'কোনো স্কিমা পাওয়া যায়নি।',
|
||||
view_all_options: 'সমস্ত বিকল্প দেখুন...',
|
||||
tables_section: {
|
||||
tables: 'টেবিল',
|
||||
|
||||
@@ -73,15 +73,6 @@ export const de: LanguageTranslation = {
|
||||
cancel: 'Abbrechen',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'Mehrere Schemas',
|
||||
description:
|
||||
'{{schemasCount}} Schemas in diesem Diagramm. Derzeit angezeigt: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'Keine',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'Kopieren fehlgeschlagen',
|
||||
@@ -117,10 +108,6 @@ export const de: LanguageTranslation = {
|
||||
copied: 'Kopiert!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'Schema:',
|
||||
filter_by_schema: 'Nach Schema filtern',
|
||||
search_schema: 'Schema suchen...',
|
||||
no_schemas_found: 'Keine Schemas gefunden.',
|
||||
view_all_options: 'Alle Optionen anzeigen...',
|
||||
tables_section: {
|
||||
tables: 'Tabellen',
|
||||
|
||||
@@ -71,14 +71,6 @@ export const en = {
|
||||
cancel: 'Cancel',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'Multiple Schemas',
|
||||
description:
|
||||
'{{schemasCount}} schemas in this diagram. Currently displaying: {{formattedSchemas}}.',
|
||||
show_me: 'Show me',
|
||||
none: 'none',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'Copy failed',
|
||||
@@ -113,10 +105,6 @@ export const en = {
|
||||
copied: 'Copied!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'Schema:',
|
||||
filter_by_schema: 'Filter by schema',
|
||||
search_schema: 'Search schema...',
|
||||
no_schemas_found: 'No schemas found.',
|
||||
view_all_options: 'View all Options...',
|
||||
tables_section: {
|
||||
tables: 'Tables',
|
||||
|
||||
@@ -106,10 +106,6 @@ export const es: LanguageTranslation = {
|
||||
copied: 'Copied!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'Esquema:',
|
||||
filter_by_schema: 'Filtrar por esquema',
|
||||
search_schema: 'Buscar esquema...',
|
||||
no_schemas_found: 'No se encontraron esquemas.',
|
||||
view_all_options: 'Ver todas las opciones...',
|
||||
tables_section: {
|
||||
tables: 'Tablas',
|
||||
@@ -424,14 +420,6 @@ export const es: LanguageTranslation = {
|
||||
confirm: '¡Claro!',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'Múltiples Esquemas',
|
||||
description:
|
||||
'{{schemasCount}} esquemas en este diagrama. Actualmente mostrando: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'nada',
|
||||
},
|
||||
// TODO: Translate
|
||||
export_diagram_dialog: {
|
||||
title: 'Export Diagram',
|
||||
|
||||
@@ -105,10 +105,6 @@ export const fr: LanguageTranslation = {
|
||||
copied: 'Copié !',
|
||||
|
||||
side_panel: {
|
||||
schema: 'Schéma:',
|
||||
filter_by_schema: 'Filtrer par schéma',
|
||||
search_schema: 'Rechercher un schéma...',
|
||||
no_schemas_found: 'Aucun schéma trouvé.',
|
||||
view_all_options: 'Voir toutes les Options...',
|
||||
tables_section: {
|
||||
tables: 'Tables',
|
||||
@@ -357,15 +353,6 @@ export const fr: LanguageTranslation = {
|
||||
transparent_description: 'Remove background color from image.',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'Schémas Multiples',
|
||||
description:
|
||||
'{{schemasCount}} schémas dans ce diagramme. Actuellement affiché(s) : {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'Aucun',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
title: 'Sélectionner un Schéma',
|
||||
description:
|
||||
|
||||
@@ -73,15 +73,6 @@ export const gu: LanguageTranslation = {
|
||||
cancel: 'રદ કરો',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'કઈંક વધારે સ્કીમા',
|
||||
description:
|
||||
'{{schemasCount}} સ્કીમા આ ડાયાગ્રામમાં છે. હાલમાં દર્શાવેલ છે: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'કઈ નહીં',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'નકલ નિષ્ફળ',
|
||||
@@ -116,10 +107,6 @@ export const gu: LanguageTranslation = {
|
||||
copied: 'નકલ થયું!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'સ્કીમા:',
|
||||
filter_by_schema: 'સ્કીમા દ્વારા ફિલ્ટર કરો',
|
||||
search_schema: 'સ્કીમા શોધો...',
|
||||
no_schemas_found: 'કોઈ સ્કીમા મળ્યા નથી.',
|
||||
view_all_options: 'બધા વિકલ્પો જુઓ...',
|
||||
tables_section: {
|
||||
tables: 'ટેબલ્સ',
|
||||
|
||||
@@ -72,15 +72,6 @@ export const hi: LanguageTranslation = {
|
||||
cancel: 'रद्द करें',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'एकाधिक स्कीमा',
|
||||
description:
|
||||
'{{schemasCount}} स्कीमा इस आरेख में हैं। वर्तमान में प्रदर्शित: {{formattedSchemas}}।',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'कोई नहीं',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'कॉपी असफल',
|
||||
@@ -116,10 +107,6 @@ export const hi: LanguageTranslation = {
|
||||
copied: 'Copied!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'स्कीमा:',
|
||||
filter_by_schema: 'स्कीमा द्वारा फ़िल्टर करें',
|
||||
search_schema: 'स्कीमा खोजें...',
|
||||
no_schemas_found: 'कोई स्कीमा नहीं मिला।',
|
||||
view_all_options: 'सभी विकल्प देखें...',
|
||||
tables_section: {
|
||||
tables: 'तालिकाएँ',
|
||||
|
||||
@@ -71,14 +71,6 @@ export const hr: LanguageTranslation = {
|
||||
cancel: 'Odustani',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'Više shema',
|
||||
description:
|
||||
'{{schemasCount}} shema u ovom dijagramu. Trenutno prikazano: {{formattedSchemas}}.',
|
||||
show_me: 'Prikaži mi',
|
||||
none: 'nijedna',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'Kopiranje neuspješno',
|
||||
@@ -113,10 +105,6 @@ export const hr: LanguageTranslation = {
|
||||
copied: 'Kopirano!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'Shema:',
|
||||
filter_by_schema: 'Filtriraj po shemi',
|
||||
search_schema: 'Pretraži shemu...',
|
||||
no_schemas_found: 'Nema pronađenih shema.',
|
||||
view_all_options: 'Prikaži sve opcije...',
|
||||
tables_section: {
|
||||
tables: 'Tablice',
|
||||
|
||||
@@ -72,15 +72,6 @@ export const id_ID: LanguageTranslation = {
|
||||
cancel: 'Batal',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'Schema Lebih dari satu',
|
||||
description:
|
||||
'{{schemasCount}} schema di diagram ini. Sedang ditampilkan: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'Tidak ada',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'Gagal menyalin',
|
||||
@@ -115,10 +106,6 @@ export const id_ID: LanguageTranslation = {
|
||||
copied: 'Tersalin!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'Skema:',
|
||||
filter_by_schema: 'Saring berdasarkan skema',
|
||||
search_schema: 'Cari skema...',
|
||||
no_schemas_found: 'Tidak ada skema yang ditemukan.',
|
||||
view_all_options: 'Tampilkan Semua Pilihan...',
|
||||
tables_section: {
|
||||
tables: 'Tabel',
|
||||
|
||||
@@ -74,15 +74,6 @@ export const ja: LanguageTranslation = {
|
||||
cancel: 'キャンセル',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: '複数のスキーマ',
|
||||
description:
|
||||
'このダイアグラムには{{schemasCount}}個のスキーマがあります。現在表示中: {{formattedSchemas}}。',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'なし',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'コピー失敗',
|
||||
@@ -119,10 +110,6 @@ export const ja: LanguageTranslation = {
|
||||
copied: 'Copied!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'スキーマ:',
|
||||
filter_by_schema: 'スキーマでフィルタ',
|
||||
search_schema: 'スキーマを検索...',
|
||||
no_schemas_found: 'スキーマが見つかりません。',
|
||||
view_all_options: 'すべてのオプションを表示...',
|
||||
tables_section: {
|
||||
tables: 'テーブル',
|
||||
|
||||
@@ -72,15 +72,6 @@ export const ko_KR: LanguageTranslation = {
|
||||
cancel: '취소',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: '다중 스키마',
|
||||
description:
|
||||
'현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: '없음',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: '복사 실패',
|
||||
@@ -115,10 +106,6 @@ export const ko_KR: LanguageTranslation = {
|
||||
copied: '복사됨!',
|
||||
|
||||
side_panel: {
|
||||
schema: '스키마:',
|
||||
filter_by_schema: '스키마로 필터링',
|
||||
search_schema: '스키마 검색...',
|
||||
no_schemas_found: '스키마를 찾을 수 없습니다.',
|
||||
view_all_options: '전체 옵션 보기...',
|
||||
tables_section: {
|
||||
tables: '테이블',
|
||||
|
||||
@@ -73,15 +73,6 @@ export const mr: LanguageTranslation = {
|
||||
cancel: 'रद्द करा',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'एकाधिक स्कीमा',
|
||||
description:
|
||||
'{{schemasCount}} स्कीमा या आरेखात आहेत. सध्या दाखवत आहोत: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'काहीही नाही',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'कॉपी अयशस्वी',
|
||||
@@ -118,10 +109,6 @@ export const mr: LanguageTranslation = {
|
||||
copied: 'Copied!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'स्कीमा:',
|
||||
filter_by_schema: 'स्कीमा द्वारे फिल्टर करा',
|
||||
search_schema: 'स्कीमा शोधा...',
|
||||
no_schemas_found: 'कोणतेही स्कीमा सापडले नाहीत.',
|
||||
view_all_options: 'सर्व पर्याय पहा...',
|
||||
tables_section: {
|
||||
tables: 'टेबल्स',
|
||||
|
||||
@@ -73,15 +73,6 @@ export const ne: LanguageTranslation = {
|
||||
cancel: 'रद्द गर्नुहोस्',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'विविध स्कीमहरू',
|
||||
description:
|
||||
'{{schemasCount}} डायाग्राममा स्कीमहरू। हालको रूपमा देखाइएको छ: {{formattedSchemas}}।',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'कुनै पनि छैन',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'प्रतिलिपि असफल',
|
||||
@@ -116,10 +107,6 @@ export const ne: LanguageTranslation = {
|
||||
copied: 'प्रतिलिपि गरियो!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'स्कीम:',
|
||||
filter_by_schema: 'स्कीम अनुसार फिल्टर गर्नुहोस्',
|
||||
search_schema: 'स्कीम खोज्नुहोस्...',
|
||||
no_schemas_found: 'कुनै स्कीमहरू फेला परेनन्',
|
||||
view_all_options: 'सबै विकल्पहरू हेर्नुहोस्',
|
||||
tables_section: {
|
||||
tables: 'तालिकाहरू',
|
||||
|
||||
@@ -73,15 +73,6 @@ export const pt_BR: LanguageTranslation = {
|
||||
cancel: 'Cancelar',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'Múltiplos Esquemas',
|
||||
description:
|
||||
'{{schemasCount}} esquemas neste diagrama. Atualmente exibindo: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'nenhum',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'Falha na cópia',
|
||||
@@ -116,10 +107,6 @@ export const pt_BR: LanguageTranslation = {
|
||||
copied: 'Copiado!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'Esquema:',
|
||||
filter_by_schema: 'Filtrar por esquema',
|
||||
search_schema: 'Buscar esquema...',
|
||||
no_schemas_found: 'Nenhum esquema encontrado.',
|
||||
view_all_options: 'Ver todas as Opções...',
|
||||
tables_section: {
|
||||
tables: 'Tabelas',
|
||||
|
||||
@@ -71,15 +71,6 @@ export const ru: LanguageTranslation = {
|
||||
cancel: 'Отменить',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'Множественные схемы',
|
||||
description:
|
||||
'{{schemasCount}} схем в этой диаграмме. В данный момент отображается: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'никто',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'Ошибка копирования',
|
||||
@@ -113,10 +104,6 @@ export const ru: LanguageTranslation = {
|
||||
show_less: 'Показать меньше',
|
||||
|
||||
side_panel: {
|
||||
schema: 'Схема:',
|
||||
filter_by_schema: 'Фильтр по схеме',
|
||||
search_schema: 'Схема поиска...',
|
||||
no_schemas_found: 'Схемы не найдены.',
|
||||
view_all_options: 'Просмотреть все варианты...',
|
||||
tables_section: {
|
||||
tables: 'Таблицы',
|
||||
|
||||
@@ -73,15 +73,6 @@ export const te: LanguageTranslation = {
|
||||
cancel: 'రద్దు',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'బహుళ స్కీమాలు',
|
||||
description:
|
||||
'{{schemasCount}} స్కీమాలు ఈ చిత్రంలో ఉన్నాయి. ప్రస్తుత స్కీమాలు: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'ఎదరికాదు',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'కాపీ విఫలమైంది',
|
||||
@@ -116,10 +107,6 @@ export const te: LanguageTranslation = {
|
||||
copied: 'కాపీ చేయబడింది!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'స్కీమా:',
|
||||
filter_by_schema: 'స్కీమా ద్వారా ఫిల్టర్ చేయండి',
|
||||
search_schema: 'స్కీమా కోసం శోధించండి...',
|
||||
no_schemas_found: 'ఏ స్కీమాలు కూడా కనుగొనబడలేదు.',
|
||||
view_all_options: 'అన్ని ఎంపికలను చూడండి...',
|
||||
tables_section: {
|
||||
tables: 'పట్టికలు',
|
||||
|
||||
@@ -73,15 +73,6 @@ export const tr: LanguageTranslation = {
|
||||
cancel: 'İptal',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'Birden Fazla Şema',
|
||||
description:
|
||||
'Bu diyagramda {{schemasCount}} şema var. Şu anda görüntülenen: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'yok',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'Kopyalama başarısız',
|
||||
@@ -115,10 +106,6 @@ export const tr: LanguageTranslation = {
|
||||
copy_to_clipboard: 'Panoya Kopyala',
|
||||
copied: 'Kopyalandı!',
|
||||
side_panel: {
|
||||
schema: 'Şema:',
|
||||
filter_by_schema: 'Şemaya Göre Filtrele',
|
||||
search_schema: 'Şema ara...',
|
||||
no_schemas_found: 'Şema bulunamadı.',
|
||||
view_all_options: 'Tüm Seçenekleri Gör...',
|
||||
tables_section: {
|
||||
tables: 'Tablolar',
|
||||
|
||||
@@ -71,15 +71,6 @@ export const uk: LanguageTranslation = {
|
||||
cancel: 'Скасувати',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'Кілька схем',
|
||||
description:
|
||||
'{{schemasCount}} схеми на цій діаграмі. Зараз відображається: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'немає',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'Помилка копіювання',
|
||||
@@ -114,10 +105,6 @@ export const uk: LanguageTranslation = {
|
||||
copied: 'Скопійовано!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'Схема:',
|
||||
filter_by_schema: 'Фільтрувати за схемою',
|
||||
search_schema: 'Пошук схеми…',
|
||||
no_schemas_found: 'Схеми не знайдено.',
|
||||
view_all_options: 'Переглянути всі параметри…',
|
||||
tables_section: {
|
||||
tables: 'Таблиці',
|
||||
|
||||
@@ -72,15 +72,6 @@ export const vi: LanguageTranslation = {
|
||||
cancel: 'Hủy',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'Có nhiều lược đồ',
|
||||
description:
|
||||
'Có {{schemasCount}} lược đồ trong sơ đồ này. Hiện đang hiển thị: {{formattedSchemas}}.',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: 'không có',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'Sao chép thất bại',
|
||||
@@ -115,10 +106,6 @@ export const vi: LanguageTranslation = {
|
||||
copied: 'Đã sao chép!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'Lược đồ:',
|
||||
filter_by_schema: 'Lọc bởi lược đồ',
|
||||
search_schema: 'Tìm kiếm lược đồ...',
|
||||
no_schemas_found: 'Không tìm thấy lược đồ.',
|
||||
view_all_options: 'Xem tất cả tùy chọn...',
|
||||
tables_section: {
|
||||
tables: 'Bảng',
|
||||
|
||||
@@ -69,15 +69,6 @@ export const zh_CN: LanguageTranslation = {
|
||||
cancel: '取消',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: '多个模式',
|
||||
description:
|
||||
'此关系图中有 {{schemasCount}} 个模式,当前显示:{{formattedSchemas}}。',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: '无',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: '复制失败',
|
||||
@@ -112,10 +103,6 @@ export const zh_CN: LanguageTranslation = {
|
||||
copied: '复制了!',
|
||||
|
||||
side_panel: {
|
||||
schema: '模式:',
|
||||
filter_by_schema: '按模式筛选',
|
||||
search_schema: '搜索模式...',
|
||||
no_schemas_found: '未找到模式。',
|
||||
view_all_options: '查看所有选项...',
|
||||
tables_section: {
|
||||
tables: '表',
|
||||
|
||||
@@ -69,15 +69,6 @@ export const zh_TW: LanguageTranslation = {
|
||||
cancel: '取消',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: '多重 Schema',
|
||||
description:
|
||||
'此圖表中包含 {{schemasCount}} 個 Schema,目前顯示:{{formattedSchemas}}。',
|
||||
// TODO: Translate
|
||||
show_me: 'Show me',
|
||||
none: '無',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: '複製失敗',
|
||||
@@ -112,10 +103,6 @@ export const zh_TW: LanguageTranslation = {
|
||||
copied: '已複製!',
|
||||
|
||||
side_panel: {
|
||||
schema: 'Schema:',
|
||||
filter_by_schema: '依 Schema 篩選',
|
||||
search_schema: '搜尋 Schema...',
|
||||
no_schemas_found: '未找到 Schema。',
|
||||
view_all_options: '顯示所有選項...',
|
||||
tables_section: {
|
||||
tables: '表格',
|
||||
|
||||
@@ -50,5 +50,8 @@ export const sqliteDataTypes: readonly DataTypeData[] = [
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
{ name: 'bigint', id: 'bigint' },
|
||||
{ name: 'bool', id: 'bool' },
|
||||
{ name: 'boolean', id: 'boolean' }, // Added for smartquery compatibility
|
||||
{ name: 'time', id: 'time' },
|
||||
{ name: 'date', id: 'date' }, // Added for smartquery compatibility
|
||||
{ name: 'datetime', id: 'datetime' }, // Added for smartquery compatibility
|
||||
] as const;
|
||||
|
||||
@@ -227,7 +227,7 @@ describe('DBML Export - SQL Generation Tests', () => {
|
||||
expect(sql).not.toContain('DEFAULT DEFAULT has default');
|
||||
// The fields should still be in the table
|
||||
expect(sql).toContain('is_active boolean');
|
||||
expect(sql).toContain('stock_count int NOT NULL'); // integer gets simplified to int
|
||||
expect(sql).toContain('stock_count integer NOT NULL'); // integer gets simplified to int
|
||||
});
|
||||
|
||||
it('should handle valid default values correctly', () => {
|
||||
|
||||
@@ -67,8 +67,9 @@ function parseSQLiteDefault(field: DBField): string {
|
||||
return `'${defaultValue.replace(/'/g, "''")}'`;
|
||||
}
|
||||
|
||||
// Map problematic types to SQLite compatible types
|
||||
// Preserve original types for SQLite export (only map when necessary)
|
||||
function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
|
||||
const originalType = typeName;
|
||||
typeName = typeName.toLowerCase();
|
||||
|
||||
// Special handling for primary key integer columns (autoincrement requires INTEGER PRIMARY KEY)
|
||||
@@ -76,59 +77,62 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
|
||||
return 'INTEGER'; // Must be uppercase for SQLite to recognize it for AUTOINCREMENT
|
||||
}
|
||||
|
||||
// Map common types to SQLite's simplified type system
|
||||
// Preserve original type names that SQLite accepts
|
||||
switch (typeName) {
|
||||
// Keep these types as-is
|
||||
case 'integer':
|
||||
case 'text':
|
||||
case 'real':
|
||||
case 'blob':
|
||||
case 'numeric':
|
||||
case 'decimal':
|
||||
case 'boolean':
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
case 'timestamp':
|
||||
case 'float':
|
||||
case 'double':
|
||||
case 'varchar':
|
||||
case 'char':
|
||||
case 'int':
|
||||
case 'smallint':
|
||||
case 'tinyint':
|
||||
case 'mediumint':
|
||||
case 'bigint':
|
||||
return 'INTEGER';
|
||||
case 'json':
|
||||
return typeName.toUpperCase();
|
||||
|
||||
case 'decimal':
|
||||
case 'numeric':
|
||||
case 'float':
|
||||
case 'double':
|
||||
case 'real':
|
||||
return 'REAL';
|
||||
|
||||
case 'char':
|
||||
// Only map types that SQLite truly doesn't recognize
|
||||
case 'nchar':
|
||||
case 'varchar':
|
||||
case 'nvarchar':
|
||||
case 'text':
|
||||
case 'ntext':
|
||||
case 'character varying':
|
||||
case 'character':
|
||||
return 'TEXT';
|
||||
|
||||
case 'date':
|
||||
case 'datetime':
|
||||
case 'timestamp':
|
||||
case 'datetime2':
|
||||
return 'TEXT'; // SQLite doesn't have dedicated date types
|
||||
return 'DATETIME';
|
||||
|
||||
case 'blob':
|
||||
case 'binary':
|
||||
case 'varbinary':
|
||||
case 'image':
|
||||
return 'BLOB';
|
||||
|
||||
case 'bit':
|
||||
case 'boolean':
|
||||
return 'INTEGER'; // SQLite doesn't have a boolean type, use INTEGER
|
||||
return 'BOOLEAN';
|
||||
|
||||
case 'user-defined':
|
||||
case 'json':
|
||||
case 'jsonb':
|
||||
return 'TEXT'; // Store as JSON text
|
||||
return 'TEXT';
|
||||
|
||||
case 'array':
|
||||
return 'TEXT'; // Store as serialized array text
|
||||
return 'TEXT';
|
||||
|
||||
case 'geometry':
|
||||
case 'geography':
|
||||
return 'BLOB'; // Store spatial data as BLOB in SQLite
|
||||
return 'BLOB';
|
||||
|
||||
case 'mediumint':
|
||||
return 'INTEGER';
|
||||
}
|
||||
|
||||
// If type has array notation (ends with []), treat as TEXT
|
||||
@@ -136,8 +140,8 @@ function mapSQLiteType(typeName: string, isPrimaryKey: boolean): string {
|
||||
return 'TEXT';
|
||||
}
|
||||
|
||||
// For any other types, default to TEXT
|
||||
return typeName;
|
||||
// For any other types, preserve the original
|
||||
return originalType.toUpperCase();
|
||||
}
|
||||
|
||||
export function exportSQLite({
|
||||
@@ -157,6 +161,11 @@ export function exportSQLite({
|
||||
// Start SQL script - SQLite doesn't use schemas, so we skip schema creation
|
||||
let sqlScript = '-- SQLite database export\n';
|
||||
|
||||
// Add PRAGMA foreign_keys = ON if there are relationships
|
||||
if (relationships && relationships.length > 0) {
|
||||
sqlScript += 'PRAGMA foreign_keys = ON;\n\n';
|
||||
}
|
||||
|
||||
// Begin transaction for faster import
|
||||
sqlScript += 'BEGIN TRANSACTION;\n';
|
||||
|
||||
@@ -205,6 +214,86 @@ export function exportSQLite({
|
||||
'integer' ||
|
||||
primaryKeyFields[0].type.name.toLowerCase() === 'int');
|
||||
|
||||
// Collect foreign key constraints for this table
|
||||
const tableForeignKeys: string[] = [];
|
||||
relationships.forEach((r: DBRelationship) => {
|
||||
const sourceTable = tables.find(
|
||||
(t) => t.id === r.sourceTableId
|
||||
);
|
||||
const targetTable = tables.find(
|
||||
(t) => t.id === r.targetTableId
|
||||
);
|
||||
|
||||
if (
|
||||
!sourceTable ||
|
||||
!targetTable ||
|
||||
sourceTable.isView ||
|
||||
targetTable.isView ||
|
||||
sqliteSystemTables.includes(
|
||||
sourceTable.name.toLowerCase()
|
||||
) ||
|
||||
sqliteSystemTables.includes(
|
||||
targetTable.name.toLowerCase()
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceField = sourceTable.fields.find(
|
||||
(f) => f.id === r.sourceFieldId
|
||||
);
|
||||
const targetField = targetTable.fields.find(
|
||||
(f) => f.id === r.targetFieldId
|
||||
);
|
||||
|
||||
if (!sourceField || !targetField) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine which table should have the foreign key based on cardinality
|
||||
let fkTable, fkField, refTable, refField;
|
||||
|
||||
if (
|
||||
r.sourceCardinality === 'one' &&
|
||||
r.targetCardinality === 'many'
|
||||
) {
|
||||
// FK goes on target table
|
||||
fkTable = targetTable;
|
||||
fkField = targetField;
|
||||
refTable = sourceTable;
|
||||
refField = sourceField;
|
||||
} else if (
|
||||
r.sourceCardinality === 'many' &&
|
||||
r.targetCardinality === 'one'
|
||||
) {
|
||||
// FK goes on source table
|
||||
fkTable = sourceTable;
|
||||
fkField = sourceField;
|
||||
refTable = targetTable;
|
||||
refField = targetField;
|
||||
} else if (
|
||||
r.sourceCardinality === 'one' &&
|
||||
r.targetCardinality === 'one'
|
||||
) {
|
||||
// For 1:1, FK can go on either side, but typically goes on the table that references the other
|
||||
// We'll keep the current behavior for 1:1
|
||||
fkTable = sourceTable;
|
||||
fkField = sourceField;
|
||||
refTable = targetTable;
|
||||
refField = targetField;
|
||||
} else {
|
||||
// Many-to-many relationships need a junction table, skip for now
|
||||
return;
|
||||
}
|
||||
|
||||
// If this foreign key belongs to the current table, add it
|
||||
if (fkTable.id === table.id) {
|
||||
tableForeignKeys.push(
|
||||
` FOREIGN KEY("${fkField.name}") REFERENCES "${refTable.name}"("${refField.name}")`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return `${schemaComment}${
|
||||
table.comments ? formatTableComment(table.comments) : ''
|
||||
}CREATE TABLE IF NOT EXISTS ${tableName} (\n${table.fields
|
||||
@@ -212,14 +301,40 @@ export function exportSQLite({
|
||||
const fieldName = `"${field.name}"`;
|
||||
|
||||
// Handle type name - map to SQLite compatible types
|
||||
const typeName = mapSQLiteType(
|
||||
const baseTypeName = mapSQLiteType(
|
||||
field.type.name,
|
||||
field.primaryKey
|
||||
);
|
||||
|
||||
// SQLite ignores length specifiers, so we don't add them
|
||||
// We'll keep this simple without size info
|
||||
const typeWithoutSize = typeName;
|
||||
// Add size/precision/scale parameters if applicable
|
||||
let typeWithParams = baseTypeName;
|
||||
|
||||
// Add character maximum length for VARCHAR, CHAR, etc.
|
||||
if (
|
||||
field.characterMaximumLength &&
|
||||
['VARCHAR', 'CHAR', 'TEXT'].includes(
|
||||
baseTypeName.toUpperCase()
|
||||
)
|
||||
) {
|
||||
typeWithParams = `${baseTypeName}(${field.characterMaximumLength})`;
|
||||
}
|
||||
// Add precision and scale for DECIMAL, NUMERIC, etc.
|
||||
else if (
|
||||
field.precision &&
|
||||
[
|
||||
'DECIMAL',
|
||||
'NUMERIC',
|
||||
'REAL',
|
||||
'FLOAT',
|
||||
'DOUBLE',
|
||||
].includes(baseTypeName.toUpperCase())
|
||||
) {
|
||||
if (field.scale) {
|
||||
typeWithParams = `${baseTypeName}(${field.precision}, ${field.scale})`;
|
||||
} else {
|
||||
typeWithParams = `${baseTypeName}(${field.precision})`;
|
||||
}
|
||||
}
|
||||
|
||||
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||
|
||||
@@ -267,7 +382,7 @@ export function exportSQLite({
|
||||
? ' PRIMARY KEY' + autoIncrement
|
||||
: '';
|
||||
|
||||
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithoutSize}${primaryKey}${notNull}${unique}${defaultValue}`;
|
||||
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithParams}${primaryKey}${notNull}${unique}${defaultValue}`;
|
||||
})
|
||||
.join(',\n')}${
|
||||
// Add PRIMARY KEY as table constraint for composite primary keys or non-INTEGER primary keys
|
||||
@@ -276,6 +391,11 @@ export function exportSQLite({
|
||||
.map((f) => `"${f.name}"`)
|
||||
.join(', ')})`
|
||||
: ''
|
||||
}${
|
||||
// Add foreign key constraints
|
||||
tableForeignKeys.length > 0
|
||||
? ',\n' + tableForeignKeys.join(',\n')
|
||||
: ''
|
||||
}\n);\n${
|
||||
// Add indexes - SQLite doesn't support indexes in CREATE TABLE
|
||||
(() => {
|
||||
@@ -333,82 +453,8 @@ export function exportSQLite({
|
||||
.filter(Boolean) // Remove empty strings (views)
|
||||
.join('\n');
|
||||
}
|
||||
// Generate table constraints and triggers for foreign keys
|
||||
// SQLite handles foreign keys differently - we'll add them with CREATE TABLE statements
|
||||
// But we'll also provide individual ALTER TABLE statements as comments for reference
|
||||
|
||||
if (relationships.length > 0) {
|
||||
sqlScript += '\n-- Foreign key constraints\n';
|
||||
sqlScript +=
|
||||
'-- Note: SQLite requires foreign_keys pragma to be enabled:\n';
|
||||
sqlScript += '-- PRAGMA foreign_keys = ON;\n';
|
||||
|
||||
relationships.forEach((r: DBRelationship) => {
|
||||
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
|
||||
const targetTable = tables.find((t) => t.id === r.targetTableId);
|
||||
|
||||
if (
|
||||
!sourceTable ||
|
||||
!targetTable ||
|
||||
sourceTable.isView ||
|
||||
targetTable.isView ||
|
||||
sqliteSystemTables.includes(sourceTable.name.toLowerCase()) ||
|
||||
sqliteSystemTables.includes(targetTable.name.toLowerCase())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceField = sourceTable.fields.find(
|
||||
(f) => f.id === r.sourceFieldId
|
||||
);
|
||||
const targetField = targetTable.fields.find(
|
||||
(f) => f.id === r.targetFieldId
|
||||
);
|
||||
|
||||
if (!sourceField || !targetField) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine which table should have the foreign key based on cardinality
|
||||
let fkTable, fkField, refTable, refField;
|
||||
|
||||
if (
|
||||
r.sourceCardinality === 'one' &&
|
||||
r.targetCardinality === 'many'
|
||||
) {
|
||||
// FK goes on target table
|
||||
fkTable = targetTable;
|
||||
fkField = targetField;
|
||||
refTable = sourceTable;
|
||||
refField = sourceField;
|
||||
} else if (
|
||||
r.sourceCardinality === 'many' &&
|
||||
r.targetCardinality === 'one'
|
||||
) {
|
||||
// FK goes on source table
|
||||
fkTable = sourceTable;
|
||||
fkField = sourceField;
|
||||
refTable = targetTable;
|
||||
refField = targetField;
|
||||
} else if (
|
||||
r.sourceCardinality === 'one' &&
|
||||
r.targetCardinality === 'one'
|
||||
) {
|
||||
// For 1:1, FK can go on either side, but typically goes on the table that references the other
|
||||
// We'll keep the current behavior for 1:1
|
||||
fkTable = sourceTable;
|
||||
fkField = sourceField;
|
||||
refTable = targetTable;
|
||||
refField = targetField;
|
||||
} else {
|
||||
// Many-to-many relationships need a junction table, skip for now
|
||||
return;
|
||||
}
|
||||
|
||||
// Create commented out version of what would be ALTER TABLE statement
|
||||
sqlScript += `-- ALTER TABLE "${fkTable.name}" ADD CONSTRAINT "fk_${fkTable.name}_${fkField.name}" FOREIGN KEY("${fkField.name}") REFERENCES "${refTable.name}"("${refField.name}");\n`;
|
||||
});
|
||||
}
|
||||
// Foreign keys are now included inline in CREATE TABLE statements
|
||||
// No need for separate ALTER TABLE statements in SQLite
|
||||
|
||||
// Commit transaction
|
||||
sqlScript += '\nCOMMIT;\n';
|
||||
|
||||
@@ -11,23 +11,7 @@ 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',
|
||||
};
|
||||
const typeMap: Record<string, string> = {};
|
||||
|
||||
return typeMap[typeName.toLowerCase()] || typeName;
|
||||
};
|
||||
@@ -362,9 +346,10 @@ export const exportBaseSQL = ({
|
||||
.join(', ');
|
||||
|
||||
if (fieldNames) {
|
||||
const indexName = table.schema
|
||||
? `${table.schema}_${index.name}`
|
||||
: index.name;
|
||||
const indexName =
|
||||
table.schema && !isDBMLFlow
|
||||
? `${table.schema}_${index.name}`
|
||||
: index.name;
|
||||
sqlScript += `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName} ON ${tableName} (${fieldNames});\n`;
|
||||
}
|
||||
});
|
||||
|
||||
1251
src/lib/dbml/apply-dbml/__tests__/apply-dbml.test.ts
Normal file
1251
src/lib/dbml/apply-dbml/__tests__/apply-dbml.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
624
src/lib/dbml/apply-dbml/apply-dbml.ts
Normal file
624
src/lib/dbml/apply-dbml/apply-dbml.ts
Normal file
@@ -0,0 +1,624 @@
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
import type { Area } from '../../domain/area';
|
||||
import {
|
||||
DBCustomTypeKind,
|
||||
type DBCustomType,
|
||||
} from '../../domain/db-custom-type';
|
||||
import type { DBDependency } from '../../domain/db-dependency';
|
||||
import type { DBField } from '../../domain/db-field';
|
||||
import type { DBIndex } from '../../domain/db-index';
|
||||
import type { DBRelationship } from '../../domain/db-relationship';
|
||||
import type { DBTable } from '../../domain/db-table';
|
||||
import type { Diagram } from '../../domain/diagram';
|
||||
|
||||
type SourceIdToDataMap = Record<
|
||||
string,
|
||||
{ schema?: string | null; name: string; color?: string }
|
||||
>;
|
||||
|
||||
type IdMappings = {
|
||||
tables: Record<string, string>;
|
||||
fields: Record<string, string>;
|
||||
};
|
||||
|
||||
// Key generation functions remain the same for consistency
|
||||
const createObjectKey = ({
|
||||
type,
|
||||
schema,
|
||||
otherSchema,
|
||||
parentName,
|
||||
otherParentName,
|
||||
name,
|
||||
otherName,
|
||||
}: {
|
||||
type:
|
||||
| 'table'
|
||||
| 'field'
|
||||
| 'index'
|
||||
| 'relationship'
|
||||
| 'customType'
|
||||
| 'dependency'
|
||||
| 'area';
|
||||
schema?: string | null;
|
||||
otherSchema?: string | null;
|
||||
parentName?: string | null;
|
||||
otherParentName?: string | null;
|
||||
name: string;
|
||||
otherName?: string | null;
|
||||
}) =>
|
||||
`${type}-${schema ? `${schema}.` : ''}${otherSchema ? `${otherSchema}.` : ''}${parentName ? `${parentName}.` : ''}${otherParentName ? `${otherParentName}.` : ''}${name}${otherName ? `.${otherName}` : ''}`;
|
||||
|
||||
const createObjectKeyFromTable = (table: DBTable) =>
|
||||
createObjectKey({
|
||||
type: 'table',
|
||||
schema: table.schema,
|
||||
name: table.name,
|
||||
});
|
||||
|
||||
const createObjectKeyFromField = (table: DBTable, field: DBField) =>
|
||||
createObjectKey({
|
||||
type: 'field',
|
||||
schema: table.schema,
|
||||
parentName: table.name,
|
||||
name: field.name,
|
||||
});
|
||||
|
||||
const createObjectKeyFromIndex = (table: DBTable, index: DBIndex) =>
|
||||
createObjectKey({
|
||||
type: 'index',
|
||||
schema: table.schema,
|
||||
parentName: table.name,
|
||||
name: index.name,
|
||||
});
|
||||
|
||||
const createObjectKeyFromRelationship = (
|
||||
relationship: DBRelationship,
|
||||
sourceIdToNameMap: SourceIdToDataMap
|
||||
) => {
|
||||
const sourceTable = sourceIdToNameMap[relationship.sourceTableId];
|
||||
const targetTable = sourceIdToNameMap[relationship.targetTableId];
|
||||
const sourceField = sourceIdToNameMap[relationship.sourceFieldId];
|
||||
const targetField = sourceIdToNameMap[relationship.targetFieldId];
|
||||
|
||||
if (!sourceTable || !targetTable || !sourceField || !targetField) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createObjectKey({
|
||||
type: 'relationship',
|
||||
schema: sourceTable.schema,
|
||||
otherSchema: targetTable.schema,
|
||||
parentName: sourceTable.name,
|
||||
otherParentName: targetTable.name,
|
||||
name: sourceField.name,
|
||||
otherName: targetField.name,
|
||||
});
|
||||
};
|
||||
|
||||
const createObjectKeyFromCustomType = (customType: DBCustomType) =>
|
||||
createObjectKey({
|
||||
type: 'customType',
|
||||
schema: customType.schema,
|
||||
name: customType.name,
|
||||
});
|
||||
|
||||
const createObjectKeyFromDependency = (
|
||||
dependency: DBDependency,
|
||||
sourceIdToNameMap: SourceIdToDataMap
|
||||
) => {
|
||||
const dependentTable = sourceIdToNameMap[dependency.dependentTableId];
|
||||
const table = sourceIdToNameMap[dependency.tableId];
|
||||
|
||||
if (!dependentTable || !table) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createObjectKey({
|
||||
type: 'dependency',
|
||||
schema: dependentTable.schema,
|
||||
otherSchema: table.schema,
|
||||
name: dependentTable.name,
|
||||
otherName: table.name,
|
||||
});
|
||||
};
|
||||
|
||||
const createObjectKeyFromArea = (area: Area) =>
|
||||
createObjectKey({
|
||||
type: 'area',
|
||||
name: area.name,
|
||||
});
|
||||
|
||||
// Helper function to build source mappings
|
||||
const buildSourceMappings = (sourceDiagram: Diagram) => {
|
||||
const objectKeysToIdsMap: Record<string, string> = {};
|
||||
const sourceIdToDataMap: SourceIdToDataMap = {};
|
||||
|
||||
// Map tables and their fields/indexes
|
||||
sourceDiagram.tables?.forEach((table) => {
|
||||
const tableKey = createObjectKeyFromTable(table);
|
||||
objectKeysToIdsMap[tableKey] = table.id;
|
||||
sourceIdToDataMap[table.id] = {
|
||||
schema: table.schema,
|
||||
name: table.name,
|
||||
color: table.color,
|
||||
};
|
||||
|
||||
table.fields?.forEach((field) => {
|
||||
const fieldKey = createObjectKeyFromField(table, field);
|
||||
objectKeysToIdsMap[fieldKey] = field.id;
|
||||
sourceIdToDataMap[field.id] = {
|
||||
schema: table.schema,
|
||||
name: field.name,
|
||||
};
|
||||
});
|
||||
|
||||
table.indexes?.forEach((index) => {
|
||||
const indexKey = createObjectKeyFromIndex(table, index);
|
||||
objectKeysToIdsMap[indexKey] = index.id;
|
||||
});
|
||||
});
|
||||
|
||||
// Map relationships
|
||||
sourceDiagram.relationships?.forEach((relationship) => {
|
||||
const key = createObjectKeyFromRelationship(
|
||||
relationship,
|
||||
sourceIdToDataMap
|
||||
);
|
||||
if (key) {
|
||||
objectKeysToIdsMap[key] = relationship.id;
|
||||
}
|
||||
});
|
||||
|
||||
// Map custom types
|
||||
sourceDiagram.customTypes?.forEach((customType) => {
|
||||
const key = createObjectKeyFromCustomType(customType);
|
||||
objectKeysToIdsMap[key] = customType.id;
|
||||
});
|
||||
|
||||
// Map dependencies
|
||||
sourceDiagram.dependencies?.forEach((dependency) => {
|
||||
const key = createObjectKeyFromDependency(
|
||||
dependency,
|
||||
sourceIdToDataMap
|
||||
);
|
||||
if (key) {
|
||||
objectKeysToIdsMap[key] = dependency.id;
|
||||
}
|
||||
});
|
||||
|
||||
// Map areas
|
||||
sourceDiagram.areas?.forEach((area) => {
|
||||
const key = createObjectKeyFromArea(area);
|
||||
objectKeysToIdsMap[key] = area.id;
|
||||
});
|
||||
|
||||
return { objectKeysToIdsMap, sourceIdToDataMap };
|
||||
};
|
||||
|
||||
// Functional helper to update tables and collect ID mappings
|
||||
const updateTables = ({
|
||||
targetTables,
|
||||
sourceTables,
|
||||
defaultDatabaseSchema,
|
||||
}: {
|
||||
targetTables: DBTable[] | undefined;
|
||||
sourceTables: DBTable[] | undefined;
|
||||
objectKeysToIdsMap: Record<string, string>;
|
||||
sourceIdToDataMap: SourceIdToDataMap;
|
||||
defaultDatabaseSchema?: string;
|
||||
}): { tables: DBTable[]; idMappings: IdMappings } => {
|
||||
if (!targetTables)
|
||||
return { tables: [], idMappings: { tables: {}, fields: {} } };
|
||||
if (!sourceTables)
|
||||
return { tables: targetTables, idMappings: { tables: {}, fields: {} } };
|
||||
|
||||
const idMappings: IdMappings = { tables: {}, fields: {} };
|
||||
|
||||
// Create a map of source tables by schema + name
|
||||
const sourceTablesByKey = new Map<string, DBTable>();
|
||||
sourceTables.forEach((table) => {
|
||||
const key = createObjectKeyFromTable(table);
|
||||
sourceTablesByKey.set(key, table);
|
||||
});
|
||||
|
||||
const updatedTables = targetTables.map((targetTable) => {
|
||||
// Try to find matching source table by schema + name
|
||||
const targetKey = createObjectKeyFromTable(targetTable);
|
||||
let sourceTable = sourceTablesByKey.get(targetKey);
|
||||
|
||||
if (!sourceTable && defaultDatabaseSchema) {
|
||||
if (!targetTable.schema) {
|
||||
// If target table has no schema, try matching with default schema
|
||||
const defaultKey = createObjectKeyFromTable({
|
||||
...targetTable,
|
||||
schema: defaultDatabaseSchema,
|
||||
});
|
||||
sourceTable = sourceTablesByKey.get(defaultKey);
|
||||
} else if (targetTable.schema === defaultDatabaseSchema) {
|
||||
// If target table's schema matches default, try matching without schema
|
||||
const noSchemaKey = createObjectKeyFromTable({
|
||||
...targetTable,
|
||||
schema: undefined,
|
||||
});
|
||||
sourceTable = sourceTablesByKey.get(noSchemaKey);
|
||||
}
|
||||
}
|
||||
|
||||
if (!sourceTable) {
|
||||
// No matching source table found - keep target as-is
|
||||
return targetTable;
|
||||
}
|
||||
|
||||
const sourceId = sourceTable.id;
|
||||
idMappings.tables[targetTable.id] = sourceId;
|
||||
|
||||
// Update fields by matching on name within the table
|
||||
const sourceFieldsByName = new Map<string, DBField>();
|
||||
sourceTable.fields?.forEach((field) => {
|
||||
sourceFieldsByName.set(field.name, field);
|
||||
});
|
||||
|
||||
const updatedFields = targetTable.fields?.map((targetField) => {
|
||||
const sourceField = sourceFieldsByName.get(targetField.name);
|
||||
if (sourceField) {
|
||||
idMappings.fields[targetField.id] = sourceField.id;
|
||||
|
||||
// Use source field properties when there's a match
|
||||
return {
|
||||
...targetField,
|
||||
id: sourceField.id,
|
||||
createdAt: sourceField.createdAt,
|
||||
};
|
||||
}
|
||||
// For new fields not in source, keep target field as-is
|
||||
return targetField;
|
||||
});
|
||||
|
||||
// Update indexes by matching on name within the table
|
||||
const sourceIndexesByName = new Map<string, DBIndex>();
|
||||
sourceTable.indexes?.forEach((index) => {
|
||||
sourceIndexesByName.set(index.name, index);
|
||||
});
|
||||
|
||||
const updatedIndexes = targetTable.indexes?.map((targetIndex) => {
|
||||
const sourceIndex = sourceIndexesByName.get(targetIndex.name);
|
||||
if (sourceIndex) {
|
||||
return {
|
||||
...targetIndex,
|
||||
id: sourceIndex.id,
|
||||
createdAt: sourceIndex.createdAt,
|
||||
};
|
||||
}
|
||||
return targetIndex;
|
||||
});
|
||||
|
||||
// Build the result table, preserving source structure
|
||||
const resultTable: DBTable = {
|
||||
...sourceTable,
|
||||
fields: updatedFields,
|
||||
indexes: updatedIndexes,
|
||||
comments: targetTable.comments,
|
||||
};
|
||||
|
||||
// Update nullable, unique, primaryKey from target fields
|
||||
if (targetTable.fields) {
|
||||
resultTable.fields = resultTable.fields?.map((field) => {
|
||||
const targetField = targetTable.fields?.find(
|
||||
(f) => f.name === field.name
|
||||
);
|
||||
if (targetField) {
|
||||
return {
|
||||
...field,
|
||||
nullable: targetField.nullable,
|
||||
unique: targetField.unique,
|
||||
primaryKey: targetField.primaryKey,
|
||||
type: targetField.type,
|
||||
};
|
||||
}
|
||||
return field;
|
||||
});
|
||||
}
|
||||
|
||||
return resultTable;
|
||||
});
|
||||
|
||||
return { tables: updatedTables, idMappings };
|
||||
};
|
||||
|
||||
// Functional helper to update custom types
|
||||
const updateCustomTypes = (
|
||||
customTypes: DBCustomType[] | undefined,
|
||||
objectKeysToIdsMap: Record<string, string>
|
||||
): DBCustomType[] => {
|
||||
if (!customTypes) return [];
|
||||
|
||||
return customTypes.map((customType) => {
|
||||
const key = createObjectKeyFromCustomType(customType);
|
||||
const sourceId = objectKeysToIdsMap[key];
|
||||
|
||||
if (sourceId) {
|
||||
return { ...customType, id: sourceId };
|
||||
}
|
||||
return customType;
|
||||
});
|
||||
};
|
||||
|
||||
// Functional helper to update relationships
|
||||
const updateRelationships = (
|
||||
targetRelationships: DBRelationship[] | undefined,
|
||||
sourceRelationships: DBRelationship[] | undefined,
|
||||
idMappings: IdMappings
|
||||
): DBRelationship[] => {
|
||||
// If target has no relationships, return empty array (relationships were removed)
|
||||
if (!targetRelationships || targetRelationships.length === 0) return [];
|
||||
|
||||
// If source has no relationships, we need to add the target relationships with updated IDs
|
||||
if (!sourceRelationships || sourceRelationships.length === 0) {
|
||||
return targetRelationships.map((targetRel) => {
|
||||
// Find the source IDs by reversing the mapping lookup
|
||||
let sourceTableId = targetRel.sourceTableId;
|
||||
let targetTableId = targetRel.targetTableId;
|
||||
let sourceFieldId = targetRel.sourceFieldId;
|
||||
let targetFieldId = targetRel.targetFieldId;
|
||||
|
||||
// Find source table/field IDs from the mappings
|
||||
for (const [targetId, srcId] of Object.entries(idMappings.tables)) {
|
||||
if (targetId === targetRel.sourceTableId) {
|
||||
sourceTableId = srcId;
|
||||
}
|
||||
if (targetId === targetRel.targetTableId) {
|
||||
targetTableId = srcId;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [targetId, srcId] of Object.entries(idMappings.fields)) {
|
||||
if (targetId === targetRel.sourceFieldId) {
|
||||
sourceFieldId = srcId;
|
||||
}
|
||||
if (targetId === targetRel.targetFieldId) {
|
||||
targetFieldId = srcId;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...targetRel,
|
||||
sourceTableId,
|
||||
targetTableId,
|
||||
sourceFieldId,
|
||||
targetFieldId,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Map source relationships that have matches in target
|
||||
const resultRelationships: DBRelationship[] = [];
|
||||
const matchedTargetRelIds = new Set<string>();
|
||||
|
||||
sourceRelationships.forEach((sourceRel) => {
|
||||
// Find matching target relationship by checking if the target has a relationship
|
||||
// between the same tables and fields (using the ID mappings)
|
||||
const targetRel = targetRelationships.find((tgtRel) => {
|
||||
const mappedSourceTableId = idMappings.tables[tgtRel.sourceTableId];
|
||||
const mappedTargetTableId = idMappings.tables[tgtRel.targetTableId];
|
||||
const mappedSourceFieldId = idMappings.fields[tgtRel.sourceFieldId];
|
||||
const mappedTargetFieldId = idMappings.fields[tgtRel.targetFieldId];
|
||||
|
||||
// Check both directions since relationships can be defined in either direction
|
||||
const directMatch =
|
||||
sourceRel.sourceTableId === mappedSourceTableId &&
|
||||
sourceRel.targetTableId === mappedTargetTableId &&
|
||||
sourceRel.sourceFieldId === mappedSourceFieldId &&
|
||||
sourceRel.targetFieldId === mappedTargetFieldId;
|
||||
|
||||
const reverseMatch =
|
||||
sourceRel.sourceTableId === mappedTargetTableId &&
|
||||
sourceRel.targetTableId === mappedSourceTableId &&
|
||||
sourceRel.sourceFieldId === mappedTargetFieldId &&
|
||||
sourceRel.targetFieldId === mappedSourceFieldId;
|
||||
|
||||
return directMatch || reverseMatch;
|
||||
});
|
||||
|
||||
if (targetRel) {
|
||||
matchedTargetRelIds.add(targetRel.id);
|
||||
// Preserve source relationship but update cardinalities from target
|
||||
const result: DBRelationship = {
|
||||
...sourceRel,
|
||||
sourceCardinality: targetRel.sourceCardinality,
|
||||
targetCardinality: targetRel.targetCardinality,
|
||||
};
|
||||
|
||||
// Only include schema fields if they exist in the source relationship
|
||||
if (!sourceRel.sourceSchema) {
|
||||
delete result.sourceSchema;
|
||||
}
|
||||
if (!sourceRel.targetSchema) {
|
||||
delete result.targetSchema;
|
||||
}
|
||||
|
||||
resultRelationships.push(result);
|
||||
}
|
||||
});
|
||||
|
||||
// Add any target relationships that weren't matched (new relationships)
|
||||
targetRelationships.forEach((targetRel) => {
|
||||
if (!matchedTargetRelIds.has(targetRel.id)) {
|
||||
// Find the source IDs by reversing the mapping lookup
|
||||
let sourceTableId = targetRel.sourceTableId;
|
||||
let targetTableId = targetRel.targetTableId;
|
||||
let sourceFieldId = targetRel.sourceFieldId;
|
||||
let targetFieldId = targetRel.targetFieldId;
|
||||
|
||||
// Find source table/field IDs from the mappings
|
||||
for (const [targetId, srcId] of Object.entries(idMappings.tables)) {
|
||||
if (targetId === targetRel.sourceTableId) {
|
||||
sourceTableId = srcId;
|
||||
}
|
||||
if (targetId === targetRel.targetTableId) {
|
||||
targetTableId = srcId;
|
||||
}
|
||||
}
|
||||
|
||||
for (const [targetId, srcId] of Object.entries(idMappings.fields)) {
|
||||
if (targetId === targetRel.sourceFieldId) {
|
||||
sourceFieldId = srcId;
|
||||
}
|
||||
if (targetId === targetRel.targetFieldId) {
|
||||
targetFieldId = srcId;
|
||||
}
|
||||
}
|
||||
|
||||
resultRelationships.push({
|
||||
...targetRel,
|
||||
sourceTableId,
|
||||
targetTableId,
|
||||
sourceFieldId,
|
||||
targetFieldId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return resultRelationships;
|
||||
};
|
||||
|
||||
// Functional helper to update dependencies
|
||||
const updateDependencies = (
|
||||
targetDependencies: DBDependency[] | undefined,
|
||||
sourceDependencies: DBDependency[] | undefined,
|
||||
idMappings: IdMappings
|
||||
): DBDependency[] => {
|
||||
if (!targetDependencies) return [];
|
||||
if (!sourceDependencies) return targetDependencies;
|
||||
|
||||
return targetDependencies.map((targetDep) => {
|
||||
// Find matching source dependency
|
||||
const sourceDep = sourceDependencies.find((srcDep) => {
|
||||
const srcTableId = idMappings.tables[targetDep.tableId];
|
||||
const srcDependentTableId =
|
||||
idMappings.tables[targetDep.dependentTableId];
|
||||
|
||||
return (
|
||||
srcDep.tableId === srcTableId &&
|
||||
srcDep.dependentTableId === srcDependentTableId
|
||||
);
|
||||
});
|
||||
|
||||
if (sourceDep) {
|
||||
return {
|
||||
...targetDep,
|
||||
id: sourceDep.id,
|
||||
tableId:
|
||||
idMappings.tables[targetDep.tableId] || targetDep.tableId,
|
||||
dependentTableId:
|
||||
idMappings.tables[targetDep.dependentTableId] ||
|
||||
targetDep.dependentTableId,
|
||||
};
|
||||
}
|
||||
|
||||
// If no match found, just update the table references
|
||||
return {
|
||||
...targetDep,
|
||||
tableId: idMappings.tables[targetDep.tableId] || targetDep.tableId,
|
||||
dependentTableId:
|
||||
idMappings.tables[targetDep.dependentTableId] ||
|
||||
targetDep.dependentTableId,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// Functional helper to update index field references
|
||||
const updateIndexFieldReferences = (
|
||||
tables: DBTable[] | undefined,
|
||||
idMappings: IdMappings
|
||||
): DBTable[] => {
|
||||
if (!tables) return [];
|
||||
|
||||
return tables.map((table) => ({
|
||||
...table,
|
||||
indexes: table.indexes?.map((index) => ({
|
||||
...index,
|
||||
fieldIds: index.fieldIds.map(
|
||||
(fieldId) => idMappings.fields[fieldId] || fieldId
|
||||
),
|
||||
})),
|
||||
}));
|
||||
};
|
||||
|
||||
export const applyDBMLChanges = ({
|
||||
sourceDiagram,
|
||||
targetDiagram,
|
||||
}: {
|
||||
sourceDiagram: Diagram;
|
||||
targetDiagram: Diagram;
|
||||
}): Diagram => {
|
||||
// Step 1: Build mappings from source diagram
|
||||
const { objectKeysToIdsMap, sourceIdToDataMap } =
|
||||
buildSourceMappings(sourceDiagram);
|
||||
|
||||
// Step 2: Update tables and collect ID mappings
|
||||
const { tables: updatedTables, idMappings } = updateTables({
|
||||
targetTables: targetDiagram.tables,
|
||||
sourceTables: sourceDiagram.tables,
|
||||
objectKeysToIdsMap,
|
||||
sourceIdToDataMap,
|
||||
defaultDatabaseSchema: defaultSchemas[sourceDiagram.databaseType],
|
||||
});
|
||||
|
||||
// Step 3: Update all other entities functionally
|
||||
const newCustomTypes = updateCustomTypes(
|
||||
targetDiagram.customTypes,
|
||||
objectKeysToIdsMap
|
||||
);
|
||||
|
||||
const updatedCustomTypes = [
|
||||
...(sourceDiagram.customTypes?.filter(
|
||||
(ct) => ct.kind === DBCustomTypeKind.composite
|
||||
) ?? []),
|
||||
...newCustomTypes,
|
||||
];
|
||||
|
||||
const updatedRelationships = updateRelationships(
|
||||
targetDiagram.relationships,
|
||||
sourceDiagram.relationships,
|
||||
idMappings
|
||||
);
|
||||
|
||||
const updatedDependencies = updateDependencies(
|
||||
targetDiagram.dependencies,
|
||||
sourceDiagram.dependencies,
|
||||
idMappings
|
||||
);
|
||||
|
||||
// Step 4: Update index field references
|
||||
const finalTables = updateIndexFieldReferences(updatedTables, idMappings);
|
||||
|
||||
// Sort relationships to match source order
|
||||
const sortedRelationships = [...updatedRelationships].sort((a, b) => {
|
||||
// Find source relationships to get their order
|
||||
const sourceRelA = sourceDiagram.relationships?.find(
|
||||
(r) => r.id === a.id
|
||||
);
|
||||
const sourceRelB = sourceDiagram.relationships?.find(
|
||||
(r) => r.id === b.id
|
||||
);
|
||||
|
||||
if (!sourceRelA || !sourceRelB) return 0;
|
||||
|
||||
const indexA = sourceDiagram.relationships?.indexOf(sourceRelA) ?? 0;
|
||||
const indexB = sourceDiagram.relationships?.indexOf(sourceRelB) ?? 0;
|
||||
|
||||
return indexA - indexB;
|
||||
});
|
||||
|
||||
// Return a new diagram object with tables sorted by order
|
||||
const result: Diagram = {
|
||||
...sourceDiagram,
|
||||
tables: finalTables.sort((a, b) => (a.order ?? 0) - (b.order ?? 0)),
|
||||
areas: targetDiagram.areas,
|
||||
relationships: sortedRelationships,
|
||||
dependencies: updatedDependencies,
|
||||
customTypes: updatedCustomTypes,
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -937,26 +937,482 @@ describe('DBML Export - Issue Fixes', () => {
|
||||
// Check that indexes are properly formatted with names
|
||||
// Note: When a table has a schema, index names are prefixed with the schema
|
||||
expect(result.standardDbml).toContain(
|
||||
'email [unique, name: "public_idx_email"]'
|
||||
'email [unique, name: "idx_email"]'
|
||||
);
|
||||
expect(result.standardDbml).toContain(
|
||||
'created_at [name: "public_idx_created_at"]'
|
||||
'created_at [name: "idx_created_at"]'
|
||||
);
|
||||
expect(result.standardDbml).toContain(
|
||||
'(email, created_at) [name: "public_idx_email_created"]'
|
||||
'(email, created_at) [name: "idx_email_created"]'
|
||||
);
|
||||
|
||||
// Verify proper index syntax in the table
|
||||
const indexSection = result.standardDbml.match(/Indexes \{[\s\S]*?\}/);
|
||||
expect(indexSection).toBeTruthy();
|
||||
expect(indexSection![0]).toContain('email [unique, name: "idx_email"]');
|
||||
expect(indexSection![0]).toContain(
|
||||
'email [unique, name: "public_idx_email"]'
|
||||
'created_at [name: "idx_created_at"]'
|
||||
);
|
||||
expect(indexSection![0]).toContain(
|
||||
'created_at [name: "public_idx_created_at"]'
|
||||
);
|
||||
expect(indexSection![0]).toContain(
|
||||
'(email, created_at) [name: "public_idx_email_created"]'
|
||||
'(email, created_at) [name: "idx_email_created"]'
|
||||
);
|
||||
});
|
||||
|
||||
it('should export in the right format', () => {
|
||||
const diagram: Diagram = {
|
||||
id: 'mqqwkkodrxxd',
|
||||
name: 'Diagram 9',
|
||||
createdAt: new Date('2025-07-30T15:44:53.967Z'),
|
||||
updatedAt: new Date('2025-07-30T16:11:22.554Z'),
|
||||
databaseType: DatabaseType.POSTGRESQL,
|
||||
tables: [
|
||||
{
|
||||
id: '8ftpn9qn0o2ddrvhzgdjro3zv',
|
||||
name: 'table_1',
|
||||
x: 260,
|
||||
y: 80,
|
||||
fields: [
|
||||
{
|
||||
id: 'w9wlmimvjaci2krhfb4v9bhy0',
|
||||
name: 'id',
|
||||
type: { id: 'bigint', name: 'bigint' },
|
||||
unique: true,
|
||||
nullable: false,
|
||||
primaryKey: true,
|
||||
createdAt: 1753890297335,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
color: '#4dee8a',
|
||||
createdAt: 1753890297335,
|
||||
isView: false,
|
||||
order: 0,
|
||||
parentAreaId: null,
|
||||
},
|
||||
{
|
||||
id: 'wofcygo4u9623oueif9k3v734',
|
||||
name: 'table_2',
|
||||
x: -178.62499999999994,
|
||||
y: -244.375,
|
||||
fields: [
|
||||
{
|
||||
id: '6ca6p6lnss4d2top8pjcfsli7',
|
||||
name: 'id',
|
||||
type: { id: 'bigint', name: 'bigint' },
|
||||
unique: true,
|
||||
nullable: false,
|
||||
primaryKey: true,
|
||||
createdAt: 1753891879081,
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
color: '#4dee8a',
|
||||
createdAt: 1753891879081,
|
||||
isView: false,
|
||||
order: 1,
|
||||
parentAreaId: null,
|
||||
},
|
||||
],
|
||||
relationships: [
|
||||
{
|
||||
id: 'o5ynn1x9nxm5ipuugo690doau',
|
||||
name: 'table_2_id_fk',
|
||||
sourceTableId: 'wofcygo4u9623oueif9k3v734',
|
||||
targetTableId: '8ftpn9qn0o2ddrvhzgdjro3zv',
|
||||
sourceFieldId: '6ca6p6lnss4d2top8pjcfsli7',
|
||||
targetFieldId: 'w9wlmimvjaci2krhfb4v9bhy0',
|
||||
sourceCardinality: 'one',
|
||||
targetCardinality: 'one',
|
||||
createdAt: 1753891882554,
|
||||
},
|
||||
],
|
||||
dependencies: [],
|
||||
areas: [],
|
||||
customTypes: [],
|
||||
};
|
||||
|
||||
const result = generateDBMLFromDiagram(diagram);
|
||||
|
||||
const expectedInlineDBML = `Table "table_1" {
|
||||
"id" bigint [pk, not null]
|
||||
}
|
||||
|
||||
Table "table_2" {
|
||||
"id" bigint [pk, not null, ref: < "table_1"."id"]
|
||||
}
|
||||
`;
|
||||
|
||||
const expectedStandardDBML = `Table "table_1" {
|
||||
"id" bigint [pk, not null]
|
||||
}
|
||||
|
||||
Table "table_2" {
|
||||
"id" bigint [pk, not null]
|
||||
}
|
||||
|
||||
Ref "fk_0_table_2_id_fk":"table_1"."id" < "table_2"."id"
|
||||
`;
|
||||
|
||||
expect(result.inlineDbml).toBe(expectedInlineDBML);
|
||||
expect(result.standardDbml).toBe(expectedStandardDBML);
|
||||
});
|
||||
|
||||
it('should handle tables with multiple relationships correctly', () => {
|
||||
const diagram: Diagram = {
|
||||
id: 'test-diagram',
|
||||
name: 'Test',
|
||||
databaseType: DatabaseType.POSTGRESQL,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
tables: [
|
||||
{
|
||||
id: 'users',
|
||||
name: 'users',
|
||||
x: 0,
|
||||
y: 0,
|
||||
fields: [
|
||||
{
|
||||
id: 'users_id',
|
||||
name: 'id',
|
||||
type: { id: 'integer', name: 'integer' },
|
||||
primaryKey: true,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
collation: null,
|
||||
default: null,
|
||||
characterMaximumLength: null,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
color: 'blue',
|
||||
isView: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'posts',
|
||||
name: 'posts',
|
||||
x: 0,
|
||||
y: 0,
|
||||
fields: [
|
||||
{
|
||||
id: 'posts_id',
|
||||
name: 'id',
|
||||
type: { id: 'integer', name: 'integer' },
|
||||
primaryKey: true,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
collation: null,
|
||||
default: null,
|
||||
characterMaximumLength: null,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'posts_user_id',
|
||||
name: 'user_id',
|
||||
type: { id: 'integer', name: 'integer' },
|
||||
primaryKey: false,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
collation: null,
|
||||
default: null,
|
||||
characterMaximumLength: null,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
color: 'blue',
|
||||
isView: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'reviews',
|
||||
name: 'reviews',
|
||||
x: 0,
|
||||
y: 0,
|
||||
fields: [
|
||||
{
|
||||
id: 'reviews_id',
|
||||
name: 'id',
|
||||
type: { id: 'integer', name: 'integer' },
|
||||
primaryKey: true,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
collation: null,
|
||||
default: null,
|
||||
characterMaximumLength: null,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'reviews_user_id',
|
||||
name: 'user_id',
|
||||
type: { id: 'integer', name: 'integer' },
|
||||
primaryKey: false,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
collation: null,
|
||||
default: null,
|
||||
characterMaximumLength: null,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
color: 'blue',
|
||||
isView: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'user_activities',
|
||||
name: 'user_activities',
|
||||
x: 0,
|
||||
y: 0,
|
||||
fields: [
|
||||
{
|
||||
id: 'activities_id',
|
||||
name: 'id',
|
||||
type: { id: 'integer', name: 'integer' },
|
||||
primaryKey: true,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
collation: null,
|
||||
default: null,
|
||||
characterMaximumLength: null,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'activities_entity_id',
|
||||
name: 'entity_id',
|
||||
type: { id: 'integer', name: 'integer' },
|
||||
primaryKey: false,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
collation: null,
|
||||
default: null,
|
||||
characterMaximumLength: null,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'activities_type',
|
||||
name: 'activity_type',
|
||||
type: { id: 'varchar', name: 'varchar' },
|
||||
primaryKey: false,
|
||||
nullable: true,
|
||||
unique: false,
|
||||
collation: null,
|
||||
default: null,
|
||||
characterMaximumLength: '50',
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
color: 'blue',
|
||||
isView: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
relationships: [
|
||||
{
|
||||
id: 'rel1',
|
||||
name: 'fk_posts_user',
|
||||
sourceTableId: 'posts',
|
||||
sourceFieldId: 'posts_user_id',
|
||||
targetTableId: 'users',
|
||||
targetFieldId: 'users_id',
|
||||
sourceCardinality: 'many',
|
||||
targetCardinality: 'one',
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'rel2',
|
||||
name: 'fk_reviews_user',
|
||||
sourceTableId: 'reviews',
|
||||
sourceFieldId: 'reviews_user_id',
|
||||
targetTableId: 'users',
|
||||
targetFieldId: 'users_id',
|
||||
sourceCardinality: 'many',
|
||||
targetCardinality: 'one',
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'rel3',
|
||||
name: 'fk_activities_posts',
|
||||
sourceTableId: 'user_activities',
|
||||
sourceFieldId: 'activities_entity_id',
|
||||
targetTableId: 'posts',
|
||||
targetFieldId: 'posts_id',
|
||||
sourceCardinality: 'many',
|
||||
targetCardinality: 'one',
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'rel4',
|
||||
name: 'fk_activities_reviews',
|
||||
sourceTableId: 'user_activities',
|
||||
sourceFieldId: 'activities_entity_id',
|
||||
targetTableId: 'reviews',
|
||||
targetFieldId: 'reviews_id',
|
||||
sourceCardinality: 'many',
|
||||
targetCardinality: 'one',
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = generateDBMLFromDiagram(diagram);
|
||||
|
||||
// Debug output removed
|
||||
// console.log('Inline DBML:', result.inlineDbml);
|
||||
|
||||
// Check standard DBML output
|
||||
expect(result.standardDbml).toContain('Table "users" {');
|
||||
expect(result.standardDbml).toContain('Table "posts" {');
|
||||
expect(result.standardDbml).toContain('Table "reviews" {');
|
||||
expect(result.standardDbml).toContain('Table "user_activities" {');
|
||||
|
||||
// Check that the entity_id field in user_activities has multiple relationships in inline DBML
|
||||
// The field should have both references in a single bracket
|
||||
expect(result.inlineDbml).toContain(
|
||||
'"entity_id" integer [not null, ref: < "posts"."id", ref: < "reviews"."id"]'
|
||||
);
|
||||
|
||||
// Check that standard DBML has separate Ref entries for each relationship
|
||||
expect(result.standardDbml).toContain(
|
||||
'Ref "fk_0_fk_posts_user":"users"."id" < "posts"."user_id"'
|
||||
);
|
||||
expect(result.standardDbml).toContain(
|
||||
'Ref "fk_1_fk_reviews_user":"users"."id" < "reviews"."user_id"'
|
||||
);
|
||||
expect(result.standardDbml).toContain(
|
||||
'Ref "fk_2_fk_activities_posts":"posts"."id" < "user_activities"."entity_id"'
|
||||
);
|
||||
expect(result.standardDbml).toContain(
|
||||
'Ref "fk_3_fk_activities_reviews":"reviews"."id" < "user_activities"."entity_id"'
|
||||
);
|
||||
|
||||
// No automatic comment is added for fields with multiple relationships
|
||||
|
||||
// Check proper formatting - closing brace should be on a new line
|
||||
expect(result.inlineDbml).toMatch(
|
||||
/Table "user_activities" \{\s*\n\s*"id".*\n\s*"entity_id".*\]\s*\n\s*"activity_type".*\n\s*\}/
|
||||
);
|
||||
|
||||
// Ensure no closing brace appears on the same line as a field with inline refs
|
||||
expect(result.inlineDbml).not.toMatch(/\[.*ref:.*\]\}/);
|
||||
});
|
||||
|
||||
it('should properly format closing brace when table has both indexes and inline refs', () => {
|
||||
const diagram: Diagram = {
|
||||
id: 'test-diagram',
|
||||
name: 'Test',
|
||||
databaseType: DatabaseType.POSTGRESQL,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
tables: [
|
||||
{
|
||||
id: 'table1',
|
||||
name: 'table_1',
|
||||
x: 0,
|
||||
y: 0,
|
||||
fields: [
|
||||
{
|
||||
id: 'field1',
|
||||
name: 'id',
|
||||
type: { id: 'bigint', name: 'bigint' },
|
||||
primaryKey: true,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
collation: null,
|
||||
default: null,
|
||||
characterMaximumLength: null,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
indexes: [
|
||||
{
|
||||
id: 'index1',
|
||||
name: 'index_1',
|
||||
unique: false,
|
||||
fieldIds: ['field1'],
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
color: 'blue',
|
||||
isView: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'table2',
|
||||
name: 'table_2',
|
||||
x: 0,
|
||||
y: 0,
|
||||
fields: [
|
||||
{
|
||||
id: 'field2',
|
||||
name: 'id',
|
||||
type: { id: 'bigint', name: 'bigint' },
|
||||
primaryKey: true,
|
||||
nullable: false,
|
||||
unique: false,
|
||||
collation: null,
|
||||
default: null,
|
||||
characterMaximumLength: null,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
indexes: [],
|
||||
color: 'blue',
|
||||
isView: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
relationships: [
|
||||
{
|
||||
id: 'rel1',
|
||||
name: 'table2_id_fkey',
|
||||
sourceTableId: 'table2',
|
||||
sourceFieldId: 'field2',
|
||||
targetTableId: 'table1',
|
||||
targetFieldId: 'field1',
|
||||
sourceCardinality: 'many',
|
||||
targetCardinality: 'one',
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = generateDBMLFromDiagram(diagram);
|
||||
|
||||
// Check that the inline DBML has proper indentation
|
||||
expect(result.inlineDbml).toContain(`Table "table_1" {
|
||||
"id" bigint [pk, not null]
|
||||
|
||||
Indexes {
|
||||
id [name: "index_1"]
|
||||
}
|
||||
}`);
|
||||
|
||||
expect(result.inlineDbml).toContain(`Table "table_2" {
|
||||
"id" bigint [pk, not null, ref: < "table_1"."id"]
|
||||
}`);
|
||||
|
||||
// The issue was that it would generate:
|
||||
// Table "table_1" {
|
||||
// "id" bigint [pk, not null]
|
||||
//
|
||||
// Indexes {
|
||||
// id [name: "index_1"]
|
||||
//
|
||||
// }
|
||||
// }
|
||||
|
||||
// Make sure there's no malformed closing brace
|
||||
expect(result.inlineDbml).not.toMatch(/\n\s*\n\s*}\s*\n}/);
|
||||
expect(result.inlineDbml).not.toMatch(/\s+\n}/);
|
||||
|
||||
// Ensure there's no extra closing brace
|
||||
const braceBalance =
|
||||
(result.inlineDbml.match(/{/g) || []).length -
|
||||
(result.inlineDbml.match(/}/g) || []).length;
|
||||
expect(braceBalance).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { DBTable } from '@/lib/domain/db-table';
|
||||
import { type DBField } from '@/lib/domain/db-field';
|
||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
|
||||
// Use DBCustomType for generating Enum DBML
|
||||
const generateEnumsDBML = (customTypes: DBCustomType[] | undefined): string => {
|
||||
@@ -249,34 +248,67 @@ const convertToInlineRefs = (dbml: string): string => {
|
||||
fullMatch: string;
|
||||
};
|
||||
} = {};
|
||||
// Updated pattern to handle various table name formats including schema.table
|
||||
const tablePattern =
|
||||
/Table\s+(?:"([^"]+)"(?:\."([^"]+)")?|(\[?[^\s[]+\]?\.\[?[^\s\]]+\]?)|(\[?[^\s[{]+\]?))\s*{([^}]*)}/g;
|
||||
|
||||
let tableMatch;
|
||||
while ((tableMatch = tablePattern.exec(dbml)) !== null) {
|
||||
// Extract table name - handle schema.table format
|
||||
// Use a more sophisticated approach to handle nested braces
|
||||
let currentPos = 0;
|
||||
while (currentPos < dbml.length) {
|
||||
// Find the next table definition
|
||||
const tableStartPattern =
|
||||
/Table\s+(?:"([^"]+)"(?:\."([^"]+)")?|(\[?[^\s[]+\]?\.\[?[^\s\]]+\]?)|(\[?[^\s[{]+\]?))\s*{/g;
|
||||
tableStartPattern.lastIndex = currentPos;
|
||||
const tableStartMatch = tableStartPattern.exec(dbml);
|
||||
|
||||
if (!tableStartMatch) break;
|
||||
|
||||
// Extract table name
|
||||
let tableName;
|
||||
if (tableMatch[1] && tableMatch[2]) {
|
||||
// Format: "schema"."table"
|
||||
tableName = `${tableMatch[1]}.${tableMatch[2]}`;
|
||||
} else if (tableMatch[1]) {
|
||||
// Format: "table" (no schema)
|
||||
tableName = tableMatch[1];
|
||||
if (tableStartMatch[1] && tableStartMatch[2]) {
|
||||
tableName = `${tableStartMatch[1]}.${tableStartMatch[2]}`;
|
||||
} else if (tableStartMatch[1]) {
|
||||
tableName = tableStartMatch[1];
|
||||
} else {
|
||||
// Other formats
|
||||
tableName = tableMatch[3] || tableMatch[4];
|
||||
tableName = tableStartMatch[3] || tableStartMatch[4];
|
||||
}
|
||||
|
||||
// Clean up any bracket syntax from table names
|
||||
const cleanTableName = tableName.replace(/\[([^\]]+)\]/g, '$1');
|
||||
|
||||
tables[cleanTableName] = {
|
||||
start: tableMatch.index,
|
||||
end: tableMatch.index + tableMatch[0].length,
|
||||
content: tableMatch[5],
|
||||
fullMatch: tableMatch[0],
|
||||
};
|
||||
// Find the matching closing brace by counting nested braces
|
||||
const openBracePos =
|
||||
tableStartMatch.index + tableStartMatch[0].length - 1;
|
||||
let braceCount = 1;
|
||||
const contentStart = openBracePos + 1;
|
||||
let contentEnd = contentStart;
|
||||
|
||||
for (let i = contentStart; i < dbml.length && braceCount > 0; i++) {
|
||||
if (dbml[i] === '{') braceCount++;
|
||||
else if (dbml[i] === '}') {
|
||||
braceCount--;
|
||||
if (braceCount === 0) {
|
||||
contentEnd = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (braceCount === 0) {
|
||||
const content = dbml.substring(contentStart, contentEnd);
|
||||
const fullMatch = dbml.substring(
|
||||
tableStartMatch.index,
|
||||
contentEnd + 1
|
||||
);
|
||||
|
||||
tables[cleanTableName] = {
|
||||
start: tableStartMatch.index,
|
||||
end: contentEnd + 1,
|
||||
content: content,
|
||||
fullMatch: fullMatch,
|
||||
};
|
||||
|
||||
currentPos = contentEnd + 1;
|
||||
} else {
|
||||
// Malformed DBML, skip this table
|
||||
currentPos = tableStartMatch.index + tableStartMatch[0].length;
|
||||
}
|
||||
}
|
||||
|
||||
if (refs.length === 0 || Object.keys(tables).length === 0) {
|
||||
@@ -286,9 +318,14 @@ const convertToInlineRefs = (dbml: string): string => {
|
||||
// Create a map for faster table lookup
|
||||
const tableMap = new Map(Object.entries(tables));
|
||||
|
||||
// 1. Add inline refs to table contents
|
||||
// 1. First, collect all refs per field
|
||||
const fieldRefs = new Map<
|
||||
string,
|
||||
{ table: string; refs: string[]; relatedTables: string[] }
|
||||
>();
|
||||
|
||||
refs.forEach((ref) => {
|
||||
let targetTableName, fieldNameToModify, inlineRefSyntax;
|
||||
let targetTableName, fieldNameToModify, inlineRefSyntax, relatedTable;
|
||||
|
||||
if (ref.direction === '<') {
|
||||
targetTableName = ref.targetSchema
|
||||
@@ -299,6 +336,7 @@ const convertToInlineRefs = (dbml: string): string => {
|
||||
? `"${ref.sourceSchema}"."${ref.sourceTable}"."${ref.sourceField}"`
|
||||
: `"${ref.sourceTable}"."${ref.sourceField}"`;
|
||||
inlineRefSyntax = `ref: < ${sourceRef}`;
|
||||
relatedTable = ref.sourceTable;
|
||||
} else {
|
||||
targetTableName = ref.sourceSchema
|
||||
? `${ref.sourceSchema}.${ref.sourceTable}`
|
||||
@@ -308,13 +346,32 @@ const convertToInlineRefs = (dbml: string): string => {
|
||||
? `"${ref.targetSchema}"."${ref.targetTable}"."${ref.targetField}"`
|
||||
: `"${ref.targetTable}"."${ref.targetField}"`;
|
||||
inlineRefSyntax = `ref: > ${targetRef}`;
|
||||
relatedTable = ref.targetTable;
|
||||
}
|
||||
|
||||
const tableData = tableMap.get(targetTableName);
|
||||
const fieldKey = `${targetTableName}.${fieldNameToModify}`;
|
||||
const existing = fieldRefs.get(fieldKey) || {
|
||||
table: targetTableName,
|
||||
refs: [],
|
||||
relatedTables: [],
|
||||
};
|
||||
existing.refs.push(inlineRefSyntax);
|
||||
existing.relatedTables.push(relatedTable);
|
||||
fieldRefs.set(fieldKey, existing);
|
||||
});
|
||||
|
||||
// 2. Apply all refs to fields
|
||||
fieldRefs.forEach((fieldData, fieldKey) => {
|
||||
// fieldKey might be "schema.table.field" or just "table.field"
|
||||
const lastDotIndex = fieldKey.lastIndexOf('.');
|
||||
const tableName = fieldKey.substring(0, lastDotIndex);
|
||||
const fieldName = fieldKey.substring(lastDotIndex + 1);
|
||||
const tableData = tableMap.get(tableName);
|
||||
|
||||
if (tableData) {
|
||||
// Updated pattern to capture field definition and all existing attributes in brackets
|
||||
const fieldPattern = new RegExp(
|
||||
`^([ \t]*"${fieldNameToModify}"[^\\n]*?)(?:\\s*(\\[[^\\]]*\\]))*\\s*(//.*)?$`,
|
||||
`^([ \t]*"${fieldName}"[^\\n]*?)(?:\\s*(\\[[^\\]]*\\]))*\\s*(//.*)?$`,
|
||||
'gm'
|
||||
);
|
||||
let newContent = tableData.content;
|
||||
@@ -322,11 +379,6 @@ const convertToInlineRefs = (dbml: string): string => {
|
||||
newContent = newContent.replace(
|
||||
fieldPattern,
|
||||
(lineMatch, fieldPart, existingBrackets, commentPart) => {
|
||||
// Avoid adding duplicate refs
|
||||
if (lineMatch.includes('ref:')) {
|
||||
return lineMatch;
|
||||
}
|
||||
|
||||
// Collect all attributes from existing brackets
|
||||
const allAttributes: string[] = [];
|
||||
if (existingBrackets) {
|
||||
@@ -344,20 +396,24 @@ const convertToInlineRefs = (dbml: string): string => {
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new ref
|
||||
allAttributes.push(inlineRefSyntax);
|
||||
// Add all refs for this field
|
||||
allAttributes.push(...fieldData.refs);
|
||||
|
||||
// Combine all attributes into a single bracket
|
||||
const combinedAttributes = allAttributes.join(', ');
|
||||
|
||||
return `${fieldPart.trim()} [${combinedAttributes}]${commentPart || ''}`;
|
||||
// Preserve original spacing from fieldPart
|
||||
const leadingSpaces = fieldPart.match(/^(\s*)/)?.[1] || '';
|
||||
const fieldDefWithoutSpaces = fieldPart.trim();
|
||||
|
||||
return `${leadingSpaces}${fieldDefWithoutSpaces} [${combinedAttributes}]${commentPart || ''}`;
|
||||
}
|
||||
);
|
||||
|
||||
// Update the table content if modified
|
||||
if (newContent !== tableData.content) {
|
||||
tableData.content = newContent;
|
||||
tableMap.set(targetTableName, tableData);
|
||||
tableMap.set(tableName, tableData);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -373,10 +429,48 @@ const convertToInlineRefs = (dbml: string): string => {
|
||||
reconstructedDbml += dbml.substring(lastIndex, tableData.start);
|
||||
// Preserve the original table definition format but with updated content
|
||||
const originalTableDef = tableData.fullMatch;
|
||||
const updatedTableDef = originalTableDef.replace(
|
||||
/{[^}]*}/,
|
||||
`{${tableData.content}}`
|
||||
let formattedContent = tableData.content;
|
||||
|
||||
// Clean up content formatting:
|
||||
// 1. Split into lines to handle each line individually
|
||||
const lines = formattedContent.split('\n');
|
||||
|
||||
// 2. Process lines to ensure proper formatting
|
||||
const processedLines = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const trimmedLine = line.trimEnd();
|
||||
|
||||
// Skip empty lines at the end if followed by a closing brace
|
||||
if (trimmedLine === '' && i === lines.length - 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip empty lines before a closing brace
|
||||
if (
|
||||
trimmedLine === '' &&
|
||||
i < lines.length - 1 &&
|
||||
lines[i + 1].trim().startsWith('}')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
processedLines.push(line);
|
||||
}
|
||||
|
||||
formattedContent = processedLines.join('\n');
|
||||
|
||||
// Ensure content ends with a newline before the table's closing brace
|
||||
if (!formattedContent.endsWith('\n')) {
|
||||
formattedContent = formattedContent + '\n';
|
||||
}
|
||||
|
||||
// Since we properly extracted content with nested braces, we need to rebuild the table definition
|
||||
const tableHeader = originalTableDef.substring(
|
||||
0,
|
||||
originalTableDef.indexOf('{') + 1
|
||||
);
|
||||
const updatedTableDef = `${tableHeader}${formattedContent}}`;
|
||||
reconstructedDbml += updatedTableDef;
|
||||
lastIndex = tableData.end;
|
||||
}
|
||||
@@ -388,7 +482,13 @@ const convertToInlineRefs = (dbml: string): string => {
|
||||
.filter((line) => !line.trim().startsWith('Ref '));
|
||||
const finalDbml = finalLines.join('\n').trim();
|
||||
|
||||
return finalDbml;
|
||||
// Clean up excessive empty lines - replace multiple consecutive empty lines with just one
|
||||
// But ensure there's at least one blank line between tables
|
||||
const cleanedDbml = finalDbml
|
||||
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
||||
.replace(/}\n(?=Table)/g, '}\n\n');
|
||||
|
||||
return cleanedDbml;
|
||||
};
|
||||
|
||||
// Function to check for SQL keywords (add more if needed)
|
||||
@@ -483,15 +583,15 @@ const fixTableBracketSyntax = (dbml: string): string => {
|
||||
};
|
||||
|
||||
// Restore schema information that may have been stripped by the DBML importer
|
||||
const restoreTableSchemas = (dbml: string, diagram: Diagram): string => {
|
||||
if (!diagram.tables) return dbml;
|
||||
const restoreTableSchemas = (dbml: string, tables: DBTable[]): string => {
|
||||
if (!tables || tables.length === 0) return dbml;
|
||||
|
||||
// Group tables by name to handle duplicates
|
||||
const tablesByName = new Map<
|
||||
string,
|
||||
Array<{ table: (typeof diagram.tables)[0]; index: number }>
|
||||
Array<{ table: DBTable; index: number }>
|
||||
>();
|
||||
diagram.tables.forEach((table, index) => {
|
||||
tables.forEach((table, index) => {
|
||||
const existing = tablesByName.get(table.name) || [];
|
||||
existing.push({ table, index });
|
||||
tablesByName.set(table.name, existing);
|
||||
@@ -541,30 +641,20 @@ const restoreTableSchemas = (dbml: string, diagram: Diagram): string => {
|
||||
}
|
||||
} else {
|
||||
// Multiple tables with the same name - need to be more careful
|
||||
const defaultSchema = defaultSchemas[diagram.databaseType];
|
||||
|
||||
// Separate tables by whether they have the default schema or not
|
||||
const defaultSchemaTable = tablesGroup.find(
|
||||
({ table }) => table.schema === defaultSchema
|
||||
);
|
||||
const nonDefaultSchemaTables = tablesGroup.filter(
|
||||
({ table }) => table.schema && table.schema !== defaultSchema
|
||||
);
|
||||
|
||||
// Find all table definitions for this name
|
||||
const escapedTableName = tableName.replace(
|
||||
/[.*+?^${}()|[\]\\]/g,
|
||||
'\\$&'
|
||||
);
|
||||
|
||||
// First, handle tables that already have schema in DBML
|
||||
const schemaTablePattern = new RegExp(
|
||||
`Table\\s+"[^"]+"\\.\\s*"${escapedTableName}"\\s*{`,
|
||||
'g'
|
||||
);
|
||||
result = result.replace(schemaTablePattern, (match) => {
|
||||
// This table already has a schema, keep it as is
|
||||
return match;
|
||||
// Get tables that need schema restoration (those without schema in DBML)
|
||||
const tablesNeedingSchema = tablesGroup.filter(({ table }) => {
|
||||
// Check if this table's schema is already in the DBML
|
||||
const schemaPattern = new RegExp(
|
||||
`Table\\s+"${table.schema}"\\.\\s*"${escapedTableName}"\\s*{`,
|
||||
'g'
|
||||
);
|
||||
return !result.match(schemaPattern);
|
||||
});
|
||||
|
||||
// Then handle tables without schema in DBML
|
||||
@@ -575,21 +665,25 @@ const restoreTableSchemas = (dbml: string, diagram: Diagram): string => {
|
||||
|
||||
let noSchemaMatchIndex = 0;
|
||||
result = result.replace(noSchemaTablePattern, (match) => {
|
||||
// If we have a table with the default schema and this is the first match without schema,
|
||||
// it should be the default schema table
|
||||
if (noSchemaMatchIndex === 0 && defaultSchemaTable) {
|
||||
noSchemaMatchIndex++;
|
||||
return `Table "${defaultSchema}"."${tableName}" {`;
|
||||
// We need to match based on the order in the DBML output
|
||||
// For PostgreSQL DBML, the @dbml/core sorts tables by:
|
||||
// 1. Tables with schemas (alphabetically)
|
||||
// 2. Tables without schemas
|
||||
// Since both our tables have schemas, they should appear in order
|
||||
|
||||
// Only process tables that need schema restoration
|
||||
if (noSchemaMatchIndex >= tablesNeedingSchema.length) {
|
||||
return match;
|
||||
}
|
||||
// Otherwise, try to match with non-default schema tables
|
||||
const remainingNonDefault =
|
||||
nonDefaultSchemaTables[
|
||||
noSchemaMatchIndex - (defaultSchemaTable ? 1 : 0)
|
||||
];
|
||||
if (remainingNonDefault) {
|
||||
noSchemaMatchIndex++;
|
||||
return `Table "${remainingNonDefault.table.schema}"."${tableName}" {`;
|
||||
|
||||
const correspondingTable =
|
||||
tablesNeedingSchema[noSchemaMatchIndex];
|
||||
noSchemaMatchIndex++;
|
||||
|
||||
if (correspondingTable && correspondingTable.table.schema) {
|
||||
return `Table "${correspondingTable.table.schema}"."${tableName}" {`;
|
||||
}
|
||||
// If the table doesn't have a schema, keep it as is
|
||||
return match;
|
||||
});
|
||||
}
|
||||
@@ -801,12 +895,26 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
|
||||
);
|
||||
|
||||
// Restore schema information that may have been stripped by DBML importer
|
||||
standard = restoreTableSchemas(standard, diagram);
|
||||
standard = restoreTableSchemas(standard, uniqueTables);
|
||||
|
||||
// Prepend Enum DBML to the standard output
|
||||
standard = enumsDBML + '\n' + standard;
|
||||
if (enumsDBML) {
|
||||
standard = enumsDBML + '\n\n' + standard;
|
||||
}
|
||||
|
||||
inline = normalizeCharTypeFormat(convertToInlineRefs(standard));
|
||||
|
||||
// Clean up excessive empty lines in both outputs
|
||||
standard = standard.replace(/\n\s*\n\s*\n/g, '\n\n');
|
||||
inline = inline.replace(/\n\s*\n\s*\n/g, '\n\n');
|
||||
|
||||
// Ensure proper formatting with newline at end
|
||||
if (!standard.endsWith('\n')) {
|
||||
standard += '\n';
|
||||
}
|
||||
if (!inline.endsWith('\n')) {
|
||||
inline += '\n';
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error(
|
||||
'Error during DBML generation process:',
|
||||
@@ -822,11 +930,11 @@ export function generateDBMLFromDiagram(diagram: Diagram): DBMLExportResult {
|
||||
|
||||
// If an error occurred, still prepend enums if they exist, or they'll be lost.
|
||||
// The error message will then follow.
|
||||
if (standard.startsWith('// Error generating DBML:')) {
|
||||
standard = enumsDBML + standard;
|
||||
if (standard.startsWith('// Error generating DBML:') && enumsDBML) {
|
||||
standard = enumsDBML + '\n\n' + standard;
|
||||
}
|
||||
if (inline.startsWith('// Error generating DBML:')) {
|
||||
inline = enumsDBML + inline;
|
||||
if (inline.startsWith('// Error generating DBML:') && enumsDBML) {
|
||||
inline = enumsDBML + '\n\n' + inline;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { importDBMLToDiagram } from '../dbml-import';
|
||||
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
|
||||
|
||||
describe('DBML Import - Fantasy Examples', () => {
|
||||
describe('Magical Academy System', () => {
|
||||
@@ -613,6 +614,228 @@ Note quest_system_note {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Enum Support', () => {
|
||||
it('should import enums as customTypes', async () => {
|
||||
const dbmlWithEnums = `
|
||||
// Test DBML with various enum definitions
|
||||
enum job_status {
|
||||
created [note: 'Waiting to be processed']
|
||||
running
|
||||
done
|
||||
failure
|
||||
}
|
||||
|
||||
// Enum with schema
|
||||
enum hr.employee_type {
|
||||
full_time
|
||||
part_time
|
||||
contractor
|
||||
intern
|
||||
}
|
||||
|
||||
// Enum with special characters and spaces
|
||||
enum grade {
|
||||
"A+"
|
||||
"A"
|
||||
"A-"
|
||||
"Not Yet Set"
|
||||
}
|
||||
|
||||
Table employees {
|
||||
id integer [pk]
|
||||
name varchar(200) [not null]
|
||||
status job_status
|
||||
type hr.employee_type
|
||||
performance_grade grade
|
||||
created_at timestamp [default: 'now()']
|
||||
}
|
||||
|
||||
Table projects {
|
||||
id integer [pk]
|
||||
name varchar(300) [not null]
|
||||
status job_status [not null]
|
||||
priority enum // inline enum without values - will be converted to varchar
|
||||
}`;
|
||||
|
||||
const diagram = await importDBMLToDiagram(dbmlWithEnums);
|
||||
|
||||
// Verify customTypes are created for enums
|
||||
expect(diagram.customTypes).toBeDefined();
|
||||
expect(diagram.customTypes).toHaveLength(3); // job_status, hr.employee_type, grade
|
||||
|
||||
// Check job_status enum
|
||||
const jobStatusEnum = diagram.customTypes?.find(
|
||||
(ct) => ct.name === 'job_status' && !ct.schema
|
||||
);
|
||||
expect(jobStatusEnum).toBeDefined();
|
||||
expect(jobStatusEnum?.kind).toBe(DBCustomTypeKind.enum);
|
||||
expect(jobStatusEnum?.values).toEqual([
|
||||
'created',
|
||||
'running',
|
||||
'done',
|
||||
'failure',
|
||||
]);
|
||||
|
||||
// Check hr.employee_type enum with schema
|
||||
const employeeTypeEnum = diagram.customTypes?.find(
|
||||
(ct) => ct.name === 'employee_type' && ct.schema === 'hr'
|
||||
);
|
||||
expect(employeeTypeEnum).toBeDefined();
|
||||
expect(employeeTypeEnum?.kind).toBe(DBCustomTypeKind.enum);
|
||||
expect(employeeTypeEnum?.values).toEqual([
|
||||
'full_time',
|
||||
'part_time',
|
||||
'contractor',
|
||||
'intern',
|
||||
]);
|
||||
|
||||
// Check grade enum with quoted values
|
||||
const gradeEnum = diagram.customTypes?.find(
|
||||
(ct) => ct.name === 'grade' && !ct.schema
|
||||
);
|
||||
expect(gradeEnum).toBeDefined();
|
||||
expect(gradeEnum?.kind).toBe(DBCustomTypeKind.enum);
|
||||
expect(gradeEnum?.values).toEqual(['A+', 'A', 'A-', 'Not Yet Set']);
|
||||
|
||||
// Verify tables are created
|
||||
expect(diagram.tables).toHaveLength(2);
|
||||
|
||||
// Check that enum fields in tables reference the custom types
|
||||
const employeesTable = diagram.tables?.find(
|
||||
(t) => t.name === 'employees'
|
||||
);
|
||||
const statusField = employeesTable?.fields.find(
|
||||
(f) => f.name === 'status'
|
||||
);
|
||||
const typeField = employeesTable?.fields.find(
|
||||
(f) => f.name === 'type'
|
||||
);
|
||||
const gradeField = employeesTable?.fields.find(
|
||||
(f) => f.name === 'performance_grade'
|
||||
);
|
||||
|
||||
// Verify fields have correct types
|
||||
expect(statusField?.type.id).toBe('job_status');
|
||||
expect(typeField?.type.id).toBe('employee_type');
|
||||
expect(gradeField?.type.id).toBe('grade');
|
||||
|
||||
// Check inline enum was converted to varchar
|
||||
const projectsTable = diagram.tables?.find(
|
||||
(t) => t.name === 'projects'
|
||||
);
|
||||
const priorityField = projectsTable?.fields.find(
|
||||
(f) => f.name === 'priority'
|
||||
);
|
||||
expect(priorityField?.type.id).toBe('varchar');
|
||||
});
|
||||
|
||||
it('should handle enum values with notes', async () => {
|
||||
const dbmlWithEnumNotes = `
|
||||
enum order_status {
|
||||
pending [note: 'Order has been placed but not confirmed']
|
||||
confirmed [note: 'Payment received and order confirmed']
|
||||
shipped [note: 'Order has been dispatched']
|
||||
delivered [note: 'Order delivered to customer']
|
||||
cancelled [note: 'Order cancelled by customer or system']
|
||||
}
|
||||
|
||||
Table orders {
|
||||
id integer [pk]
|
||||
status order_status [not null]
|
||||
}`;
|
||||
|
||||
const diagram = await importDBMLToDiagram(dbmlWithEnumNotes);
|
||||
|
||||
// Verify enum is created
|
||||
expect(diagram.customTypes).toHaveLength(1);
|
||||
|
||||
const orderStatusEnum = diagram.customTypes?.[0];
|
||||
expect(orderStatusEnum?.name).toBe('order_status');
|
||||
expect(orderStatusEnum?.kind).toBe(DBCustomTypeKind.enum);
|
||||
expect(orderStatusEnum?.values).toEqual([
|
||||
'pending',
|
||||
'confirmed',
|
||||
'shipped',
|
||||
'delivered',
|
||||
'cancelled',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle multiple schemas with same enum names', async () => {
|
||||
const dbmlWithSameEnumNames = `
|
||||
// Public schema status enum
|
||||
enum status {
|
||||
active
|
||||
inactive
|
||||
deleted
|
||||
}
|
||||
|
||||
// Admin schema status enum with different values
|
||||
enum admin.status {
|
||||
pending_approval
|
||||
approved
|
||||
rejected
|
||||
suspended
|
||||
}
|
||||
|
||||
Table public.users {
|
||||
id integer [pk]
|
||||
status status
|
||||
}
|
||||
|
||||
Table admin.users {
|
||||
id integer [pk]
|
||||
status admin.status
|
||||
}`;
|
||||
|
||||
const diagram = await importDBMLToDiagram(dbmlWithSameEnumNames);
|
||||
|
||||
// Verify both enums are created
|
||||
expect(diagram.customTypes).toHaveLength(2);
|
||||
|
||||
// Check public.status enum
|
||||
const publicStatusEnum = diagram.customTypes?.find(
|
||||
(ct) => ct.name === 'status' && !ct.schema
|
||||
);
|
||||
expect(publicStatusEnum).toBeDefined();
|
||||
expect(publicStatusEnum?.values).toEqual([
|
||||
'active',
|
||||
'inactive',
|
||||
'deleted',
|
||||
]);
|
||||
|
||||
// Check admin.status enum
|
||||
const adminStatusEnum = diagram.customTypes?.find(
|
||||
(ct) => ct.name === 'status' && ct.schema === 'admin'
|
||||
);
|
||||
expect(adminStatusEnum).toBeDefined();
|
||||
expect(adminStatusEnum?.values).toEqual([
|
||||
'pending_approval',
|
||||
'approved',
|
||||
'rejected',
|
||||
'suspended',
|
||||
]);
|
||||
|
||||
// Verify fields reference correct enums
|
||||
const publicUsersTable = diagram.tables?.find(
|
||||
(t) => t.name === 'users' && t.schema === 'public'
|
||||
);
|
||||
const adminUsersTable = diagram.tables?.find(
|
||||
(t) => t.name === 'users' && t.schema === 'admin'
|
||||
);
|
||||
|
||||
const publicStatusField = publicUsersTable?.fields.find(
|
||||
(f) => f.name === 'status'
|
||||
);
|
||||
const adminStatusField = adminUsersTable?.fields.find(
|
||||
(f) => f.name === 'status'
|
||||
);
|
||||
|
||||
expect(publicStatusField?.type.id).toBe('status');
|
||||
expect(adminStatusField?.type.id).toBe('status');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases and Special Features', () => {
|
||||
it('should handle tables with all DBML features', async () => {
|
||||
const edgeCaseDBML = `
|
||||
@@ -793,11 +1016,11 @@ Table "bb"."users" {
|
||||
expect(bbUsersTable?.fields).toHaveLength(1);
|
||||
|
||||
expect(aaUsersTable?.fields[0].name).toBe('id');
|
||||
expect(aaUsersTable?.fields[0].type.id).toBe('int');
|
||||
expect(aaUsersTable?.fields[0].type.id).toBe('integer');
|
||||
expect(aaUsersTable?.fields[0].primaryKey).toBe(true);
|
||||
|
||||
expect(bbUsersTable?.fields[0].name).toBe('id');
|
||||
expect(bbUsersTable?.fields[0].type.id).toBe('int');
|
||||
expect(bbUsersTable?.fields[0].type.id).toBe('integer');
|
||||
expect(bbUsersTable?.fields[0].primaryKey).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1021,4 +1244,47 @@ Table "public_3"."comments" {
|
||||
expect(relationshipsHaveSchemas).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Notes Support', () => {
|
||||
it('should import table with note', async () => {
|
||||
const dbmlWithTableNote = `
|
||||
Table products {
|
||||
id integer [pk]
|
||||
name varchar(100)
|
||||
Note: 'This table stores product information'
|
||||
}`;
|
||||
|
||||
const diagram = await importDBMLToDiagram(dbmlWithTableNote);
|
||||
|
||||
expect(diagram.tables).toHaveLength(1);
|
||||
const productsTable = diagram.tables?.[0];
|
||||
expect(productsTable?.name).toBe('products');
|
||||
expect(productsTable?.comments).toBe(
|
||||
'This table stores product information'
|
||||
);
|
||||
});
|
||||
|
||||
it('should import field with note', async () => {
|
||||
const dbmlWithFieldNote = `
|
||||
Table orders {
|
||||
id integer [pk]
|
||||
total numeric(10,2) [note: 'Order total including tax']
|
||||
}`;
|
||||
|
||||
const diagram = await importDBMLToDiagram(dbmlWithFieldNote);
|
||||
|
||||
expect(diagram.tables).toHaveLength(1);
|
||||
const ordersTable = diagram.tables?.[0];
|
||||
expect(ordersTable?.fields).toHaveLength(2);
|
||||
|
||||
const totalField = ordersTable?.fields.find(
|
||||
(f) => f.name === 'total'
|
||||
);
|
||||
|
||||
// Field notes should be imported
|
||||
expect(totalField).toBeDefined();
|
||||
expect(totalField?.name).toBe('total');
|
||||
expect(totalField?.comments).toBe('Order total including tax');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
40
src/lib/dbml/dbml-import/dbml-import-error.ts
Normal file
40
src/lib/dbml/dbml-import/dbml-import-error.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export interface DBMLError {
|
||||
message: string;
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
export function parseDBMLError(error: unknown): DBMLError | null {
|
||||
try {
|
||||
if (typeof error === 'string') {
|
||||
const parsed = JSON.parse(error);
|
||||
if (parsed.diags?.[0]) {
|
||||
const diag = parsed.diags[0];
|
||||
|
||||
return {
|
||||
message: diag.message,
|
||||
line: diag.location.start.line,
|
||||
column: diag.location.start.column,
|
||||
};
|
||||
}
|
||||
} else if (error && typeof error === 'object' && 'diags' in error) {
|
||||
const parsed = error as {
|
||||
diags: Array<{
|
||||
message: string;
|
||||
location: { start: { line: number; column: number } };
|
||||
}>;
|
||||
};
|
||||
if (parsed.diags?.[0]) {
|
||||
return {
|
||||
message: parsed.diags[0].message,
|
||||
line: parsed.diags[0].location.start.line,
|
||||
column: parsed.diags[0].location.start.column,
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing DBML error:', e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -4,10 +4,16 @@ import { generateDiagramId, generateId } from '@/lib/utils';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { Cardinality, DBRelationship } from '@/lib/domain/db-relationship';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||
import { genericDataTypes } from '@/lib/data/data-types/generic-data-types';
|
||||
import type { DataTypeData } from '@/lib/data/data-types/data-types';
|
||||
import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
|
||||
import { randomColor } from '@/lib/colors';
|
||||
import { DatabaseType } from '@/lib/domain/database-type';
|
||||
import type Field from '@dbml/core/types/model_structure/field';
|
||||
import type { DBIndex } from '@/lib/domain';
|
||||
import {
|
||||
DBCustomTypeKind,
|
||||
type DBCustomType,
|
||||
} from '@/lib/domain/db-custom-type';
|
||||
|
||||
// Preprocess DBML to handle unsupported features
|
||||
export const preprocessDBML = (content: string): string => {
|
||||
@@ -19,8 +25,8 @@ export const preprocessDBML = (content: string): string => {
|
||||
// Remove Note blocks
|
||||
processed = processed.replace(/Note\s+\w+\s*\{[^}]*\}/gs, '');
|
||||
|
||||
// Remove enum definitions (blocks)
|
||||
processed = processed.replace(/enum\s+\w+\s*\{[^}]*\}/gs, '');
|
||||
// Don't remove enum definitions - we'll parse them
|
||||
// processed = processed.replace(/enum\s+\w+\s*\{[^}]*\}/gs, '');
|
||||
|
||||
// Handle array types by converting them to text
|
||||
processed = processed.replace(/(\w+)\[\]/g, 'text');
|
||||
@@ -77,6 +83,10 @@ interface DBMLField {
|
||||
pk?: boolean;
|
||||
not_null?: boolean;
|
||||
increment?: boolean;
|
||||
characterMaximumLength?: string | null;
|
||||
precision?: number | null;
|
||||
scale?: number | null;
|
||||
note?: string | { value: string } | null;
|
||||
}
|
||||
|
||||
interface DBMLIndexColumn {
|
||||
@@ -110,39 +120,51 @@ interface DBMLRef {
|
||||
endpoints: [DBMLEndpoint, DBMLEndpoint];
|
||||
}
|
||||
|
||||
const mapDBMLTypeToGenericType = (dbmlType: string): DataType => {
|
||||
interface DBMLEnum {
|
||||
name: string;
|
||||
schema?: string | { name: string };
|
||||
values: Array<{ name: string; note?: string }>;
|
||||
note?: string | { value: string } | null;
|
||||
}
|
||||
|
||||
const mapDBMLTypeToDataType = (
|
||||
dbmlType: string,
|
||||
options?: { databaseType?: DatabaseType; enums?: DBMLEnum[] }
|
||||
): DataTypeData => {
|
||||
const normalizedType = dbmlType.toLowerCase().replace(/\(.*\)/, '');
|
||||
const matchedType = genericDataTypes.find((t) => t.id === normalizedType);
|
||||
if (matchedType) return matchedType;
|
||||
const typeMap: Record<string, string> = {
|
||||
int: 'int',
|
||||
integer: 'int',
|
||||
varchar: 'varchar',
|
||||
bool: 'boolean',
|
||||
boolean: 'boolean',
|
||||
number: 'numeric',
|
||||
string: 'varchar',
|
||||
text: 'text',
|
||||
timestamp: 'timestamp',
|
||||
datetime: 'timestamp',
|
||||
float: 'float',
|
||||
double: 'double',
|
||||
decimal: 'decimal',
|
||||
bigint: 'bigint',
|
||||
smallint: 'smallint',
|
||||
char: 'char',
|
||||
};
|
||||
const mappedType = typeMap[normalizedType];
|
||||
if (mappedType) {
|
||||
const foundType = genericDataTypes.find((t) => t.id === mappedType);
|
||||
if (foundType) return foundType;
|
||||
|
||||
// Check if it's an enum type
|
||||
if (options?.enums) {
|
||||
const enumDef = options.enums.find((e) => {
|
||||
// Check both with and without schema prefix
|
||||
const enumName = e.name.toLowerCase();
|
||||
const enumFullName = e.schema
|
||||
? `${e.schema}.${enumName}`
|
||||
: enumName;
|
||||
return (
|
||||
normalizedType === enumName || normalizedType === enumFullName
|
||||
);
|
||||
});
|
||||
|
||||
if (enumDef) {
|
||||
// Return enum as custom type reference
|
||||
return {
|
||||
id: enumDef.name,
|
||||
name: enumDef.name,
|
||||
} satisfies DataTypeData;
|
||||
}
|
||||
}
|
||||
const type = genericDataTypes.find((t) => t.id === 'varchar')!;
|
||||
|
||||
const matchedType = findDataTypeDataById(
|
||||
normalizedType,
|
||||
options?.databaseType
|
||||
);
|
||||
if (matchedType) return matchedType;
|
||||
|
||||
return {
|
||||
id: type.id,
|
||||
name: type.name,
|
||||
};
|
||||
id: normalizedType.split(' ').join('_').toLowerCase(),
|
||||
name: normalizedType,
|
||||
} satisfies DataTypeData;
|
||||
};
|
||||
|
||||
const determineCardinality = (
|
||||
@@ -163,7 +185,10 @@ const determineCardinality = (
|
||||
};
|
||||
|
||||
export const importDBMLToDiagram = async (
|
||||
dbmlContent: string
|
||||
dbmlContent: string,
|
||||
options?: {
|
||||
databaseType?: DatabaseType;
|
||||
}
|
||||
): Promise<Diagram> => {
|
||||
try {
|
||||
// Handle empty content
|
||||
@@ -171,7 +196,7 @@ export const importDBMLToDiagram = async (
|
||||
return {
|
||||
id: generateDiagramId(),
|
||||
name: 'DBML Import',
|
||||
databaseType: DatabaseType.GENERIC,
|
||||
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
|
||||
tables: [],
|
||||
relationships: [],
|
||||
createdAt: new Date(),
|
||||
@@ -189,7 +214,7 @@ export const importDBMLToDiagram = async (
|
||||
return {
|
||||
id: generateDiagramId(),
|
||||
name: 'DBML Import',
|
||||
databaseType: DatabaseType.GENERIC,
|
||||
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
|
||||
tables: [],
|
||||
relationships: [],
|
||||
createdAt: new Date(),
|
||||
@@ -204,7 +229,7 @@ export const importDBMLToDiagram = async (
|
||||
return {
|
||||
id: generateDiagramId(),
|
||||
name: 'DBML Import',
|
||||
databaseType: DatabaseType.GENERIC,
|
||||
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
|
||||
tables: [],
|
||||
relationships: [],
|
||||
createdAt: new Date(),
|
||||
@@ -215,6 +240,55 @@ export const importDBMLToDiagram = async (
|
||||
// Process all schemas, not just the first one
|
||||
const allTables: DBMLTable[] = [];
|
||||
const allRefs: DBMLRef[] = [];
|
||||
const allEnums: DBMLEnum[] = [];
|
||||
|
||||
const getFieldExtraAttributes = (
|
||||
field: Field,
|
||||
enums: DBMLEnum[]
|
||||
): Partial<DBMLField> => {
|
||||
if (!field.type || !field.type.args) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const args = field.type.args.split(',') as string[];
|
||||
|
||||
const dataType = mapDBMLTypeToDataType(field.type.type_name, {
|
||||
...options,
|
||||
enums,
|
||||
});
|
||||
|
||||
if (dataType.fieldAttributes?.hasCharMaxLength) {
|
||||
const charMaxLength = args?.[0];
|
||||
return {
|
||||
characterMaximumLength: charMaxLength,
|
||||
};
|
||||
} else if (
|
||||
dataType.fieldAttributes?.precision &&
|
||||
dataType.fieldAttributes?.scale
|
||||
) {
|
||||
const precisionNum = args?.[0] ? parseInt(args[0]) : undefined;
|
||||
const scaleNum = args?.[1] ? parseInt(args[1]) : undefined;
|
||||
|
||||
const precision = precisionNum
|
||||
? isNaN(precisionNum)
|
||||
? undefined
|
||||
: precisionNum
|
||||
: undefined;
|
||||
|
||||
const scale = scaleNum
|
||||
? isNaN(scaleNum)
|
||||
? undefined
|
||||
: scaleNum
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
precision,
|
||||
scale,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
parsedData.schemas.forEach((schema) => {
|
||||
if (schema.tables) {
|
||||
@@ -230,17 +304,18 @@ export const importDBMLToDiagram = async (
|
||||
name: table.name,
|
||||
schema: schemaName,
|
||||
note: table.note,
|
||||
fields: table.fields.map(
|
||||
(field) =>
|
||||
({
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
unique: field.unique,
|
||||
pk: field.pk,
|
||||
not_null: field.not_null,
|
||||
increment: field.increment,
|
||||
}) satisfies DBMLField
|
||||
),
|
||||
fields: table.fields.map((field): DBMLField => {
|
||||
return {
|
||||
name: field.name,
|
||||
type: field.type,
|
||||
unique: field.unique,
|
||||
pk: field.pk,
|
||||
not_null: field.not_null,
|
||||
increment: field.increment,
|
||||
note: field.note,
|
||||
...getFieldExtraAttributes(field, allEnums),
|
||||
} satisfies DBMLField;
|
||||
}),
|
||||
indexes:
|
||||
table.indexes?.map((dbmlIndex) => {
|
||||
let indexColumns: string[];
|
||||
@@ -314,15 +389,34 @@ export const importDBMLToDiagram = async (
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (schema.enums) {
|
||||
schema.enums.forEach((enumDef) => {
|
||||
// Get schema name from enum or use schema's name
|
||||
const enumSchema =
|
||||
typeof enumDef.schema === 'string'
|
||||
? enumDef.schema
|
||||
: enumDef.schema?.name || schema.name;
|
||||
|
||||
allEnums.push({
|
||||
name: enumDef.name,
|
||||
schema: enumSchema === 'public' ? '' : enumSchema,
|
||||
values: enumDef.values || [],
|
||||
note: enumDef.note,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Extract only the necessary data from the parsed DBML
|
||||
const extractedData: {
|
||||
tables: DBMLTable[];
|
||||
refs: DBMLRef[];
|
||||
enums: DBMLEnum[];
|
||||
} = {
|
||||
tables: allTables,
|
||||
refs: allRefs,
|
||||
enums: allEnums,
|
||||
};
|
||||
|
||||
// Convert DBML tables to ChartDB table objects
|
||||
@@ -332,18 +426,40 @@ export const importDBMLToDiagram = async (
|
||||
const tableSpacing = 300;
|
||||
|
||||
// Create fields first so we have their IDs
|
||||
const fields = table.fields.map((field) => ({
|
||||
id: generateId(),
|
||||
name: field.name.replace(/['"]/g, ''),
|
||||
type: mapDBMLTypeToGenericType(field.type.type_name),
|
||||
nullable: !field.not_null,
|
||||
primaryKey: field.pk || false,
|
||||
unique: field.unique || false,
|
||||
createdAt: Date.now(),
|
||||
}));
|
||||
const fields: DBField[] = table.fields.map((field) => {
|
||||
// Extract field note/comment
|
||||
let fieldComment: string | undefined;
|
||||
if (field.note) {
|
||||
if (typeof field.note === 'string') {
|
||||
fieldComment = field.note;
|
||||
} else if (
|
||||
typeof field.note === 'object' &&
|
||||
'value' in field.note
|
||||
) {
|
||||
fieldComment = field.note.value;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: generateId(),
|
||||
name: field.name.replace(/['"]/g, ''),
|
||||
type: mapDBMLTypeToDataType(field.type.type_name, {
|
||||
...options,
|
||||
enums: extractedData.enums,
|
||||
}),
|
||||
nullable: !field.not_null,
|
||||
primaryKey: field.pk || false,
|
||||
unique: field.unique || false,
|
||||
createdAt: Date.now(),
|
||||
characterMaximumLength: field.characterMaximumLength,
|
||||
precision: field.precision,
|
||||
scale: field.scale,
|
||||
...(fieldComment ? { comments: fieldComment } : {}),
|
||||
};
|
||||
});
|
||||
|
||||
// Convert DBML indexes to ChartDB indexes
|
||||
const indexes =
|
||||
const indexes: DBIndex[] =
|
||||
table.indexes?.map((dbmlIndex) => {
|
||||
const fieldIds = dbmlIndex.columns.map((columnName) => {
|
||||
const field = fields.find((f) => f.name === columnName);
|
||||
@@ -395,7 +511,7 @@ export const importDBMLToDiagram = async (
|
||||
isView: false,
|
||||
createdAt: Date.now(),
|
||||
comments: tableComment,
|
||||
};
|
||||
} as DBTable;
|
||||
});
|
||||
|
||||
// Create relationships using the refs
|
||||
@@ -449,12 +565,43 @@ export const importDBMLToDiagram = async (
|
||||
}
|
||||
);
|
||||
|
||||
// Convert DBML enums to custom types
|
||||
const customTypes: DBCustomType[] = extractedData.enums.map(
|
||||
(enumDef) => {
|
||||
// Extract values from enum
|
||||
const values = enumDef.values
|
||||
.map((v) => {
|
||||
// Handle both string values and objects with name property
|
||||
if (typeof v === 'string') {
|
||||
return v;
|
||||
} else if (v && typeof v === 'object' && 'name' in v) {
|
||||
return v.name.replace(/["']/g, ''); // Remove quotes from values
|
||||
}
|
||||
return '';
|
||||
})
|
||||
.filter((v) => v !== '');
|
||||
|
||||
return {
|
||||
id: generateId(),
|
||||
schema:
|
||||
typeof enumDef.schema === 'string'
|
||||
? enumDef.schema
|
||||
: undefined,
|
||||
name: enumDef.name,
|
||||
kind: DBCustomTypeKind.enum,
|
||||
values,
|
||||
order: 0,
|
||||
} satisfies DBCustomType;
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
id: generateDiagramId(),
|
||||
name: 'DBML Import',
|
||||
databaseType: DatabaseType.GENERIC,
|
||||
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
|
||||
tables,
|
||||
relationships,
|
||||
customTypes,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export interface ChartDBConfig {
|
||||
defaultDiagramId: string;
|
||||
exportActions?: Date[];
|
||||
hiddenTablesByDiagram?: Record<string, string[]>; // Maps diagram ID to array of hidden table IDs
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
import type { ViewInfo } from '../data/import-metadata/metadata-types/view-info';
|
||||
import { DatabaseType } from './database-type';
|
||||
import {
|
||||
schemaNameToDomainSchemaName,
|
||||
schemaNameToSchemaId,
|
||||
} from './db-schema';
|
||||
import { schemaNameToDomainSchemaName } from './db-schema';
|
||||
import { decodeViewDefinition, type DBTable } from './db-table';
|
||||
import { generateId } from '@/lib/utils';
|
||||
import type { AST } from 'node-sql-parser';
|
||||
@@ -27,18 +24,6 @@ export const dbDependencySchema: z.ZodType<DBDependency> = z.object({
|
||||
createdAt: z.number(),
|
||||
});
|
||||
|
||||
export const shouldShowDependencyBySchemaFilter = (
|
||||
dependency: DBDependency,
|
||||
filteredSchemas?: string[]
|
||||
): boolean =>
|
||||
!filteredSchemas ||
|
||||
!dependency.schema ||
|
||||
!dependency.dependentSchema ||
|
||||
(filteredSchemas.includes(schemaNameToSchemaId(dependency.schema)) &&
|
||||
filteredSchemas.includes(
|
||||
schemaNameToSchemaId(dependency.dependentSchema)
|
||||
));
|
||||
|
||||
const astDatabaseTypes: Record<DatabaseType, string> = {
|
||||
[DatabaseType.POSTGRESQL]: 'postgresql',
|
||||
[DatabaseType.MYSQL]: 'postgresql',
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
import type { ForeignKeyInfo } from '../data/import-metadata/metadata-types/foreign-key-info';
|
||||
import type { DBField } from './db-field';
|
||||
import {
|
||||
schemaNameToDomainSchemaName,
|
||||
schemaNameToSchemaId,
|
||||
} from './db-schema';
|
||||
import { schemaNameToDomainSchemaName } from './db-schema';
|
||||
import type { DBTable } from './db-table';
|
||||
import { generateId } from '@/lib/utils';
|
||||
|
||||
@@ -43,20 +40,6 @@ export type RelationshipType =
|
||||
| 'many_to_many';
|
||||
export type Cardinality = 'one' | 'many';
|
||||
|
||||
export const shouldShowRelationshipBySchemaFilter = (
|
||||
relationship: DBRelationship,
|
||||
filteredSchemas?: string[]
|
||||
): boolean =>
|
||||
!filteredSchemas ||
|
||||
!relationship.sourceSchema ||
|
||||
!relationship.targetSchema ||
|
||||
(filteredSchemas.includes(
|
||||
schemaNameToSchemaId(relationship.sourceSchema)
|
||||
) &&
|
||||
filteredSchemas.includes(
|
||||
schemaNameToSchemaId(relationship.targetSchema)
|
||||
));
|
||||
|
||||
const determineCardinality = (
|
||||
field: DBField,
|
||||
isTablePKComplex: boolean
|
||||
|
||||
@@ -18,10 +18,7 @@ import {
|
||||
deepCopy,
|
||||
generateId,
|
||||
} from '../utils';
|
||||
import {
|
||||
schemaNameToDomainSchemaName,
|
||||
schemaNameToSchemaId,
|
||||
} from './db-schema';
|
||||
import { schemaNameToDomainSchemaName } from './db-schema';
|
||||
import { DatabaseType } from './database-type';
|
||||
import type { DatabaseMetadata } from '../data/import-metadata/metadata-types/database-metadata';
|
||||
import { z } from 'zod';
|
||||
@@ -77,26 +74,6 @@ export const generateTableKey = ({
|
||||
tableName: string;
|
||||
}) => `${schemaNameToDomainSchemaName(schemaName) ?? ''}.${tableName}`;
|
||||
|
||||
export const shouldShowTableSchemaBySchemaFilter = ({
|
||||
filteredSchemas,
|
||||
tableSchema,
|
||||
}: {
|
||||
tableSchema?: string | null;
|
||||
filteredSchemas?: string[];
|
||||
}): boolean =>
|
||||
!filteredSchemas ||
|
||||
!tableSchema ||
|
||||
filteredSchemas.includes(schemaNameToSchemaId(tableSchema));
|
||||
|
||||
export const shouldShowTablesBySchemaFilter = (
|
||||
table: DBTable,
|
||||
filteredSchemas?: string[]
|
||||
): boolean =>
|
||||
shouldShowTableSchemaBySchemaFilter({
|
||||
filteredSchemas,
|
||||
tableSchema: table?.schema,
|
||||
});
|
||||
|
||||
export const decodeViewDefinition = (
|
||||
databaseType: DatabaseType,
|
||||
viewDefinition?: string
|
||||
|
||||
147
src/lib/domain/diagram-filter/diagram-filter.ts
Normal file
147
src/lib/domain/diagram-filter/diagram-filter.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
// union logic filter
|
||||
export interface DiagramFilter {
|
||||
schemaIds?: string[];
|
||||
tableIds?: string[];
|
||||
}
|
||||
|
||||
export interface TableInfo {
|
||||
id: string;
|
||||
schemaId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces/optimizes a DiagramFilter by removing redundant entries
|
||||
* - Removes tableIds that belong to schemas already in schemaIds (union logic)
|
||||
* - Consolidates complete schemas: if all tables from a schema are in tableIds, adds the schema to schemaIds
|
||||
* - Returns undefined for both fields if everything is displayed
|
||||
* - Returns empty arrays if nothing should be displayed
|
||||
*/
|
||||
export function reduceFilter(
|
||||
filter: DiagramFilter,
|
||||
tables: TableInfo[]
|
||||
): DiagramFilter {
|
||||
let { schemaIds, tableIds } = filter;
|
||||
|
||||
// If no filters are defined, everything is visible
|
||||
if (!schemaIds && !tableIds) {
|
||||
return { schemaIds: undefined, tableIds: undefined };
|
||||
}
|
||||
|
||||
// Get all unique schema IDs from tables
|
||||
const allSchemaIds = [
|
||||
...new Set(tables.filter((t) => t.schemaId).map((t) => t.schemaId!)),
|
||||
];
|
||||
const allTableIds = tables.map((t) => t.id);
|
||||
|
||||
// in case its db with no schemas
|
||||
if (allSchemaIds.length === 0) {
|
||||
const tableSet = new Set(tableIds);
|
||||
if (tableSet.size === allTableIds.length) {
|
||||
return { schemaIds: undefined, tableIds: undefined };
|
||||
}
|
||||
|
||||
return { schemaIds: undefined, tableIds: Array.from(tableSet) };
|
||||
}
|
||||
|
||||
// Build a map of schema to its tables
|
||||
const schemaToTables = new Map<string, string[]>();
|
||||
tables.forEach((table) => {
|
||||
if (table.schemaId) {
|
||||
if (!schemaToTables.has(table.schemaId)) {
|
||||
schemaToTables.set(table.schemaId, []);
|
||||
}
|
||||
schemaToTables.get(table.schemaId)!.push(table.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Consolidate complete schemas: if all tables from a schema are in tableIds, add schema to schemaIds
|
||||
if (tableIds) {
|
||||
const tableSet = new Set(tableIds);
|
||||
const consolidatedSchemaIds = new Set(schemaIds || []);
|
||||
let consolidatedTableIds = [...tableIds];
|
||||
|
||||
for (const [schemaId, schemaTables] of schemaToTables.entries()) {
|
||||
// Check if all tables from this schema are in tableIds
|
||||
if (schemaTables.every((tableId) => tableSet.has(tableId))) {
|
||||
// Add schema to schemaIds
|
||||
consolidatedSchemaIds.add(schemaId);
|
||||
// Remove these tables from tableIds
|
||||
consolidatedTableIds = consolidatedTableIds.filter(
|
||||
(id) => !schemaTables.includes(id)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
schemaIds =
|
||||
consolidatedSchemaIds.size > 0
|
||||
? Array.from(consolidatedSchemaIds)
|
||||
: schemaIds;
|
||||
tableIds =
|
||||
consolidatedTableIds.length > 0 ? consolidatedTableIds : undefined;
|
||||
}
|
||||
|
||||
// If all schemas are in the filter, everything is visible
|
||||
if (schemaIds && schemaIds.length === allSchemaIds.length) {
|
||||
const schemasSet = new Set(schemaIds);
|
||||
const allSchemasIncluded = allSchemaIds.every((id) =>
|
||||
schemasSet.has(id)
|
||||
);
|
||||
if (allSchemasIncluded) {
|
||||
return { schemaIds: undefined, tableIds: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
// If schemaIds is defined, remove tables from tableIds that belong to those schemas
|
||||
let reducedTableIds = tableIds;
|
||||
if (schemaIds && tableIds) {
|
||||
const schemaSet = new Set(schemaIds);
|
||||
reducedTableIds = tableIds.filter((tableId) => {
|
||||
const table = tables.find((t) => t.id === tableId);
|
||||
// Keep table in tableIds only if it doesn't belong to a schema in schemaIds
|
||||
return !table?.schemaId || !schemaSet.has(table.schemaId);
|
||||
});
|
||||
|
||||
// If no tables remain after reduction, set to undefined
|
||||
if (reducedTableIds.length === 0) {
|
||||
reducedTableIds = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all tables are now visible (either through schemas or direct table IDs)
|
||||
if (schemaIds && reducedTableIds) {
|
||||
const schemaSet = new Set(schemaIds);
|
||||
const tableSet = new Set(reducedTableIds);
|
||||
|
||||
const visibleTables = tables.filter((table) => {
|
||||
// Table is visible if it's in tableIds OR its schema is in schemaIds
|
||||
return (
|
||||
tableSet.has(table.id) ||
|
||||
(table.schemaId && schemaSet.has(table.schemaId))
|
||||
);
|
||||
});
|
||||
|
||||
if (visibleTables.length === tables.length) {
|
||||
return { schemaIds: undefined, tableIds: undefined };
|
||||
}
|
||||
} else if (schemaIds && !reducedTableIds) {
|
||||
// Only schemaIds is defined, check if all tables are covered by schemas
|
||||
const schemaSet = new Set(schemaIds);
|
||||
const visibleTables = tables.filter(
|
||||
(table) => table.schemaId && schemaSet.has(table.schemaId)
|
||||
);
|
||||
|
||||
if (visibleTables.length === tables.length) {
|
||||
return { schemaIds: undefined, tableIds: undefined };
|
||||
}
|
||||
} else if (!schemaIds && reducedTableIds) {
|
||||
// Only tableIds is defined, check if all tables are in the filter
|
||||
if (reducedTableIds.length === allTableIds.length) {
|
||||
return { schemaIds: undefined, tableIds: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
schemaIds,
|
||||
tableIds: reducedTableIds,
|
||||
};
|
||||
}
|
||||
114
src/lib/domain/diagram-filter/filter.ts
Normal file
114
src/lib/domain/diagram-filter/filter.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { schemaNameToSchemaId } from '../db-schema';
|
||||
import type { DiagramFilter } from './diagram-filter';
|
||||
|
||||
export const filterTable = ({
|
||||
table,
|
||||
filter,
|
||||
options = { defaultSchema: undefined },
|
||||
}: {
|
||||
table: { id: string; schema?: string | null };
|
||||
filter?: DiagramFilter;
|
||||
options?: {
|
||||
defaultSchema?: string;
|
||||
};
|
||||
}): boolean => {
|
||||
if (!filter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!filter.tableIds && !filter.schemaIds) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (filter.tableIds && filter.tableIds.includes(table.id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const tableSchema = table.schema ?? options.defaultSchema;
|
||||
|
||||
if (
|
||||
tableSchema &&
|
||||
filter.schemaIds &&
|
||||
filter.schemaIds.includes(schemaNameToSchemaId(tableSchema))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const filterTableBySchema = ({
|
||||
table,
|
||||
schemaIdsFilter,
|
||||
options = { defaultSchema: undefined },
|
||||
}: {
|
||||
table: { id: string; schema?: string | null };
|
||||
schemaIdsFilter?: string[];
|
||||
options?: {
|
||||
defaultSchema?: string;
|
||||
};
|
||||
}): boolean => {
|
||||
if (!schemaIdsFilter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const tableSchemaId = table.schema ?? options.defaultSchema;
|
||||
|
||||
if (tableSchemaId) {
|
||||
return schemaIdsFilter.includes(schemaNameToSchemaId(tableSchemaId));
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const filterSchema = ({
|
||||
schemaId,
|
||||
schemaIdsFilter,
|
||||
}: {
|
||||
schemaId?: string;
|
||||
schemaIdsFilter?: string[];
|
||||
}): boolean => {
|
||||
if (!schemaIdsFilter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!schemaId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return schemaIdsFilter.includes(schemaId);
|
||||
};
|
||||
|
||||
export const filterRelationship = ({
|
||||
tableA: { id: tableAId, schema: tableASchema },
|
||||
tableB: { id: tableBId, schema: tableBSchema },
|
||||
filter,
|
||||
options = { defaultSchema: undefined },
|
||||
}: {
|
||||
tableA: { id: string; schema?: string | null };
|
||||
tableB: { id: string; schema?: string | null };
|
||||
filter?: DiagramFilter;
|
||||
options?: {
|
||||
defaultSchema?: string;
|
||||
};
|
||||
}): boolean => {
|
||||
if (!filter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isTableAVisible = filterTable({
|
||||
table: { id: tableAId, schema: tableASchema },
|
||||
filter,
|
||||
options,
|
||||
});
|
||||
|
||||
const isTableBVisible = filterTable({
|
||||
table: { id: tableBId, schema: tableBSchema },
|
||||
filter,
|
||||
options,
|
||||
});
|
||||
|
||||
return isTableAVisible && isTableBVisible;
|
||||
};
|
||||
|
||||
export const filterDependency = filterRelationship;
|
||||
@@ -329,6 +329,27 @@ function compareFieldProperties({
|
||||
changedAttributes.push('comments');
|
||||
}
|
||||
|
||||
if (
|
||||
(newField.characterMaximumLength || oldField.characterMaximumLength) &&
|
||||
oldField.characterMaximumLength !== newField.characterMaximumLength
|
||||
) {
|
||||
changedAttributes.push('characterMaximumLength');
|
||||
}
|
||||
|
||||
if (
|
||||
(newField.scale || oldField.scale) &&
|
||||
oldField.scale !== newField.scale
|
||||
) {
|
||||
changedAttributes.push('scale');
|
||||
}
|
||||
|
||||
if (
|
||||
(newField.precision || oldField.precision) &&
|
||||
oldField.precision !== newField.precision
|
||||
) {
|
||||
changedAttributes.push('precision');
|
||||
}
|
||||
|
||||
if (changedAttributes.length > 0) {
|
||||
for (const attribute of changedAttributes) {
|
||||
diffMap.set(
|
||||
|
||||
@@ -12,7 +12,10 @@ export type FieldDiffAttribute =
|
||||
| 'primaryKey'
|
||||
| 'unique'
|
||||
| 'nullable'
|
||||
| 'comments';
|
||||
| 'comments'
|
||||
| 'characterMaximumLength'
|
||||
| 'precision'
|
||||
| 'scale';
|
||||
|
||||
export const fieldDiffAttributeSchema: z.ZodType<FieldDiffAttribute> = z.union([
|
||||
z.literal('name'),
|
||||
@@ -61,8 +64,8 @@ export interface FieldDiffChanged {
|
||||
fieldId: string;
|
||||
tableId: string;
|
||||
attribute: FieldDiffAttribute;
|
||||
oldValue: string | boolean | DataType;
|
||||
newValue: string | boolean | DataType;
|
||||
oldValue: string | boolean | DataType | number;
|
||||
newValue: string | boolean | DataType | number;
|
||||
}
|
||||
|
||||
export const fieldDiffChangedSchema: z.ZodType<FieldDiffChanged> = z.object({
|
||||
|
||||
@@ -80,7 +80,7 @@ export const AreaNode: React.FC<NodeProps<AreaNodeType>> = React.memo(
|
||||
<NodeResizer
|
||||
isVisible={focused}
|
||||
lineClassName="!border-4 !border-transparent"
|
||||
handleClassName="!h-[18px] !w-[18px] !rounded-full !bg-pink-600"
|
||||
handleClassName="!h-[10px] !w-[10px] !rounded-full !bg-pink-600"
|
||||
minHeight={100}
|
||||
minWidth={100}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { Area } from '@/lib/domain/area';
|
||||
import { calcTableHeight } from '@/lib/domain/db-table';
|
||||
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
import { filterTable } from '@/lib/domain/diagram-filter/filter';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
import { DatabaseType } from '@/lib/domain/database-type';
|
||||
|
||||
/**
|
||||
* Check if a table is inside an area based on their positions and dimensions
|
||||
@@ -30,16 +34,54 @@ const isTableInsideArea = (table: DBTable, area: Area): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if an area is visible based on its tables
|
||||
*/
|
||||
const isAreaVisible = (
|
||||
area: Area,
|
||||
tables: DBTable[],
|
||||
filter?: DiagramFilter,
|
||||
databaseType?: DatabaseType
|
||||
): boolean => {
|
||||
const tablesInArea = tables.filter((t) => t.parentAreaId === area.id);
|
||||
|
||||
// If area has no tables, consider it visible
|
||||
if (tablesInArea.length === 0) return true;
|
||||
|
||||
// Area is visible if at least one table in it is visible
|
||||
return tablesInArea.some((table) =>
|
||||
filterTable({
|
||||
table: { id: table.id, schema: table.schema },
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema:
|
||||
defaultSchemas[databaseType || DatabaseType.GENERIC],
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find which area contains a table
|
||||
*/
|
||||
const findContainingArea = (table: DBTable, areas: Area[]): Area | null => {
|
||||
const findContainingArea = (
|
||||
table: DBTable,
|
||||
areas: Area[],
|
||||
tables: DBTable[],
|
||||
filter?: DiagramFilter,
|
||||
databaseType?: DatabaseType
|
||||
): Area | null => {
|
||||
// Sort areas by order (if available) to prioritize top-most areas
|
||||
const sortedAreas = [...areas].sort(
|
||||
(a, b) => (b.order ?? 0) - (a.order ?? 0)
|
||||
);
|
||||
|
||||
for (const area of sortedAreas) {
|
||||
// Skip hidden areas - they shouldn't capture tables
|
||||
if (!isAreaVisible(area, tables, filter, databaseType)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isTableInsideArea(table, area)) {
|
||||
return area;
|
||||
}
|
||||
@@ -53,10 +95,33 @@ const findContainingArea = (table: DBTable, areas: Area[]): Area | null => {
|
||||
*/
|
||||
export const updateTablesParentAreas = (
|
||||
tables: DBTable[],
|
||||
areas: Area[]
|
||||
areas: Area[],
|
||||
filter?: DiagramFilter,
|
||||
databaseType?: DatabaseType
|
||||
): DBTable[] => {
|
||||
return tables.map((table) => {
|
||||
const containingArea = findContainingArea(table, areas);
|
||||
// Skip hidden tables - they shouldn't be assigned to areas
|
||||
const isTableVisible = filterTable({
|
||||
table: { id: table.id, schema: table.schema },
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema:
|
||||
defaultSchemas[databaseType || DatabaseType.GENERIC],
|
||||
},
|
||||
});
|
||||
|
||||
if (!isTableVisible) {
|
||||
// Hidden tables keep their current parent area (don't change)
|
||||
return table;
|
||||
}
|
||||
|
||||
const containingArea = findContainingArea(
|
||||
table,
|
||||
areas,
|
||||
tables,
|
||||
filter,
|
||||
databaseType
|
||||
);
|
||||
const newParentAreaId = containingArea?.id || null;
|
||||
|
||||
// Only update if parentAreaId has changed
|
||||
@@ -80,3 +145,26 @@ export const getTablesInArea = (
|
||||
): DBTable[] => {
|
||||
return tables.filter((table) => table.parentAreaId === areaId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get visible tables that are inside a specific area
|
||||
*/
|
||||
export const getVisibleTablesInArea = (
|
||||
areaId: string,
|
||||
tables: DBTable[],
|
||||
filter?: DiagramFilter,
|
||||
databaseType?: DatabaseType
|
||||
): DBTable[] => {
|
||||
return tables.filter((table) => {
|
||||
if (table.parentAreaId !== areaId) return false;
|
||||
|
||||
return filterTable({
|
||||
table: { id: table.id, schema: table.schema },
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema:
|
||||
defaultSchemas[databaseType || DatabaseType.GENERIC],
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -11,12 +11,13 @@ import { useReactFlow } from '@xyflow/react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Table, Workflow, Group } from 'lucide-react';
|
||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||
|
||||
export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { createTable, filteredSchemas, schemas, readonly, createArea } =
|
||||
useChartDB();
|
||||
const { createTable, readonly, createArea } = useChartDB();
|
||||
const { schemasDisplayed } = useDiagramFilter();
|
||||
const { openCreateRelationshipDialog, openTableSchemaDialog } = useDialog();
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const { t } = useTranslation();
|
||||
@@ -30,7 +31,7 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
||||
y: event.clientY,
|
||||
});
|
||||
|
||||
if ((filteredSchemas?.length ?? 0) > 1) {
|
||||
if (schemasDisplayed.length > 1) {
|
||||
openTableSchemaDialog({
|
||||
onConfirm: ({ schema }) =>
|
||||
createTable({
|
||||
@@ -38,14 +39,12 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
||||
y: position.y,
|
||||
schema: schema.name,
|
||||
}),
|
||||
schemas: schemas.filter((schema) =>
|
||||
filteredSchemas?.includes(schema.id)
|
||||
),
|
||||
schemas: schemasDisplayed,
|
||||
});
|
||||
} else {
|
||||
const schema =
|
||||
filteredSchemas?.length === 1
|
||||
? schemas.find((s) => s.id === filteredSchemas[0])?.name
|
||||
schemasDisplayed?.length === 1
|
||||
? schemasDisplayed[0]?.name
|
||||
: undefined;
|
||||
createTable({
|
||||
x: position.x,
|
||||
@@ -58,8 +57,7 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
||||
createTable,
|
||||
screenToFlowPosition,
|
||||
openTableSchemaDialog,
|
||||
schemas,
|
||||
filteredSchemas,
|
||||
schemasDisplayed,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -82,13 +80,15 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
|
||||
openCreateRelationshipDialog();
|
||||
}, [openCreateRelationshipDialog]);
|
||||
|
||||
if (!isDesktop || readonly) {
|
||||
if (!isDesktop) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
||||
<ContextMenuTrigger disabled={readonly}>
|
||||
{children}
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem
|
||||
onClick={createTableHandler}
|
||||
|
||||
@@ -5,33 +5,49 @@ import React, {
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { X, Search, Eye, EyeOff, Database, Table, Funnel } from 'lucide-react';
|
||||
import {
|
||||
X,
|
||||
Search,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Database,
|
||||
Table,
|
||||
Funnel,
|
||||
Layers,
|
||||
Box,
|
||||
} from 'lucide-react';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/button/button';
|
||||
import { Input } from '@/components/input/input';
|
||||
import { shouldShowTableSchemaBySchemaFilter } from '@/lib/domain/db-table';
|
||||
import { schemaNameToSchemaId } from '@/lib/domain/db-schema';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
import { useReactFlow } from '@xyflow/react';
|
||||
import { TreeView } from '@/components/tree-view/tree-view';
|
||||
import type { TreeNode } from '@/components/tree-view/tree';
|
||||
import { ScrollArea } from '@/components/scroll-area/scroll-area';
|
||||
import { filterSchema, filterTable } from '@/lib/domain/diagram-filter/filter';
|
||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
|
||||
|
||||
export interface CanvasFilterProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
type NodeType = 'schema' | 'table';
|
||||
type NodeType = 'schema' | 'area' | 'table';
|
||||
type GroupingMode = 'schema' | 'area';
|
||||
|
||||
type SchemaContext = { name: string };
|
||||
type SchemaContext = { name: string; visible: boolean };
|
||||
type AreaContext = { id: string; name: string; visible: boolean };
|
||||
type TableContext = {
|
||||
tableSchema?: string | null;
|
||||
hidden: boolean;
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
type NodeContext = {
|
||||
schema: SchemaContext;
|
||||
area: AreaContext;
|
||||
table: TableContext;
|
||||
};
|
||||
|
||||
@@ -43,19 +59,19 @@ type RelevantTableData = {
|
||||
|
||||
export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
const { t } = useTranslation();
|
||||
const { tables, databaseType, areas } = useChartDB();
|
||||
const {
|
||||
tables,
|
||||
databaseType,
|
||||
hiddenTableIds,
|
||||
addHiddenTableId,
|
||||
removeHiddenTableId,
|
||||
filteredSchemas,
|
||||
filterSchemas,
|
||||
} = useChartDB();
|
||||
filter,
|
||||
toggleSchemaFilter,
|
||||
toggleTableFilter,
|
||||
clearTableIdsFilter,
|
||||
setTableIdsFilterEmpty,
|
||||
} = useDiagramFilter();
|
||||
const { fitView, setNodes } = useReactFlow();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [expanded, setExpanded] = useState<Record<string, boolean>>({});
|
||||
const [isFilterVisible, setIsFilterVisible] = useState(false);
|
||||
const [groupingMode, setGroupingMode] = useState<GroupingMode>('schema');
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Extract only the properties needed for tree data
|
||||
@@ -69,72 +85,262 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
[tables]
|
||||
);
|
||||
|
||||
const databaseWithSchemas = useMemo(
|
||||
() => !!defaultSchemas[databaseType],
|
||||
[databaseType]
|
||||
);
|
||||
|
||||
// Convert tables to tree nodes
|
||||
const treeData = useMemo(() => {
|
||||
// Group tables by schema
|
||||
const tablesBySchema = new Map<string, RelevantTableData[]>();
|
||||
|
||||
relevantTableData.forEach((table) => {
|
||||
const schema =
|
||||
table.schema ?? defaultSchemas[databaseType] ?? 'default';
|
||||
if (!tablesBySchema.has(schema)) {
|
||||
tablesBySchema.set(schema, []);
|
||||
}
|
||||
tablesBySchema.get(schema)!.push(table);
|
||||
});
|
||||
|
||||
// Sort tables within each schema
|
||||
tablesBySchema.forEach((tables) => {
|
||||
tables.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
// Convert to tree nodes
|
||||
const nodes: TreeNode<NodeType, NodeContext>[] = [];
|
||||
|
||||
tablesBySchema.forEach((schemaTables, schemaName) => {
|
||||
const schemaId = schemaNameToSchemaId(schemaName);
|
||||
const schemaHidden = filteredSchemas
|
||||
? !filteredSchemas.includes(schemaId)
|
||||
: false;
|
||||
const schemaNode: TreeNode<NodeType, NodeContext> = {
|
||||
id: `schema-${schemaName}`,
|
||||
name: `${schemaName} (${schemaTables.length})`,
|
||||
type: 'schema',
|
||||
isFolder: true,
|
||||
icon: Database,
|
||||
context: { name: schemaName },
|
||||
className: schemaHidden ? 'opacity-50' : '',
|
||||
children: schemaTables.map(
|
||||
(table): TreeNode<NodeType, NodeContext> => {
|
||||
const tableHidden =
|
||||
hiddenTableIds?.includes(table.id) ?? false;
|
||||
const visibleBySchema =
|
||||
shouldShowTableSchemaBySchemaFilter({
|
||||
tableSchema: table.schema,
|
||||
filteredSchemas,
|
||||
});
|
||||
const hidden = tableHidden || !visibleBySchema;
|
||||
if (groupingMode === 'area') {
|
||||
// Group tables by area
|
||||
const tablesByArea = new Map<string | null, DBTable[]>();
|
||||
const tablesWithoutArea: DBTable[] = [];
|
||||
|
||||
return {
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
type: 'table',
|
||||
isFolder: false,
|
||||
icon: Table,
|
||||
context: {
|
||||
tableSchema: table.schema,
|
||||
hidden: tableHidden,
|
||||
},
|
||||
className: hidden ? 'opacity-50' : '',
|
||||
};
|
||||
tables.forEach((table) => {
|
||||
if (table.parentAreaId) {
|
||||
if (!tablesByArea.has(table.parentAreaId)) {
|
||||
tablesByArea.set(table.parentAreaId, []);
|
||||
}
|
||||
),
|
||||
};
|
||||
nodes.push(schemaNode);
|
||||
});
|
||||
tablesByArea.get(table.parentAreaId)!.push(table);
|
||||
} else {
|
||||
tablesWithoutArea.push(table);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort tables within each area
|
||||
tablesByArea.forEach((areaTables) => {
|
||||
areaTables.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
tablesWithoutArea.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Create nodes for areas
|
||||
areas.forEach((area) => {
|
||||
const areaTables = tablesByArea.get(area.id) || [];
|
||||
|
||||
// Check if at least one table in the area is visible
|
||||
const areaVisible =
|
||||
areaTables.length === 0 ||
|
||||
areaTables.some((table) =>
|
||||
filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const areaNode: TreeNode<NodeType, NodeContext> = {
|
||||
id: `area-${area.id}`,
|
||||
name: `${area.name} (${areaTables.length})`,
|
||||
type: 'area',
|
||||
isFolder: true,
|
||||
icon: Box,
|
||||
context: {
|
||||
id: area.id,
|
||||
name: area.name,
|
||||
visible: areaVisible,
|
||||
} as AreaContext,
|
||||
className: !areaVisible ? 'opacity-50' : '',
|
||||
children: areaTables.map(
|
||||
(table): TreeNode<NodeType, NodeContext> => {
|
||||
const tableVisible = filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
type: 'table',
|
||||
isFolder: false,
|
||||
icon: Table,
|
||||
context: {
|
||||
tableSchema: table.schema,
|
||||
visible: tableVisible,
|
||||
} as TableContext,
|
||||
className: !tableVisible ? 'opacity-50' : '',
|
||||
};
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
if (areaTables.length > 0) {
|
||||
nodes.push(areaNode);
|
||||
}
|
||||
});
|
||||
|
||||
// Add ungrouped tables
|
||||
if (tablesWithoutArea.length > 0) {
|
||||
const ungroupedVisible = tablesWithoutArea.some((table) =>
|
||||
filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const ungroupedNode: TreeNode<NodeType, NodeContext> = {
|
||||
id: 'ungrouped',
|
||||
name: `Ungrouped (${tablesWithoutArea.length})`,
|
||||
type: 'area',
|
||||
isFolder: true,
|
||||
icon: Layers,
|
||||
context: {
|
||||
id: 'ungrouped',
|
||||
name: 'Ungrouped',
|
||||
visible: ungroupedVisible,
|
||||
} as AreaContext,
|
||||
className: !ungroupedVisible ? 'opacity-50' : '',
|
||||
children: tablesWithoutArea.map(
|
||||
(table): TreeNode<NodeType, NodeContext> => {
|
||||
const tableVisible = filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
type: 'table',
|
||||
isFolder: false,
|
||||
icon: Table,
|
||||
context: {
|
||||
tableSchema: table.schema,
|
||||
visible: tableVisible,
|
||||
} as TableContext,
|
||||
className: !tableVisible ? 'opacity-50' : '',
|
||||
};
|
||||
}
|
||||
),
|
||||
};
|
||||
nodes.push(ungroupedNode);
|
||||
}
|
||||
} else {
|
||||
// Group tables by schema (existing logic)
|
||||
const tablesBySchema = new Map<string, RelevantTableData[]>();
|
||||
|
||||
relevantTableData.forEach((table) => {
|
||||
const schema = !databaseWithSchemas
|
||||
? 'All Tables'
|
||||
: (table.schema ??
|
||||
defaultSchemas[databaseType] ??
|
||||
'default');
|
||||
|
||||
if (!tablesBySchema.has(schema)) {
|
||||
tablesBySchema.set(schema, []);
|
||||
}
|
||||
tablesBySchema.get(schema)!.push(table);
|
||||
});
|
||||
|
||||
// Sort tables within each schema
|
||||
tablesBySchema.forEach((tables) => {
|
||||
tables.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
tablesBySchema.forEach((schemaTables, schemaName) => {
|
||||
let schemaVisible;
|
||||
|
||||
if (databaseWithSchemas) {
|
||||
const schemaId = schemaNameToSchemaId(schemaName);
|
||||
schemaVisible = filterSchema({
|
||||
schemaId,
|
||||
schemaIdsFilter: filter?.schemaIds,
|
||||
});
|
||||
} else {
|
||||
// if at least one table is visible, the schema is considered visible
|
||||
schemaVisible = schemaTables.some((table) =>
|
||||
filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const schemaNode: TreeNode<NodeType, NodeContext> = {
|
||||
id: `schema-${schemaName}`,
|
||||
name: `${schemaName} (${schemaTables.length})`,
|
||||
type: 'schema',
|
||||
isFolder: true,
|
||||
icon: Database,
|
||||
context: {
|
||||
name: schemaName,
|
||||
visible: schemaVisible,
|
||||
} as SchemaContext,
|
||||
className: !schemaVisible ? 'opacity-50' : '',
|
||||
children: schemaTables.map(
|
||||
(table): TreeNode<NodeType, NodeContext> => {
|
||||
const tableVisible = filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
});
|
||||
|
||||
const hidden = !tableVisible;
|
||||
|
||||
return {
|
||||
id: table.id,
|
||||
name: table.name,
|
||||
type: 'table',
|
||||
isFolder: false,
|
||||
icon: Table,
|
||||
context: {
|
||||
tableSchema: table.schema,
|
||||
visible: tableVisible,
|
||||
} as TableContext,
|
||||
className: hidden ? 'opacity-50' : '',
|
||||
};
|
||||
}
|
||||
),
|
||||
};
|
||||
nodes.push(schemaNode);
|
||||
});
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}, [relevantTableData, databaseType, hiddenTableIds, filteredSchemas]);
|
||||
}, [
|
||||
relevantTableData,
|
||||
tables,
|
||||
databaseType,
|
||||
filter,
|
||||
databaseWithSchemas,
|
||||
groupingMode,
|
||||
areas,
|
||||
]);
|
||||
|
||||
// Initialize expanded state with all schemas expanded
|
||||
useMemo(() => {
|
||||
@@ -170,17 +376,6 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
return result;
|
||||
}, [treeData, searchQuery]);
|
||||
|
||||
const toggleTableVisibility = useCallback(
|
||||
async (tableId: string, hidden: boolean) => {
|
||||
if (hidden) {
|
||||
await addHiddenTableId(tableId);
|
||||
} else {
|
||||
await removeHiddenTableId(tableId);
|
||||
}
|
||||
},
|
||||
[addHiddenTableId, removeHiddenTableId]
|
||||
);
|
||||
|
||||
const focusOnTable = useCallback(
|
||||
(tableId: string) => {
|
||||
// Make sure the table is visible
|
||||
@@ -220,11 +415,12 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
const renderActions = useCallback(
|
||||
(node: TreeNode<NodeType, NodeContext>) => {
|
||||
if (node.type === 'schema') {
|
||||
const schemaContext = node.context as SchemaContext;
|
||||
const schemaId = schemaNameToSchemaId(schemaContext.name);
|
||||
const schemaHidden = filteredSchemas
|
||||
? !filteredSchemas.includes(schemaId)
|
||||
: false;
|
||||
const context = node.context as SchemaContext;
|
||||
const schemaVisible = context.visible;
|
||||
const schemaName = context.name;
|
||||
if (!schemaName) return null;
|
||||
|
||||
const schemaId = schemaNameToSchemaId(schemaName);
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -233,30 +429,93 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
className="size-7 h-fit p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// unhide all tables in this schema
|
||||
node.children?.forEach((child) => {
|
||||
if (
|
||||
child.type === 'table' &&
|
||||
hiddenTableIds?.includes(child.id)
|
||||
) {
|
||||
removeHiddenTableId(child.id);
|
||||
}
|
||||
});
|
||||
if (schemaHidden) {
|
||||
filterSchemas([
|
||||
...(filteredSchemas ?? []),
|
||||
schemaId,
|
||||
]);
|
||||
|
||||
if (databaseWithSchemas) {
|
||||
toggleSchemaFilter(schemaId);
|
||||
} else {
|
||||
filterSchemas(
|
||||
filteredSchemas?.filter(
|
||||
(s) => s !== schemaId
|
||||
) ?? []
|
||||
);
|
||||
// Toggle visibility of all tables in this schema
|
||||
if (schemaVisible) {
|
||||
setTableIdsFilterEmpty();
|
||||
} else {
|
||||
clearTableIdsFilter();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{schemaHidden ? (
|
||||
{!schemaVisible ? (
|
||||
<EyeOff className="size-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<Eye className="size-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === 'area') {
|
||||
const context = node.context as AreaContext;
|
||||
const areaVisible = context.visible;
|
||||
const areaId = context.id;
|
||||
if (!areaId) return null;
|
||||
|
||||
// Get all tables in this area
|
||||
const areaTables =
|
||||
areaId === 'ungrouped'
|
||||
? tables.filter((t) => !t.parentAreaId)
|
||||
: tables.filter((t) => t.parentAreaId === areaId);
|
||||
const tableIds = areaTables.map((t) => t.id);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="size-7 h-fit p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// Toggle all tables in this area
|
||||
if (areaVisible) {
|
||||
// Hide all tables in this area
|
||||
tableIds.forEach((id) => {
|
||||
const isVisible = filterTable({
|
||||
table: {
|
||||
id,
|
||||
schema: tables.find(
|
||||
(t) => t.id === id
|
||||
)?.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema:
|
||||
defaultSchemas[databaseType],
|
||||
},
|
||||
});
|
||||
if (isVisible) {
|
||||
toggleTableFilter(id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Show all tables in this area
|
||||
tableIds.forEach((id) => {
|
||||
const isVisible = filterTable({
|
||||
table: {
|
||||
id,
|
||||
schema: tables.find(
|
||||
(t) => t.id === id
|
||||
)?.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema:
|
||||
defaultSchemas[databaseType],
|
||||
},
|
||||
});
|
||||
if (!isVisible) {
|
||||
toggleTableFilter(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!areaVisible ? (
|
||||
<EyeOff className="size-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<Eye className="size-3.5" />
|
||||
@@ -267,14 +526,8 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
|
||||
if (node.type === 'table') {
|
||||
const tableId = node.id;
|
||||
const tableContext = node.context as TableContext;
|
||||
const hidden = tableContext.hidden;
|
||||
const tableSchema = tableContext.tableSchema;
|
||||
|
||||
const visibleBySchema = shouldShowTableSchemaBySchemaFilter({
|
||||
tableSchema,
|
||||
filteredSchemas,
|
||||
});
|
||||
const context = node.context as TableContext;
|
||||
const tableVisible = context.visible;
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -283,35 +536,10 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
className="size-7 h-fit p-0"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (!visibleBySchema && tableSchema) {
|
||||
// Unhide schema and hide all other tables
|
||||
const schemaId =
|
||||
schemaNameToSchemaId(tableSchema);
|
||||
filterSchemas([
|
||||
...(filteredSchemas ?? []),
|
||||
schemaId,
|
||||
]);
|
||||
const schemaNode = treeData.find(
|
||||
(s) =>
|
||||
(s.context as SchemaContext).name ===
|
||||
tableSchema
|
||||
);
|
||||
if (schemaNode) {
|
||||
schemaNode.children?.forEach((child) => {
|
||||
if (
|
||||
child.id !== tableId &&
|
||||
!hiddenTableIds?.includes(child.id)
|
||||
) {
|
||||
addHiddenTableId(child.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
toggleTableVisibility(tableId, !hidden);
|
||||
}
|
||||
toggleTableFilter(tableId);
|
||||
}}
|
||||
>
|
||||
{hidden || !visibleBySchema ? (
|
||||
{!tableVisible ? (
|
||||
<EyeOff className="size-3.5 text-muted-foreground" />
|
||||
) : (
|
||||
<Eye className="size-3.5" />
|
||||
@@ -323,13 +551,14 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
return null;
|
||||
},
|
||||
[
|
||||
toggleTableVisibility,
|
||||
filteredSchemas,
|
||||
filterSchemas,
|
||||
treeData,
|
||||
hiddenTableIds,
|
||||
addHiddenTableId,
|
||||
removeHiddenTableId,
|
||||
toggleSchemaFilter,
|
||||
toggleTableFilter,
|
||||
clearTableIdsFilter,
|
||||
setTableIdsFilterEmpty,
|
||||
databaseWithSchemas,
|
||||
tables,
|
||||
filter,
|
||||
databaseType,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -337,20 +566,16 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
const handleNodeClick = useCallback(
|
||||
(node: TreeNode<NodeType, NodeContext>) => {
|
||||
if (node.type === 'table') {
|
||||
const tableContext = node.context as TableContext;
|
||||
const tableSchema = tableContext.tableSchema;
|
||||
const visibleBySchema = shouldShowTableSchemaBySchemaFilter({
|
||||
tableSchema,
|
||||
filteredSchemas,
|
||||
});
|
||||
const context = node.context as TableContext;
|
||||
const isTableVisible = context.visible;
|
||||
|
||||
// Only focus if neither table is hidden nor filtered by schema
|
||||
if (!tableContext.hidden && visibleBySchema) {
|
||||
// Only focus if table is visible
|
||||
if (isTableVisible) {
|
||||
focusOnTable(node.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
[focusOnTable, filteredSchemas]
|
||||
[focusOnTable]
|
||||
);
|
||||
|
||||
// Animate in on mount and focus search input
|
||||
@@ -405,13 +630,34 @@ export const CanvasFilter: React.FC<CanvasFilterProps> = ({ onClose }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grouping Toggle */}
|
||||
<div className="border-b p-2">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={groupingMode}
|
||||
onValueChange={(value) => {
|
||||
if (value) setGroupingMode(value as GroupingMode);
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
<ToggleGroupItem value="schema" className="flex-1 text-xs">
|
||||
<Database className="mr-1.5 size-3.5" />
|
||||
{t('canvas_filter.group_by_schema', 'Group by Schema')}
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="area" className="flex-1 text-xs">
|
||||
<Box className="mr-1.5 size-3.5" />
|
||||
{t('canvas_filter.group_by_area', 'Group by Area')}
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
|
||||
{/* Table Tree */}
|
||||
<ScrollArea className="flex-1 rounded-b-lg" type="auto">
|
||||
<TreeView
|
||||
data={filteredTreeData}
|
||||
onNodeClick={handleNodeClick}
|
||||
renderActionsComponent={renderActions}
|
||||
defaultFolderIcon={Database}
|
||||
defaultFolderIcon={groupingMode === 'area' ? Box : Database}
|
||||
defaultIcon={Table}
|
||||
expanded={expanded}
|
||||
setExpanded={setExpanded}
|
||||
|
||||
@@ -54,10 +54,7 @@ import { Badge } from '@/components/badge/badge';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import {
|
||||
MIN_TABLE_SIZE,
|
||||
shouldShowTablesBySchemaFilter,
|
||||
} from '@/lib/domain/db-table';
|
||||
import { MIN_TABLE_SIZE } from '@/lib/domain/db-table';
|
||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -89,11 +86,15 @@ 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';
|
||||
import { updateTablesParentAreas, getTablesInArea } from './area-utils';
|
||||
import { updateTablesParentAreas, getVisibleTablesInArea } from './area-utils';
|
||||
import { CanvasFilter } from './canvas-filter/canvas-filter';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { ShowAllButton } from './show-all-button';
|
||||
import { useIsLostInCanvas } from './hooks/use-is-lost-in-canvas';
|
||||
import type { DiagramFilter } from '@/lib/domain/diagram-filter/diagram-filter';
|
||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||
import { filterTable } from '@/lib/domain/diagram-filter/filter';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
|
||||
const HIGHLIGHTED_EDGE_Z_INDEX = 1;
|
||||
const DEFAULT_EDGE_Z_INDEX = 0;
|
||||
@@ -118,13 +119,8 @@ const initialEdges: EdgeType[] = [];
|
||||
|
||||
const tableToTableNode = (
|
||||
table: DBTable,
|
||||
{
|
||||
filteredSchemas,
|
||||
hiddenTableIds,
|
||||
}: {
|
||||
filteredSchemas?: string[];
|
||||
hiddenTableIds?: string[];
|
||||
}
|
||||
filter: DiagramFilter | undefined,
|
||||
databaseType: DatabaseType
|
||||
): TableNodeType => {
|
||||
// Always use absolute position for now
|
||||
const position = { x: table.x, y: table.y };
|
||||
@@ -138,21 +134,48 @@ const tableToTableNode = (
|
||||
isOverlapping: false,
|
||||
},
|
||||
width: table.width ?? MIN_TABLE_SIZE,
|
||||
hidden:
|
||||
!shouldShowTablesBySchemaFilter(table, filteredSchemas) ||
|
||||
(hiddenTableIds?.includes(table.id) ?? false),
|
||||
hidden: !filterTable({
|
||||
table: { id: table.id, schema: table.schema },
|
||||
filter,
|
||||
options: { defaultSchema: defaultSchemas[databaseType] },
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
const areaToAreaNode = (
|
||||
area: Area,
|
||||
tables: DBTable[],
|
||||
filter?: DiagramFilter,
|
||||
databaseType?: DatabaseType
|
||||
): AreaNodeType => {
|
||||
// Get all tables in this area
|
||||
const tablesInArea = tables.filter((t) => t.parentAreaId === area.id);
|
||||
|
||||
// Check if at least one table in the area is visible
|
||||
const hasVisibleTable =
|
||||
tablesInArea.length === 0 ||
|
||||
tablesInArea.some((table) =>
|
||||
filterTable({
|
||||
table: { id: table.id, schema: table.schema },
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema:
|
||||
defaultSchemas[databaseType || DatabaseType.GENERIC],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
id: area.id,
|
||||
type: 'area',
|
||||
position: { x: area.x, y: area.y },
|
||||
data: { area },
|
||||
width: area.width,
|
||||
height: area.height,
|
||||
zIndex: -10,
|
||||
hidden: !hasVisibleTable,
|
||||
};
|
||||
};
|
||||
|
||||
export interface CanvasProps {
|
||||
initialTables: DBTable[];
|
||||
@@ -178,7 +201,6 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
removeDependencies,
|
||||
getField,
|
||||
databaseType,
|
||||
filteredSchemas,
|
||||
events,
|
||||
dependencies,
|
||||
readonly,
|
||||
@@ -186,7 +208,6 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
updateArea,
|
||||
highlightedCustomType,
|
||||
highlightCustomTypeId,
|
||||
hiddenTableIds,
|
||||
} = useChartDB();
|
||||
const { showSidePanel } = useLayout();
|
||||
const { effectiveTheme } = useTheme();
|
||||
@@ -204,12 +225,13 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
showFilter,
|
||||
setShowFilter,
|
||||
} = useCanvas();
|
||||
const { filter } = useDiagramFilter();
|
||||
|
||||
const [isInitialLoadingNodes, setIsInitialLoadingNodes] = useState(true);
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<NodeType>(
|
||||
initialTables.map((table) =>
|
||||
tableToTableNode(table, { filteredSchemas, hiddenTableIds })
|
||||
tableToTableNode(table, filter, databaseType)
|
||||
)
|
||||
);
|
||||
const [edges, setEdges, onEdgesChange] =
|
||||
@@ -223,12 +245,12 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
|
||||
useEffect(() => {
|
||||
const initialNodes = initialTables.map((table) =>
|
||||
tableToTableNode(table, { filteredSchemas, hiddenTableIds })
|
||||
tableToTableNode(table, filter, databaseType)
|
||||
);
|
||||
if (equal(initialNodes, nodes)) {
|
||||
setIsInitialLoadingNodes(false);
|
||||
}
|
||||
}, [initialTables, nodes, filteredSchemas, hiddenTableIds]);
|
||||
}, [initialTables, nodes, filter, databaseType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitialLoadingNodes) {
|
||||
@@ -391,10 +413,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
...tables.map((table) => {
|
||||
const isOverlapping =
|
||||
(overlapGraph.graph.get(table.id) ?? []).length > 0;
|
||||
const node = tableToTableNode(table, {
|
||||
filteredSchemas,
|
||||
hiddenTableIds,
|
||||
});
|
||||
const node = tableToTableNode(table, filter, databaseType);
|
||||
|
||||
// Check if table uses the highlighted custom type
|
||||
let hasHighlightedCustomType = false;
|
||||
@@ -415,7 +434,9 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
},
|
||||
};
|
||||
}),
|
||||
...areas.map(areaToAreaNode),
|
||||
...areas.map((area) =>
|
||||
areaToAreaNode(area, tables, filter, databaseType)
|
||||
),
|
||||
];
|
||||
|
||||
// Check if nodes actually changed
|
||||
@@ -429,21 +450,30 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
tables,
|
||||
areas,
|
||||
setNodes,
|
||||
filteredSchemas,
|
||||
hiddenTableIds,
|
||||
filter,
|
||||
databaseType,
|
||||
overlapGraph.lastUpdated,
|
||||
overlapGraph.graph,
|
||||
highlightOverlappingTables,
|
||||
highlightedCustomType,
|
||||
]);
|
||||
|
||||
const prevFilteredSchemas = useRef<string[] | undefined>(undefined);
|
||||
const prevFilter = useRef<DiagramFilter | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
if (!equal(filteredSchemas, prevFilteredSchemas.current)) {
|
||||
if (!equal(filter, prevFilter.current)) {
|
||||
debounce(() => {
|
||||
const overlappingTablesInDiagram = findOverlappingTables({
|
||||
tables: tables.filter((table) =>
|
||||
shouldShowTablesBySchemaFilter(table, filteredSchemas)
|
||||
filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
})
|
||||
),
|
||||
});
|
||||
setOverlapGraph(overlappingTablesInDiagram);
|
||||
@@ -453,9 +483,9 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
maxZoom: 0.8,
|
||||
});
|
||||
}, 500)();
|
||||
prevFilteredSchemas.current = filteredSchemas;
|
||||
prevFilter.current = filter;
|
||||
}
|
||||
}, [filteredSchemas, fitView, tables, setOverlapGraph]);
|
||||
}, [filter, fitView, tables, setOverlapGraph, databaseType]);
|
||||
|
||||
// Handle parent area updates when tables move
|
||||
const tablePositions = useMemo(
|
||||
@@ -465,7 +495,12 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
|
||||
useEffect(() => {
|
||||
const checkParentAreas = debounce(() => {
|
||||
const updatedTables = updateTablesParentAreas(tables, areas);
|
||||
const updatedTables = updateTablesParentAreas(
|
||||
tables,
|
||||
areas,
|
||||
filter,
|
||||
databaseType
|
||||
);
|
||||
const needsUpdate: Array<{
|
||||
id: string;
|
||||
parentAreaId: string | null;
|
||||
@@ -475,6 +510,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
const oldTable = tables[index];
|
||||
if (
|
||||
oldTable &&
|
||||
(!!newTable.parentAreaId || !!oldTable.parentAreaId) &&
|
||||
newTable.parentAreaId !== oldTable.parentAreaId
|
||||
) {
|
||||
needsUpdate.push({
|
||||
@@ -505,7 +541,14 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
}, 300);
|
||||
|
||||
checkParentAreas();
|
||||
}, [tablePositions, areas, updateTablesState, tables]);
|
||||
}, [
|
||||
tablePositions,
|
||||
areas,
|
||||
updateTablesState,
|
||||
tables,
|
||||
filter,
|
||||
databaseType,
|
||||
]);
|
||||
|
||||
const onConnectHandler = useCallback(
|
||||
async (params: AddEdgeParams) => {
|
||||
@@ -884,16 +927,37 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
const deltaX = change.position.x - currentArea.x;
|
||||
const deltaY = change.position.y - currentArea.y;
|
||||
|
||||
const childTables = getTablesInArea(
|
||||
// Only move visible child tables
|
||||
const childTables = getVisibleTablesInArea(
|
||||
change.id,
|
||||
tables
|
||||
tables,
|
||||
filter,
|
||||
databaseType
|
||||
);
|
||||
|
||||
// Update child table positions in storage
|
||||
if (childTables.length > 0) {
|
||||
updateTablesState((currentTables) =>
|
||||
currentTables.map((table) => {
|
||||
if (table.parentAreaId === change.id) {
|
||||
// Only move visible tables that are in this area
|
||||
const isVisible = filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema:
|
||||
defaultSchemas[
|
||||
databaseType
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (
|
||||
table.parentAreaId === change.id &&
|
||||
isVisible
|
||||
) {
|
||||
return {
|
||||
id: table.id,
|
||||
x: table.x + deltaX,
|
||||
@@ -957,6 +1021,8 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
tables,
|
||||
areas,
|
||||
getNode,
|
||||
databaseType,
|
||||
filter,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -1012,6 +1078,21 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
overlapGraph
|
||||
);
|
||||
setOverlapGraph(newOverlappingGraph);
|
||||
|
||||
setTimeout(() => {
|
||||
setNodes((prevNodes) =>
|
||||
prevNodes.map((n) => {
|
||||
if (n.id === event.data.id) {
|
||||
return {
|
||||
...n,
|
||||
measured,
|
||||
};
|
||||
}
|
||||
|
||||
return n;
|
||||
})
|
||||
);
|
||||
}, 0);
|
||||
} else if (
|
||||
event.action === 'add_field' ||
|
||||
event.action === 'remove_field'
|
||||
@@ -1045,13 +1126,30 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
const diagramTables = event.data.diagram.tables ?? [];
|
||||
const overlappingTablesInDiagram = findOverlappingTables({
|
||||
tables: diagramTables.filter((table) =>
|
||||
shouldShowTablesBySchemaFilter(table, filteredSchemas)
|
||||
filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
})
|
||||
),
|
||||
});
|
||||
setOverlapGraph(overlappingTablesInDiagram);
|
||||
}
|
||||
},
|
||||
[overlapGraph, setOverlapGraph, getNode, nodes, filteredSchemas]
|
||||
[
|
||||
overlapGraph,
|
||||
setOverlapGraph,
|
||||
getNode,
|
||||
nodes,
|
||||
filter,
|
||||
setNodes,
|
||||
databaseType,
|
||||
]
|
||||
);
|
||||
|
||||
events.useSubscription(eventConsumer);
|
||||
|
||||
@@ -152,7 +152,13 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
checkIfNewField,
|
||||
getFieldNewName,
|
||||
getFieldNewType,
|
||||
getFieldNewNullable,
|
||||
getFieldNewPrimaryKey,
|
||||
getFieldNewCharacterMaximumLength,
|
||||
getFieldNewPrecision,
|
||||
getFieldNewScale,
|
||||
checkIfFieldHasChange,
|
||||
isSummaryOnly,
|
||||
} = useDiff();
|
||||
|
||||
const [diffState, setDiffState] = useState<{
|
||||
@@ -160,12 +166,22 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
isDiffNewField: boolean;
|
||||
fieldDiffChangedName: string | null;
|
||||
fieldDiffChangedType: DBField['type'] | null;
|
||||
fieldDiffChangedNullable: boolean | null;
|
||||
fieldDiffChangedCharacterMaximumLength: string | null;
|
||||
fieldDiffChangedScale: number | null;
|
||||
fieldDiffChangedPrecision: number | null;
|
||||
fieldDiffChangedPrimaryKey: boolean | null;
|
||||
isDiffFieldChanged: boolean;
|
||||
}>({
|
||||
isDiffFieldRemoved: false,
|
||||
isDiffNewField: false,
|
||||
fieldDiffChangedName: null,
|
||||
fieldDiffChangedType: null,
|
||||
fieldDiffChangedNullable: null,
|
||||
fieldDiffChangedCharacterMaximumLength: null,
|
||||
fieldDiffChangedScale: null,
|
||||
fieldDiffChangedPrecision: null,
|
||||
fieldDiffChangedPrimaryKey: null,
|
||||
isDiffFieldChanged: false,
|
||||
});
|
||||
|
||||
@@ -183,6 +199,22 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
fieldDiffChangedType: getFieldNewType({
|
||||
fieldId: field.id,
|
||||
}),
|
||||
fieldDiffChangedNullable: getFieldNewNullable({
|
||||
fieldId: field.id,
|
||||
}),
|
||||
fieldDiffChangedPrimaryKey: getFieldNewPrimaryKey({
|
||||
fieldId: field.id,
|
||||
}),
|
||||
fieldDiffChangedCharacterMaximumLength:
|
||||
getFieldNewCharacterMaximumLength({
|
||||
fieldId: field.id,
|
||||
}),
|
||||
fieldDiffChangedScale: getFieldNewScale({
|
||||
fieldId: field.id,
|
||||
}),
|
||||
fieldDiffChangedPrecision: getFieldNewPrecision({
|
||||
fieldId: field.id,
|
||||
}),
|
||||
isDiffFieldChanged: checkIfFieldHasChange({
|
||||
fieldId: field.id,
|
||||
tableId: tableNodeId,
|
||||
@@ -195,7 +227,12 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
checkIfNewField,
|
||||
getFieldNewName,
|
||||
getFieldNewType,
|
||||
getFieldNewPrimaryKey,
|
||||
getFieldNewNullable,
|
||||
checkIfFieldHasChange,
|
||||
getFieldNewCharacterMaximumLength,
|
||||
getFieldNewPrecision,
|
||||
getFieldNewScale,
|
||||
field.id,
|
||||
tableNodeId,
|
||||
]);
|
||||
@@ -206,6 +243,11 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
fieldDiffChangedName,
|
||||
fieldDiffChangedType,
|
||||
isDiffFieldChanged,
|
||||
fieldDiffChangedNullable,
|
||||
fieldDiffChangedPrimaryKey,
|
||||
fieldDiffChangedCharacterMaximumLength,
|
||||
fieldDiffChangedScale,
|
||||
fieldDiffChangedPrecision,
|
||||
} = diffState;
|
||||
|
||||
const enterEditMode = useCallback((e: React.MouseEvent) => {
|
||||
@@ -233,6 +275,7 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
'z-0 max-h-0 overflow-hidden opacity-0': !visible,
|
||||
'bg-sky-200 dark:bg-sky-800 hover:bg-sky-100 dark:hover:bg-sky-900 border-sky-300 dark:border-sky-700':
|
||||
isDiffFieldChanged &&
|
||||
!isSummaryOnly &&
|
||||
!isDiffFieldRemoved &&
|
||||
!isDiffNewField,
|
||||
'bg-red-200 dark:bg-red-800 hover:bg-red-100 dark:hover:bg-red-900 border-red-300 dark:border-red-700':
|
||||
@@ -297,7 +340,7 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
<SquareMinus className="size-3.5 text-red-800 dark:text-red-200" />
|
||||
) : isDiffNewField ? (
|
||||
<SquarePlus className="size-3.5 text-green-800 dark:text-green-200" />
|
||||
) : isDiffFieldChanged ? (
|
||||
) : isDiffFieldChanged && !isSummaryOnly ? (
|
||||
<SquareDot className="size-3.5 shrink-0 text-sky-800 dark:text-sky-200" />
|
||||
) : null}
|
||||
{editMode && !readonly ? (
|
||||
@@ -330,6 +373,7 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
isDiffNewField,
|
||||
'text-sky-800 font-normal dark:text-sky-200':
|
||||
isDiffFieldChanged &&
|
||||
!isSummaryOnly &&
|
||||
!isDiffFieldRemoved &&
|
||||
!isDiffNewField,
|
||||
})}
|
||||
@@ -359,7 +403,9 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
</div>
|
||||
{editMode ? null : (
|
||||
<div className="ml-2 flex shrink-0 items-center justify-end gap-1.5">
|
||||
{field.primaryKey ? (
|
||||
{(field.primaryKey &&
|
||||
fieldDiffChangedPrimaryKey === null) ||
|
||||
fieldDiffChangedPrimaryKey ? (
|
||||
<div
|
||||
className={cn(
|
||||
'text-muted-foreground',
|
||||
@@ -371,6 +417,7 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
? 'text-green-800 dark:text-green-200'
|
||||
: '',
|
||||
isDiffFieldChanged &&
|
||||
!isSummaryOnly &&
|
||||
!isDiffFieldRemoved &&
|
||||
!isDiffNewField
|
||||
? 'text-sky-800 dark:text-sky-200'
|
||||
@@ -383,7 +430,8 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'content-center text-right text-xs text-muted-foreground overflow-hidden min-w-[3rem] max-w-[8rem]',
|
||||
'content-center text-right text-xs text-muted-foreground overflow-hidden max-w-[8rem]',
|
||||
field.primaryKey ? 'min-w-0' : 'min-w-[3rem]',
|
||||
!readonly ? 'group-hover:hidden' : '',
|
||||
isDiffFieldRemoved
|
||||
? 'text-red-800 dark:text-red-200'
|
||||
@@ -393,6 +441,7 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
: '',
|
||||
isDiffFieldChanged &&
|
||||
!isDiffFieldRemoved &&
|
||||
!isSummaryOnly &&
|
||||
!isDiffNewField
|
||||
? 'text-sky-800 dark:text-sky-200'
|
||||
: ''
|
||||
@@ -411,9 +460,36 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
`${field.type.name.split(' ')[0]}${showFieldAttributes ? generateDBFieldSuffix(field) : ''}`
|
||||
`${field.type.name.split(' ')[0]}${
|
||||
showFieldAttributes
|
||||
? generateDBFieldSuffix({
|
||||
...field,
|
||||
...{
|
||||
precision:
|
||||
fieldDiffChangedPrecision ??
|
||||
field.precision,
|
||||
scale:
|
||||
fieldDiffChangedScale ??
|
||||
field.scale,
|
||||
characterMaximumLength:
|
||||
fieldDiffChangedCharacterMaximumLength ??
|
||||
field.characterMaximumLength,
|
||||
},
|
||||
})
|
||||
: ''
|
||||
}`
|
||||
)}
|
||||
{fieldDiffChangedNullable !== null ? (
|
||||
fieldDiffChangedNullable ? (
|
||||
<span className="font-semibold">?</span>
|
||||
) : (
|
||||
<span className="line-through">?</span>
|
||||
)
|
||||
) : field.nullable ? (
|
||||
'?'
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{field.nullable ? '?' : ''}
|
||||
</span>
|
||||
</div>
|
||||
{readonly ? null : (
|
||||
|
||||
@@ -86,6 +86,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
checkIfTableHasChange,
|
||||
checkIfNewTable,
|
||||
checkIfTableRemoved,
|
||||
isSummaryOnly,
|
||||
} = useDiff();
|
||||
|
||||
const fields = useMemo(() => table.fields, [table.fields]);
|
||||
@@ -312,7 +313,10 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
hasHighlightedCustomType
|
||||
? 'ring-2 ring-offset-slate-50 dark:ring-offset-slate-900 ring-yellow-500 ring-offset-2 animate-scale'
|
||||
: '',
|
||||
isDiffTableChanged && !isDiffNewTable && !isDiffTableRemoved
|
||||
isDiffTableChanged &&
|
||||
!isSummaryOnly &&
|
||||
!isDiffNewTable &&
|
||||
!isDiffTableRemoved
|
||||
? 'outline outline-[3px] outline-sky-500 dark:outline-sky-900 outline-offset-[5px]'
|
||||
: '',
|
||||
isDiffNewTable
|
||||
@@ -327,7 +331,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
isOverlapping,
|
||||
highlightOverlappingTables,
|
||||
hasHighlightedCustomType,
|
||||
|
||||
isSummaryOnly,
|
||||
isDiffTableChanged,
|
||||
isDiffNewTable,
|
||||
isDiffTableRemoved,
|
||||
@@ -364,7 +368,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
? 'new'
|
||||
: isDiffTableRemoved
|
||||
? 'removed'
|
||||
: isDiffTableChanged
|
||||
: isDiffTableChanged && !isSummaryOnly
|
||||
? 'changed'
|
||||
: 'none'
|
||||
}
|
||||
@@ -397,7 +401,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
Table Removed
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : isDiffTableChanged ? (
|
||||
) : isDiffTableChanged && !isSummaryOnly ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SquareDot
|
||||
@@ -433,7 +437,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
<Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-red-200 px-2 py-0.5 text-sm font-normal text-red-900 dark:bg-red-800 dark:text-red-200">
|
||||
{table.name}
|
||||
</Label>
|
||||
) : isDiffTableChanged ? (
|
||||
) : isDiffTableChanged && !isSummaryOnly ? (
|
||||
<Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-sky-200 px-2 py-0.5 text-sm font-normal text-sky-900 dark:bg-sky-800 dark:text-sky-200">
|
||||
{table.name}
|
||||
</Label>
|
||||
|
||||
@@ -15,8 +15,8 @@ import { Button } from '@/components/button/button';
|
||||
import { keyboardShortcutsForOS } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
|
||||
import { KeyboardShortcutAction } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
|
||||
import { useCanvas } from '@/hooks/use-canvas';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||
|
||||
const convertToPercentage = (value: number) => `${Math.round(value * 100)}%`;
|
||||
|
||||
@@ -30,7 +30,7 @@ export const Toolbar: React.FC<ToolbarProps> = () => {
|
||||
const { getZoom, zoomIn, zoomOut, fitView } = useReactFlow();
|
||||
const [zoom, setZoom] = useState<string>(convertToPercentage(getZoom()));
|
||||
const { setShowFilter } = useCanvas();
|
||||
const { hiddenTableIds } = useChartDB();
|
||||
const { hasActiveFilter } = useDiagramFilter();
|
||||
|
||||
const toggleFilter = useCallback(() => {
|
||||
setShowFilter((prev) => !prev);
|
||||
@@ -80,8 +80,7 @@ export const Toolbar: React.FC<ToolbarProps> = () => {
|
||||
'transition-all duration-200',
|
||||
{
|
||||
'bg-pink-500 text-white hover:bg-pink-600 hover:text-white':
|
||||
(hiddenTableIds ?? []).length >
|
||||
0,
|
||||
hasActiveFilter,
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import React, { Suspense, useCallback, useEffect, useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import React, { Suspense, useEffect } from 'react';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { useDialog } from '@/hooks/use-dialog';
|
||||
import { Toaster } from '@/components/toast/toaster';
|
||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
||||
import { useLayout } from '@/hooks/use-layout';
|
||||
import { useToast } from '@/components/toast/use-toast';
|
||||
import { ToastAction } from '@/components/toast/toast';
|
||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FullScreenLoaderProvider } from '@/context/full-screen-spinner-context/full-screen-spinner-provider';
|
||||
import { LayoutProvider } from '@/context/layout-context/layout-provider';
|
||||
import { LocalConfigProvider } from '@/context/local-config-context/local-config-provider';
|
||||
@@ -30,6 +25,7 @@ import { HIDE_CHARTDB_CLOUD } from '@/lib/env';
|
||||
import { useDiagramLoader } from './use-diagram-loader';
|
||||
import { DiffProvider } from '@/context/diff-context/diff-provider';
|
||||
import { TopNavbarMock } from './top-navbar/top-navbar-mock';
|
||||
import { DiagramFilterProvider } from '@/context/diagram-filter-context/diagram-filter-provider';
|
||||
|
||||
const OPEN_STAR_US_AFTER_SECONDS = 30;
|
||||
const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1;
|
||||
@@ -43,21 +39,11 @@ export const EditorMobileLayoutLazy = React.lazy(
|
||||
);
|
||||
|
||||
const EditorPageComponent: React.FC = () => {
|
||||
const { diagramName, currentDiagram, schemas, filteredSchemas } =
|
||||
useChartDB();
|
||||
const { openSelectSchema, showSidePanel } = useLayout();
|
||||
const { diagramName, currentDiagram } = useChartDB();
|
||||
const { openStarUsDialog } = useDialog();
|
||||
const { diagramId } = useParams<{ diagramId: string }>();
|
||||
const { isMd: isDesktop } = useBreakpoint('md');
|
||||
const {
|
||||
hideMultiSchemaNotification,
|
||||
setHideMultiSchemaNotification,
|
||||
starUsDialogLastOpen,
|
||||
setStarUsDialogLastOpen,
|
||||
githubRepoOpened,
|
||||
} = useLocalConfig();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { starUsDialogLastOpen, setStarUsDialogLastOpen, githubRepoOpened } =
|
||||
useLocalConfig();
|
||||
const { initialDiagram } = useDiagramLoader();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -85,73 +71,6 @@ const EditorPageComponent: React.FC = () => {
|
||||
starUsDialogLastOpen,
|
||||
]);
|
||||
|
||||
const lastDiagramId = useRef<string>('');
|
||||
|
||||
const handleChangeSchema = useCallback(async () => {
|
||||
showSidePanel();
|
||||
if (!isDesktop) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
openSelectSchema();
|
||||
}, [openSelectSchema, showSidePanel, isDesktop]);
|
||||
|
||||
useEffect(() => {
|
||||
if (lastDiagramId.current === currentDiagram.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastDiagramId.current = currentDiagram.id;
|
||||
if (schemas.length > 1 && !hideMultiSchemaNotification) {
|
||||
const formattedSchemas = !filteredSchemas
|
||||
? t('multiple_schemas_alert.none')
|
||||
: filteredSchemas
|
||||
.map((filteredSchema) =>
|
||||
schemas.find((schema) => schema.id === filteredSchema)
|
||||
)
|
||||
.map((schema) => `'${schema?.name}'`)
|
||||
.join(', ');
|
||||
toast({
|
||||
duration: Infinity,
|
||||
title: t('multiple_schemas_alert.title'),
|
||||
description: t('multiple_schemas_alert.description', {
|
||||
schemasCount: schemas.length,
|
||||
formattedSchemas,
|
||||
}),
|
||||
variant: 'default',
|
||||
layout: 'column',
|
||||
hideCloseButton: true,
|
||||
className:
|
||||
'top-0 right-0 flex fixed md:max-w-[420px] md:top-4 md:right-4',
|
||||
action: (
|
||||
<div className="flex justify-between gap-1">
|
||||
<div />
|
||||
<ToastAction
|
||||
onClick={() => {
|
||||
handleChangeSchema();
|
||||
setHideMultiSchemaNotification(true);
|
||||
}}
|
||||
altText="Show me the schemas"
|
||||
className="border border-pink-600 bg-pink-600 text-white hover:bg-pink-500"
|
||||
>
|
||||
{t('multiple_schemas_alert.show_me')}
|
||||
</ToastAction>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}
|
||||
}, [
|
||||
schemas,
|
||||
filteredSchemas,
|
||||
toast,
|
||||
currentDiagram.id,
|
||||
diagramId,
|
||||
openSelectSchema,
|
||||
t,
|
||||
handleChangeSchema,
|
||||
hideMultiSchemaNotification,
|
||||
setHideMultiSchemaNotification,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
@@ -202,21 +121,23 @@ export const EditorPage: React.FC = () => (
|
||||
<RedoUndoStackProvider>
|
||||
<DiffProvider>
|
||||
<ChartDBProvider>
|
||||
<HistoryProvider>
|
||||
<ReactFlowProvider>
|
||||
<CanvasProvider>
|
||||
<ExportImageProvider>
|
||||
<AlertProvider>
|
||||
<DialogProvider>
|
||||
<KeyboardShortcutsProvider>
|
||||
<EditorPageComponent />
|
||||
</KeyboardShortcutsProvider>
|
||||
</DialogProvider>
|
||||
</AlertProvider>
|
||||
</ExportImageProvider>
|
||||
</CanvasProvider>
|
||||
</ReactFlowProvider>
|
||||
</HistoryProvider>
|
||||
<DiagramFilterProvider>
|
||||
<HistoryProvider>
|
||||
<ReactFlowProvider>
|
||||
<CanvasProvider>
|
||||
<ExportImageProvider>
|
||||
<AlertProvider>
|
||||
<DialogProvider>
|
||||
<KeyboardShortcutsProvider>
|
||||
<EditorPageComponent />
|
||||
</KeyboardShortcutsProvider>
|
||||
</DialogProvider>
|
||||
</AlertProvider>
|
||||
</ExportImageProvider>
|
||||
</CanvasProvider>
|
||||
</ReactFlowProvider>
|
||||
</HistoryProvider>
|
||||
</DiagramFilterProvider>
|
||||
</ChartDBProvider>
|
||||
</DiffProvider>
|
||||
</RedoUndoStackProvider>
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from '@/lib/domain/db-custom-type';
|
||||
import { Badge } from '@/components/badge/badge';
|
||||
import { checkIfCustomTypeUsed } from '../utils';
|
||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||
|
||||
export interface CustomTypeListItemHeaderProps {
|
||||
customType: DBCustomType;
|
||||
@@ -45,12 +46,11 @@ export const CustomTypeListItemHeader: React.FC<
|
||||
const {
|
||||
updateCustomType,
|
||||
removeCustomType,
|
||||
schemas,
|
||||
filteredSchemas,
|
||||
highlightedCustomType,
|
||||
highlightCustomTypeId,
|
||||
tables,
|
||||
} = useChartDB();
|
||||
const { schemasDisplayed } = useDiagramFilter();
|
||||
const { t } = useTranslation();
|
||||
const [editMode, setEditMode] = React.useState(false);
|
||||
const [customTypeName, setCustomTypeName] = React.useState(customType.name);
|
||||
@@ -161,11 +161,11 @@ export const CustomTypeListItemHeader: React.FC<
|
||||
isHighlighted,
|
||||
]);
|
||||
|
||||
let schemaToDisplay;
|
||||
|
||||
if (schemas.length > 1 && !!filteredSchemas && filteredSchemas.length > 1) {
|
||||
schemaToDisplay = customType.schema;
|
||||
}
|
||||
const schemaToDisplay = useMemo(() => {
|
||||
if (schemasDisplayed.length > 1) {
|
||||
return customType.schema;
|
||||
}
|
||||
}, [customType.schema, schemasDisplayed.length]);
|
||||
|
||||
return (
|
||||
<div className="group flex h-11 flex-1 items-center justify-between gap-1 overflow-hidden">
|
||||
|
||||
@@ -13,13 +13,16 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@/components/tooltip/tooltip';
|
||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||
import { shouldShowDependencyBySchemaFilter } from '@/lib/domain/db-dependency';
|
||||
import { useLayout } from '@/hooks/use-layout';
|
||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||
import { filterDependency } from '@/lib/domain/diagram-filter/filter';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
|
||||
export interface DependenciesSectionProps {}
|
||||
|
||||
export const DependenciesSection: React.FC<DependenciesSectionProps> = () => {
|
||||
const { dependencies, filteredSchemas, getTable } = useChartDB();
|
||||
const { dependencies, getTable, databaseType } = useChartDB();
|
||||
const { filter } = useDiagramFilter();
|
||||
const [filterText, setFilterText] = React.useState('');
|
||||
const { closeAllDependenciesInSidebar } = useLayout();
|
||||
const { t } = useTranslation();
|
||||
@@ -44,12 +47,26 @@ export const DependenciesSection: React.FC<DependenciesSectionProps> = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const filterSchema: (dependency: DBDependency) => boolean = (
|
||||
const filterDependencies: (dependency: DBDependency) => boolean = (
|
||||
dependency
|
||||
) => shouldShowDependencyBySchemaFilter(dependency, filteredSchemas);
|
||||
) =>
|
||||
filterDependency({
|
||||
tableA: {
|
||||
id: dependency.tableId,
|
||||
schema: dependency.schema,
|
||||
},
|
||||
tableB: {
|
||||
id: dependency.dependentTableId,
|
||||
schema: dependency.dependentSchema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
});
|
||||
|
||||
return dependencies
|
||||
.filter(filterSchema)
|
||||
.filter(filterDependencies)
|
||||
.filter(filterName)
|
||||
.sort((a, b) => {
|
||||
const dependentTableA = getTable(a.dependentTableId);
|
||||
@@ -60,7 +77,7 @@ export const DependenciesSection: React.FC<DependenciesSectionProps> = () => {
|
||||
`${dependentTableB?.name}${tableB?.name}`
|
||||
);
|
||||
});
|
||||
}, [dependencies, filterText, filteredSchemas, getTable]);
|
||||
}, [dependencies, filterText, filter, getTable, databaseType]);
|
||||
|
||||
return (
|
||||
<section className="flex flex-1 flex-col overflow-hidden px-2">
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Input } from '@/components/input/input';
|
||||
import { RelationshipList } from './relationship-list/relationship-list';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||
import { shouldShowRelationshipBySchemaFilter } from '@/lib/domain/db-relationship';
|
||||
import { useLayout } from '@/hooks/use-layout';
|
||||
import { EmptyState } from '@/components/empty-state/empty-state';
|
||||
import { ScrollArea } from '@/components/scroll-area/scroll-area';
|
||||
@@ -16,11 +15,15 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@/components/tooltip/tooltip';
|
||||
import { useDialog } from '@/hooks/use-dialog';
|
||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||
import { filterRelationship } from '@/lib/domain/diagram-filter/filter';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
|
||||
export interface RelationshipsSectionProps {}
|
||||
|
||||
export const RelationshipsSection: React.FC<RelationshipsSectionProps> = () => {
|
||||
const { relationships, filteredSchemas } = useChartDB();
|
||||
const { relationships, databaseType } = useChartDB();
|
||||
const { filter } = useDiagramFilter();
|
||||
const [filterText, setFilterText] = React.useState('');
|
||||
const { closeAllRelationshipsInSidebar } = useLayout();
|
||||
const { t } = useTranslation();
|
||||
@@ -34,13 +37,26 @@ export const RelationshipsSection: React.FC<RelationshipsSectionProps> = () => {
|
||||
!filterText?.trim?.() ||
|
||||
relationship.name.toLowerCase().includes(filterText.toLowerCase());
|
||||
|
||||
const filterSchema: (relationship: DBRelationship) => boolean = (
|
||||
const filterRelationships: (relationship: DBRelationship) => boolean = (
|
||||
relationship
|
||||
) =>
|
||||
shouldShowRelationshipBySchemaFilter(relationship, filteredSchemas);
|
||||
filterRelationship({
|
||||
tableA: {
|
||||
id: relationship.sourceTableId,
|
||||
schema: relationship.sourceSchema,
|
||||
},
|
||||
tableB: {
|
||||
id: relationship.targetTableId,
|
||||
schema: relationship.targetSchema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
});
|
||||
|
||||
return relationships.filter(filterSchema).filter(filterName);
|
||||
}, [relationships, filterText, filteredSchemas]);
|
||||
return relationships.filter(filterRelationships).filter(filterName);
|
||||
}, [relationships, filterText, filter, databaseType]);
|
||||
|
||||
const handleCreateRelationship = useCallback(async () => {
|
||||
setFilterText('');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -12,8 +12,6 @@ import { RelationshipsSection } from './relationships-section/relationships-sect
|
||||
import { useLayout } from '@/hooks/use-layout';
|
||||
import type { SidebarSection } from '@/context/layout-context/layout-context';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { SelectBoxOption } from '@/components/select-box/select-box';
|
||||
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';
|
||||
@@ -25,69 +23,12 @@ export interface SidePanelProps {}
|
||||
|
||||
export const SidePanel: React.FC<SidePanelProps> = () => {
|
||||
const { t } = useTranslation();
|
||||
const { schemas, filterSchemas, filteredSchemas, databaseType } =
|
||||
useChartDB();
|
||||
const {
|
||||
selectSidebarSection,
|
||||
selectedSidebarSection,
|
||||
isSelectSchemaOpen,
|
||||
openSelectSchema,
|
||||
closeSelectSchema,
|
||||
} = useLayout();
|
||||
const { databaseType } = useChartDB();
|
||||
const { selectSidebarSection, selectedSidebarSection } = useLayout();
|
||||
const { isMd: isDesktop } = useBreakpoint('md');
|
||||
|
||||
const schemasOptions: SelectBoxOption[] = useMemo(
|
||||
() =>
|
||||
schemas.map(
|
||||
(schema): SelectBoxOption => ({
|
||||
label: schema.name,
|
||||
value: schema.id,
|
||||
description: `(${schema.tableCount} tables)`,
|
||||
})
|
||||
),
|
||||
[schemas]
|
||||
);
|
||||
|
||||
const setIsSelectSchemaOpen = useCallback(
|
||||
(open: boolean) => {
|
||||
if (open) {
|
||||
openSelectSchema();
|
||||
} else {
|
||||
closeSelectSchema();
|
||||
}
|
||||
},
|
||||
[openSelectSchema, closeSelectSchema]
|
||||
);
|
||||
|
||||
return (
|
||||
<aside className="flex h-full flex-col overflow-hidden">
|
||||
{schemasOptions.length > 0 ? (
|
||||
<div className="flex items-center justify-center border-b pl-3 pt-0.5">
|
||||
<div className="shrink-0 text-sm font-semibold">
|
||||
{t('side_panel.schema')}
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1">
|
||||
<SelectBox
|
||||
oneLine
|
||||
className="w-full rounded-none border-none"
|
||||
selectAll
|
||||
deselectAll
|
||||
options={schemasOptions}
|
||||
value={filteredSchemas ?? []}
|
||||
onChange={(values) => {
|
||||
filterSchemas(values as string[]);
|
||||
}}
|
||||
placeholder={t('side_panel.filter_by_schema')}
|
||||
inputPlaceholder={t('side_panel.search_schema')}
|
||||
emptyPlaceholder={t('side_panel.no_schemas_found')}
|
||||
multiple
|
||||
open={isSelectSchemaOpen}
|
||||
onOpenChange={setIsSelectSchemaOpen}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isDesktop ? (
|
||||
<div className="flex justify-center border-b pt-0.5">
|
||||
<Select
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import React, { useMemo, useState, useEffect, useCallback } from 'react';
|
||||
import React, {
|
||||
useMemo,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
@@ -7,8 +13,28 @@ import type { EffectiveTheme } from '@/context/theme-context/theme-context';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import { useToast } from '@/components/toast/use-toast';
|
||||
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
|
||||
import { ArrowLeftRight } from 'lucide-react';
|
||||
import {
|
||||
AlertCircle,
|
||||
ArrowLeftRight,
|
||||
Check,
|
||||
Pencil,
|
||||
PencilOff,
|
||||
Undo2,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { generateDBMLFromDiagram } from '@/lib/dbml/dbml-export/dbml-export';
|
||||
import { useDiff } from '@/context/diff-context/use-diff';
|
||||
import { importDBMLToDiagram } from '@/lib/dbml/dbml-import/dbml-import';
|
||||
import { applyDBMLChanges } from '@/lib/dbml/apply-dbml/apply-dbml';
|
||||
import { useDebounce } from '@/hooks/use-debounce';
|
||||
import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
|
||||
import {
|
||||
clearErrorHighlight,
|
||||
highlightErrorLine,
|
||||
} from '@/components/code-snippet/dbml/utils';
|
||||
import type * as monaco from 'monaco-editor';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
|
||||
|
||||
export interface TableDBMLProps {
|
||||
filteredTables: DBTable[];
|
||||
@@ -18,62 +44,53 @@ const getEditorTheme = (theme: EffectiveTheme) => {
|
||||
return theme === 'dark' ? 'dbml-dark' : 'dbml-light';
|
||||
};
|
||||
|
||||
export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
|
||||
const { currentDiagram } = useChartDB();
|
||||
export const TableDBML: React.FC<TableDBMLProps> = () => {
|
||||
const { currentDiagram, updateDiagramData, databaseType } = useChartDB();
|
||||
const { effectiveTheme } = useTheme();
|
||||
const { toast } = useToast();
|
||||
const [dbmlFormat, setDbmlFormat] = useState<'inline' | 'standard'>(
|
||||
'inline'
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [standardDbml, setStandardDbml] = useState('');
|
||||
const [inlineDbml, setInlineDbml] = useState('');
|
||||
const isMountedRef = useRef(true);
|
||||
const [isEditButtonEmphasized, setIsEditButtonEmphasized] = useState(false);
|
||||
|
||||
// --- Effect for handling empty field name warnings ---
|
||||
useEffect(() => {
|
||||
let foundInvalidFields = false;
|
||||
const invalidTableNames = new Set<string>();
|
||||
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
|
||||
const decorationsCollection =
|
||||
useRef<monaco.editor.IEditorDecorationsCollection>();
|
||||
|
||||
filteredTables.forEach((table) => {
|
||||
table.fields.forEach((field) => {
|
||||
if (field.name === '') {
|
||||
foundInvalidFields = true;
|
||||
invalidTableNames.add(table.name);
|
||||
const handleEditorDidMount = useCallback(
|
||||
(editor: monaco.editor.IStandaloneCodeEditor) => {
|
||||
editorRef.current = editor;
|
||||
decorationsCollection.current =
|
||||
editor.createDecorationsCollection();
|
||||
|
||||
if (readOnlyDisposableRef.current) {
|
||||
readOnlyDisposableRef.current.dispose();
|
||||
}
|
||||
|
||||
const readOnlyDisposable = editor.onDidAttemptReadOnlyEdit(() => {
|
||||
if (emphasisTimeoutRef.current) {
|
||||
clearTimeout(emphasisTimeoutRef.current);
|
||||
}
|
||||
|
||||
setIsEditButtonEmphasized(false);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
setIsEditButtonEmphasized(true);
|
||||
|
||||
emphasisTimeoutRef.current = setTimeout(() => {
|
||||
setIsEditButtonEmphasized(false);
|
||||
}, 600);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if (foundInvalidFields) {
|
||||
const tableNamesString = Array.from(invalidTableNames).join(', ');
|
||||
toast({
|
||||
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(() => {
|
||||
// Create a filtered diagram with only the selected tables
|
||||
const filteredDiagram: Diagram = {
|
||||
...currentDiagram,
|
||||
tables: filteredTables,
|
||||
};
|
||||
|
||||
const result = generateDBMLFromDiagram(filteredDiagram);
|
||||
|
||||
// Handle errors
|
||||
if (result.error) {
|
||||
toast({
|
||||
title: 'DBML Export Error',
|
||||
description: `Could not generate DBML: ${result.error.substring(0, 100)}${result.error.length > 100 ? '...' : ''}`,
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
standardDbml: result.standardDbml,
|
||||
inlineDbml: result.inlineDbml,
|
||||
};
|
||||
}, [currentDiagram, filteredTables, toast]);
|
||||
readOnlyDisposableRef.current = readOnlyDisposable;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Determine which DBML string to display
|
||||
const dbmlToDisplay = useMemo(
|
||||
@@ -86,30 +103,339 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
|
||||
setDbmlFormat((prev) => (prev === 'inline' ? 'standard' : 'inline'));
|
||||
}, []);
|
||||
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const [editedDbml, setEditedDbml] = useState<string>('');
|
||||
const lastDBMLChange = useRef(editedDbml);
|
||||
const { calculateDiff, originalDiagram, resetDiff, hasDiff, newDiagram } =
|
||||
useDiff();
|
||||
const { loadDiagramFromData } = useChartDB();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
const [warningMessage, setWarningMessage] = useState<string>();
|
||||
const { t } = useTranslation();
|
||||
const { hideLoader, showLoader } = useFullScreenLoader();
|
||||
const emphasisTimeoutRef = useRef<NodeJS.Timeout>();
|
||||
const readOnlyDisposableRef = useRef<monaco.IDisposable>();
|
||||
|
||||
// --- Check for empty field name warnings only on mount ---
|
||||
useEffect(() => {
|
||||
// Only check when not in edit mode
|
||||
if (isEditMode) return;
|
||||
|
||||
let foundInvalidFields = false;
|
||||
const invalidTableNames = new Set<string>();
|
||||
|
||||
currentDiagram.tables?.forEach((table) => {
|
||||
table.fields.forEach((field) => {
|
||||
if (field.name === '') {
|
||||
foundInvalidFields = true;
|
||||
invalidTableNames.add(table.name);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (foundInvalidFields) {
|
||||
const tableNamesString = Array.from(invalidTableNames).join(', ');
|
||||
setWarningMessage(
|
||||
`Some fields had empty names in tables: [${tableNamesString}] and were excluded from the DBML export.`
|
||||
);
|
||||
}
|
||||
}, [currentDiagram.tables, t, isEditMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessage(undefined);
|
||||
clearErrorHighlight(decorationsCollection.current);
|
||||
|
||||
const generateDBML = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
const result = generateDBMLFromDiagram(currentDiagram);
|
||||
|
||||
// Handle errors
|
||||
if (result.error) {
|
||||
toast({
|
||||
title: 'DBML Export Error',
|
||||
description: `Could not generate DBML: ${result.error.substring(0, 100)}${result.error.length > 100 ? '...' : ''}`,
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
setStandardDbml(result.standardDbml);
|
||||
setInlineDbml(result.inlineDbml);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
setTimeout(() => generateDBML(), 0);
|
||||
}, [currentDiagram, toast, isEditMode]);
|
||||
|
||||
// Update editedDbml when dbmlToDisplay changes
|
||||
useEffect(() => {
|
||||
if (!isLoading && dbmlToDisplay && !isEditMode) {
|
||||
setEditedDbml(dbmlToDisplay);
|
||||
lastDBMLChange.current = dbmlToDisplay;
|
||||
}
|
||||
}, [dbmlToDisplay, isLoading, isEditMode]);
|
||||
|
||||
// Create the showDiff function
|
||||
const showDiff = useCallback(
|
||||
async (dbmlContent: string) => {
|
||||
clearErrorHighlight(decorationsCollection.current);
|
||||
setErrorMessage(undefined);
|
||||
try {
|
||||
const diagramFromDBML: Diagram = await importDBMLToDiagram(
|
||||
dbmlContent,
|
||||
{ databaseType }
|
||||
);
|
||||
|
||||
const sourceDiagram: Diagram =
|
||||
originalDiagram ?? currentDiagram;
|
||||
|
||||
const targetDiagram: Diagram = {
|
||||
...sourceDiagram,
|
||||
tables: diagramFromDBML.tables,
|
||||
relationships: diagramFromDBML.relationships,
|
||||
customTypes: diagramFromDBML.customTypes,
|
||||
};
|
||||
|
||||
const newDiagram = applyDBMLChanges({
|
||||
sourceDiagram,
|
||||
targetDiagram,
|
||||
});
|
||||
|
||||
if (originalDiagram) {
|
||||
resetDiff();
|
||||
loadDiagramFromData(originalDiagram);
|
||||
}
|
||||
|
||||
calculateDiff({
|
||||
diagram: sourceDiagram,
|
||||
newDiagram,
|
||||
options: { summaryOnly: true },
|
||||
});
|
||||
} catch (error) {
|
||||
const dbmlError = parseDBMLError(error);
|
||||
|
||||
if (dbmlError) {
|
||||
highlightErrorLine({
|
||||
error: dbmlError,
|
||||
model: editorRef.current?.getModel(),
|
||||
editorDecorationsCollection:
|
||||
decorationsCollection.current,
|
||||
});
|
||||
|
||||
setErrorMessage(
|
||||
t('import_dbml_dialog.error.description') +
|
||||
` (1 error found - in line ${dbmlError.line})`
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
t,
|
||||
originalDiagram,
|
||||
currentDiagram,
|
||||
resetDiff,
|
||||
loadDiagramFromData,
|
||||
calculateDiff,
|
||||
databaseType,
|
||||
]
|
||||
);
|
||||
|
||||
const debouncedShowDiff = useDebounce(showDiff, 1000);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEditMode || !editedDbml) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only calculate diff if the DBML has changed
|
||||
if (editedDbml === lastDBMLChange.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastDBMLChange.current = editedDbml;
|
||||
|
||||
debouncedShowDiff(editedDbml);
|
||||
}, [editedDbml, isEditMode, debouncedShowDiff]);
|
||||
|
||||
const acceptChanges = useCallback(async () => {
|
||||
if (!editedDbml) return;
|
||||
if (!newDiagram) return;
|
||||
|
||||
showLoader();
|
||||
|
||||
await updateDiagramData(newDiagram, { forceUpdateStorage: true });
|
||||
|
||||
resetDiff();
|
||||
setEditedDbml(editedDbml);
|
||||
setIsEditMode(false);
|
||||
lastDBMLChange.current = editedDbml;
|
||||
hideLoader();
|
||||
}, [
|
||||
editedDbml,
|
||||
updateDiagramData,
|
||||
newDiagram,
|
||||
resetDiff,
|
||||
showLoader,
|
||||
hideLoader,
|
||||
]);
|
||||
|
||||
const undoChanges = useCallback(() => {
|
||||
if (!editedDbml) return;
|
||||
if (!originalDiagram) return;
|
||||
|
||||
loadDiagramFromData(originalDiagram);
|
||||
setIsEditMode(false);
|
||||
resetDiff();
|
||||
setEditedDbml(dbmlToDisplay);
|
||||
lastDBMLChange.current = dbmlToDisplay;
|
||||
}, [
|
||||
editedDbml,
|
||||
loadDiagramFromData,
|
||||
originalDiagram,
|
||||
resetDiff,
|
||||
dbmlToDisplay,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
isMountedRef.current = true;
|
||||
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
|
||||
if (emphasisTimeoutRef.current) {
|
||||
clearTimeout(emphasisTimeoutRef.current);
|
||||
}
|
||||
|
||||
if (readOnlyDisposableRef.current) {
|
||||
readOnlyDisposableRef.current.dispose();
|
||||
readOnlyDisposableRef.current = undefined;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const currentUndoChanges = undoChanges;
|
||||
|
||||
return () => {
|
||||
setTimeout(() => {
|
||||
if (!isMountedRef.current) {
|
||||
currentUndoChanges();
|
||||
}
|
||||
}, 0);
|
||||
};
|
||||
}, [undoChanges]);
|
||||
|
||||
return (
|
||||
<CodeSnippet
|
||||
code={dbmlToDisplay}
|
||||
actionsTooltipSide="right"
|
||||
className="my-0.5"
|
||||
actions={[
|
||||
{
|
||||
label: `Show ${dbmlFormat === 'inline' ? 'Standard' : 'Inline'} Refs`,
|
||||
icon: ArrowLeftRight,
|
||||
onClick: toggleFormat,
|
||||
},
|
||||
]}
|
||||
editorProps={{
|
||||
height: '100%',
|
||||
defaultLanguage: 'dbml',
|
||||
beforeMount: setupDBMLLanguage,
|
||||
loading: false,
|
||||
theme: getEditorTheme(effectiveTheme),
|
||||
options: {
|
||||
wordWrap: 'off',
|
||||
mouseWheelZoom: false,
|
||||
domReadOnly: true,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<CodeSnippet
|
||||
code={editedDbml}
|
||||
loading={isLoading}
|
||||
actionsTooltipSide="right"
|
||||
className="my-0.5"
|
||||
allowCopy={!isEditMode}
|
||||
actions={
|
||||
isEditMode && hasDiff
|
||||
? [
|
||||
{
|
||||
label: 'Accept Changes',
|
||||
icon: Check,
|
||||
onClick: acceptChanges,
|
||||
className:
|
||||
'h-7 items-center gap-1.5 rounded-md border border-green-200 bg-green-50 px-2.5 py-1.5 text-xs font-medium text-green-600 shadow-sm hover:bg-green-100 dark:border-green-800 dark:bg-green-800 dark:text-green-200 dark:hover:bg-green-700',
|
||||
},
|
||||
{
|
||||
label: 'Undo Changes',
|
||||
icon: Undo2,
|
||||
onClick: undoChanges,
|
||||
className:
|
||||
'h-7 items-center gap-1.5 rounded-md border border-red-200 bg-red-50 px-2.5 py-1.5 text-xs font-medium text-red-600 shadow-sm hover:bg-red-100 dark:border-red-800 dark:bg-red-800 dark:text-red-200 dark:hover:bg-red-700',
|
||||
},
|
||||
]
|
||||
: isEditMode && !hasDiff
|
||||
? [
|
||||
{
|
||||
label: 'View',
|
||||
icon: PencilOff,
|
||||
onClick: () =>
|
||||
setIsEditMode((prev) => !prev),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: `Show ${dbmlFormat === 'inline' ? 'Standard' : 'Inline'} Refs`,
|
||||
icon: ArrowLeftRight,
|
||||
onClick: toggleFormat,
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
icon: Pencil,
|
||||
onClick: () =>
|
||||
setIsEditMode((prev) => !prev),
|
||||
className: isEditButtonEmphasized
|
||||
? 'dbml-edit-button-emphasis'
|
||||
: undefined,
|
||||
},
|
||||
]
|
||||
}
|
||||
editorProps={{
|
||||
height: '100%',
|
||||
defaultLanguage: 'dbml',
|
||||
beforeMount: setupDBMLLanguage,
|
||||
theme: getEditorTheme(effectiveTheme),
|
||||
onMount: handleEditorDidMount,
|
||||
options: {
|
||||
wordWrap: 'off',
|
||||
mouseWheelZoom: false,
|
||||
readOnly: !isEditMode,
|
||||
},
|
||||
onChange: (value) => {
|
||||
setEditedDbml(value ?? '');
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{warningMessage ? (
|
||||
<div className="my-2 rounded-md border border-blue-200 bg-blue-50 p-3 dark:border-blue-900/50 dark:bg-blue-950/20">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertCircle className="mt-0.5 size-4 shrink-0 text-blue-600 dark:text-blue-400" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-blue-800 dark:text-blue-200">
|
||||
Warning
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-blue-700 dark:text-blue-300">
|
||||
{warningMessage}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setWarningMessage(undefined)}
|
||||
className="rounded p-0.5 text-blue-600 hover:bg-blue-100 dark:text-blue-400 dark:hover:bg-blue-900/50"
|
||||
aria-label="Close warning"
|
||||
>
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{errorMessage ? (
|
||||
<div className="my-2 rounded-md border border-orange-200 bg-orange-50 p-3 dark:border-orange-900/50 dark:bg-orange-950/20">
|
||||
<div className="flex gap-2">
|
||||
<AlertCircle className="mt-0.5 size-4 shrink-0 text-orange-600 dark:text-orange-400" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium text-orange-800 dark:text-orange-200">
|
||||
Syntax Error
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-orange-700 dark:text-orange-300">
|
||||
{errorMessage ||
|
||||
t('import_dbml_dialog.error.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Textarea } from '@/components/textarea/textarea';
|
||||
import { useDebounce } from '@/hooks/use-debounce';
|
||||
import equal from 'fast-deep-equal';
|
||||
import type { DatabaseType } from '@/lib/domain';
|
||||
import type { DatabaseType, DBTable } from '@/lib/domain';
|
||||
|
||||
import {
|
||||
Select,
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
|
||||
export interface TableFieldPopoverProps {
|
||||
field: DBField;
|
||||
table: DBTable;
|
||||
databaseType: DatabaseType;
|
||||
updateField: (attrs: Partial<DBField>) => void;
|
||||
removeField: () => void;
|
||||
@@ -36,6 +37,7 @@ export interface TableFieldPopoverProps {
|
||||
|
||||
export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
|
||||
field,
|
||||
table,
|
||||
databaseType,
|
||||
updateField,
|
||||
removeField,
|
||||
@@ -44,6 +46,19 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
|
||||
const [localField, setLocalField] = React.useState<DBField>(field);
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
|
||||
// Check if this field is the only primary key in the table
|
||||
const isOnlyPrimaryKey = React.useMemo(() => {
|
||||
if (!field.primaryKey) return false;
|
||||
|
||||
// Early exit if we find another primary key
|
||||
for (const f of table.fields) {
|
||||
if (f.id !== field.id && f.primaryKey) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}, [table.fields, field.primaryKey, field.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalField(field);
|
||||
}, [field]);
|
||||
@@ -113,7 +128,7 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
|
||||
</Label>
|
||||
<Checkbox
|
||||
checked={localField.unique}
|
||||
disabled={field.primaryKey}
|
||||
disabled={isOnlyPrimaryKey}
|
||||
onCheckedChange={(value) =>
|
||||
setLocalField((current) => ({
|
||||
...current,
|
||||
|
||||
@@ -23,8 +23,10 @@ import type {
|
||||
} from '@/components/select-box/select-box';
|
||||
import { SelectBox } from '@/components/select-box/select-box';
|
||||
import { TableFieldPopover } from './table-field-modal/table-field-modal';
|
||||
import type { DBTable } from '@/lib/domain';
|
||||
|
||||
export interface TableFieldProps {
|
||||
table: DBTable;
|
||||
field: DBField;
|
||||
updateField: (attrs: Partial<DBField>) => void;
|
||||
removeField: () => void;
|
||||
@@ -76,6 +78,7 @@ const generateFieldRegexPatterns = (
|
||||
};
|
||||
|
||||
export const TableField: React.FC<TableFieldProps> = ({
|
||||
table,
|
||||
field,
|
||||
updateField,
|
||||
removeField,
|
||||
@@ -83,6 +86,13 @@ export const TableField: React.FC<TableFieldProps> = ({
|
||||
const { databaseType, customTypes } = useChartDB();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Only calculate primary key fields, not just count
|
||||
const primaryKeyFields = useMemo(() => {
|
||||
return table.fields.filter((f) => f.primaryKey);
|
||||
}, [table.fields]);
|
||||
|
||||
const primaryKeyCount = primaryKeyFields.length;
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({ id: field.id });
|
||||
|
||||
@@ -191,6 +201,42 @@ export const TableField: React.FC<TableFieldProps> = ({
|
||||
transition,
|
||||
};
|
||||
|
||||
const handlePrimaryKeyToggle = useCallback(
|
||||
(value: boolean) => {
|
||||
if (value) {
|
||||
// When setting as primary key
|
||||
const updates: Partial<DBField> = {
|
||||
primaryKey: true,
|
||||
};
|
||||
// Only auto-set unique if this will be the only primary key
|
||||
if (primaryKeyCount === 0) {
|
||||
updates.unique = true;
|
||||
}
|
||||
updateField(updates);
|
||||
} else {
|
||||
// When removing primary key
|
||||
updateField({
|
||||
primaryKey: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
[primaryKeyCount, updateField]
|
||||
);
|
||||
|
||||
const handleNullableToggle = useCallback(
|
||||
(value: boolean) => {
|
||||
updateField({ nullable: value });
|
||||
},
|
||||
[updateField]
|
||||
);
|
||||
|
||||
const handleNameChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateField({ name: e.target.value });
|
||||
},
|
||||
[updateField]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-1 touch-none flex-row justify-between gap-2 p-1"
|
||||
@@ -215,11 +261,7 @@ export const TableField: React.FC<TableFieldProps> = ({
|
||||
'side_panel.tables_section.table.field_name'
|
||||
)}
|
||||
value={field.name}
|
||||
onChange={(e) =>
|
||||
updateField({
|
||||
name: e.target.value,
|
||||
})
|
||||
}
|
||||
onChange={handleNameChange}
|
||||
/>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
@@ -265,11 +307,7 @@ export const TableField: React.FC<TableFieldProps> = ({
|
||||
<span>
|
||||
<TableFieldToggle
|
||||
pressed={field.nullable}
|
||||
onPressedChange={(value) =>
|
||||
updateField({
|
||||
nullable: value,
|
||||
})
|
||||
}
|
||||
onPressedChange={handleNullableToggle}
|
||||
>
|
||||
N
|
||||
</TableFieldToggle>
|
||||
@@ -284,12 +322,7 @@ export const TableField: React.FC<TableFieldProps> = ({
|
||||
<span>
|
||||
<TableFieldToggle
|
||||
pressed={field.primaryKey}
|
||||
onPressedChange={(value) =>
|
||||
updateField({
|
||||
unique: value,
|
||||
primaryKey: value,
|
||||
})
|
||||
}
|
||||
onPressedChange={handlePrimaryKeyToggle}
|
||||
>
|
||||
<KeyRound className="h-3.5" />
|
||||
</TableFieldToggle>
|
||||
@@ -301,6 +334,7 @@ export const TableField: React.FC<TableFieldProps> = ({
|
||||
</Tooltip>
|
||||
<TableFieldPopover
|
||||
field={field}
|
||||
table={table}
|
||||
updateField={updateField}
|
||||
removeField={removeField}
|
||||
databaseType={databaseType}
|
||||
|
||||
@@ -56,6 +56,32 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
|
||||
>(['fields']);
|
||||
const sensors = useSensors(useSensor(PointerSensor));
|
||||
|
||||
// Create a memoized version of the field updater that handles primary key logic
|
||||
const handleFieldUpdate = useCallback(
|
||||
(fieldId: string, attrs: Partial<DBField>) => {
|
||||
updateField(table.id, fieldId, attrs);
|
||||
|
||||
// Handle the case when removing a primary key and only one remains
|
||||
if (attrs.primaryKey === false) {
|
||||
const remainingPrimaryKeys = table.fields.filter(
|
||||
(f) => f.id !== fieldId && f.primaryKey
|
||||
);
|
||||
if (remainingPrimaryKeys.length === 1) {
|
||||
// Set the remaining primary key field as unique
|
||||
updateField(
|
||||
table.id,
|
||||
remainingPrimaryKeys[0].id,
|
||||
{
|
||||
unique: true,
|
||||
},
|
||||
{ updateHistory: false }
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[table.id, table.fields, updateField]
|
||||
);
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
@@ -147,14 +173,9 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
|
||||
<TableField
|
||||
key={field.id}
|
||||
field={field}
|
||||
updateField={(
|
||||
attrs: Partial<DBField>
|
||||
) =>
|
||||
updateField(
|
||||
table.id,
|
||||
field.id,
|
||||
attrs
|
||||
)
|
||||
table={table}
|
||||
updateField={(attrs) =>
|
||||
handleFieldUpdate(field.id, attrs)
|
||||
}
|
||||
removeField={() =>
|
||||
removeField(table.id, field.id)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
CircleDotDashed,
|
||||
GripVertical,
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
import { cloneTable } from '@/lib/clone';
|
||||
import type { DBSchema } from '@/lib/domain';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||
|
||||
export interface TableListItemHeaderProps {
|
||||
table: DBTable;
|
||||
@@ -55,9 +56,9 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
|
||||
createField,
|
||||
createTable,
|
||||
schemas,
|
||||
filteredSchemas,
|
||||
databaseType,
|
||||
} = useChartDB();
|
||||
const { schemasDisplayed } = useDiagramFilter();
|
||||
const { openTableSchemaDialog } = useDialog();
|
||||
const { t } = useTranslation();
|
||||
const { fitView, setNodes } = useReactFlow();
|
||||
@@ -265,13 +266,13 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
|
||||
]
|
||||
);
|
||||
|
||||
let schemaToDisplay;
|
||||
const schemaToDisplay = useMemo(() => {
|
||||
if (schemasDisplayed.length > 1) {
|
||||
return table.schema;
|
||||
}
|
||||
}, [table.schema, schemasDisplayed.length]);
|
||||
|
||||
if (schemas.length > 1 && !!filteredSchemas && filteredSchemas.length > 1) {
|
||||
schemaToDisplay = table.schema;
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (table.name.trim()) {
|
||||
setTableName(table.name.trim());
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Button } from '@/components/button/button';
|
||||
import { Table, List, X, Code } from 'lucide-react';
|
||||
import { Input } from '@/components/input/input';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { useLayout } from '@/hooks/use-layout';
|
||||
import { EmptyState } from '@/components/empty-state/empty-state';
|
||||
@@ -21,11 +20,15 @@ import { TableDBML } from './table-dbml/table-dbml';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { getOperatingSystem } from '@/lib/utils';
|
||||
import type { DBSchema } from '@/lib/domain';
|
||||
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
|
||||
import { filterTable } from '@/lib/domain/diagram-filter/filter';
|
||||
import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
|
||||
export interface TablesSectionProps {}
|
||||
|
||||
export const TablesSection: React.FC<TablesSectionProps> = () => {
|
||||
const { createTable, tables, filteredSchemas, schemas } = useChartDB();
|
||||
const { createTable, tables, databaseType } = useChartDB();
|
||||
const { filter, schemasDisplayed } = useDiagramFilter();
|
||||
const { openTableSchemaDialog } = useDialog();
|
||||
const viewport = useViewport();
|
||||
const { t } = useTranslation();
|
||||
@@ -39,11 +42,20 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
|
||||
!filterText?.trim?.() ||
|
||||
table.name.toLowerCase().includes(filterText.toLowerCase());
|
||||
|
||||
const filterSchema: (table: DBTable) => boolean = (table) =>
|
||||
shouldShowTablesBySchemaFilter(table, filteredSchemas);
|
||||
const filterTables: (table: DBTable) => boolean = (table) =>
|
||||
filterTable({
|
||||
table: {
|
||||
id: table.id,
|
||||
schema: table.schema,
|
||||
},
|
||||
filter,
|
||||
options: {
|
||||
defaultSchema: defaultSchemas[databaseType],
|
||||
},
|
||||
});
|
||||
|
||||
return tables.filter(filterSchema).filter(filterTableName);
|
||||
}, [tables, filterText, filteredSchemas]);
|
||||
return tables.filter(filterTables).filter(filterTableName);
|
||||
}, [tables, filterText, filter, databaseType]);
|
||||
|
||||
const createTableWithLocation = useCallback(
|
||||
async ({ schema }: { schema?: DBSchema }) => {
|
||||
@@ -71,25 +83,20 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
|
||||
const handleCreateTable = useCallback(async () => {
|
||||
setFilterText('');
|
||||
|
||||
if ((filteredSchemas?.length ?? 0) > 1) {
|
||||
if (schemasDisplayed.length > 1) {
|
||||
openTableSchemaDialog({
|
||||
onConfirm: createTableWithLocation,
|
||||
schemas: schemas.filter((schema) =>
|
||||
filteredSchemas?.includes(schema.id)
|
||||
),
|
||||
schemas: schemasDisplayed,
|
||||
});
|
||||
} else {
|
||||
const schema =
|
||||
filteredSchemas?.length === 1
|
||||
? schemas.find((s) => s.id === filteredSchemas[0])
|
||||
: undefined;
|
||||
schemasDisplayed.length === 1 ? schemasDisplayed[0] : undefined;
|
||||
createTableWithLocation({ schema });
|
||||
}
|
||||
}, [
|
||||
createTableWithLocation,
|
||||
filteredSchemas,
|
||||
schemasDisplayed,
|
||||
openTableSchemaDialog,
|
||||
schemas,
|
||||
setFilterText,
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user