mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-04 22:13:15 +00:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2edc8dfde8 | ||
|
|
004d530880 | ||
|
|
fd2cc9fcfc | ||
|
|
4c93326bb6 | ||
|
|
ef3d7a8b67 | ||
|
|
3b3be086b1 | ||
|
|
b424518212 | ||
|
|
99a8201398 | ||
|
|
eb9b41e4f6 | ||
|
|
fef6d3f499 | ||
|
|
14f11c27a7 | ||
|
|
2118bce0f0 | ||
|
|
88be6c1fd4 | ||
|
|
0dcc9b9568 | ||
|
|
ff3269ec05 | ||
|
|
659dc2e3e7 | ||
|
|
c36cd33180 | ||
|
|
58231c9139 | ||
|
|
1643e7bdeb | ||
|
|
42d4cbac8c | ||
|
|
7452ca6965 | ||
|
|
27aede7794 | ||
|
|
e9e2736cb2 | ||
|
|
74c1730425 | ||
|
|
94bed7fcce | ||
|
|
8abf2a7bfc | ||
|
|
ee659eaa03 | ||
|
|
7c5db0848e | ||
|
|
4b43f720e9 | ||
|
|
766b5164b8 | ||
|
|
7868ca9f42 | ||
|
|
0411742864 | ||
|
|
9831ac5a10 | ||
|
|
91c6fb9249 | ||
|
|
c155013668 | ||
|
|
1b0f293c87 | ||
|
|
df2dc03aa0 | ||
|
|
205d431c89 | ||
|
|
0abe18cdf9 | ||
|
|
a151f56b5d | ||
|
|
2b6b733261 |
58
CHANGELOG.md
58
CHANGELOG.md
@@ -1,5 +1,63 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.7.0](https://github.com/chartdb/chartdb/compare/v1.6.1...v1.7.0) (2025-02-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **dbml-editor:** add dbml editor in side pannel ([#534](https://github.com/chartdb/chartdb/issues/534)) ([88be6c1](https://github.com/chartdb/chartdb/commit/88be6c1fd4a7e1f20937e8204c14d8fc1c2665b4))
|
||||||
|
* **import-dbml:** add import dbml functionality ([#549](https://github.com/chartdb/chartdb/issues/549)) ([b424518](https://github.com/chartdb/chartdb/commit/b424518212290a870fdb7c420a303f65f5901429))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **canvas edit:** add option to edit names in canvas ([#536](https://github.com/chartdb/chartdb/issues/536)) ([0dcc9b9](https://github.com/chartdb/chartdb/commit/0dcc9b9568cfe749d44d2e93cb365ba3d3a1e71c))
|
||||||
|
* **dbml-editor:** add shortcuts to dbml and filter: [#534](https://github.com/chartdb/chartdb/issues/534) ([#535](https://github.com/chartdb/chartdb/issues/535)) ([3b3be08](https://github.com/chartdb/chartdb/commit/3b3be086b1e8d5acf999f8504580d9e2f956f7da))
|
||||||
|
* **dbml:** add error handling ([#545](https://github.com/chartdb/chartdb/issues/545)) ([fef6d3f](https://github.com/chartdb/chartdb/commit/fef6d3f4996130a3769d1f25b4b1f2090293a1bf))
|
||||||
|
* **empty-state:** fix dark-mode for empty-state ([#547](https://github.com/chartdb/chartdb/issues/547)) ([99a8201](https://github.com/chartdb/chartdb/commit/99a820139861546a012d7b562ddbb9b77698151a))
|
||||||
|
* **examples:** fix employee example dbml ([#544](https://github.com/chartdb/chartdb/issues/544)) ([2118bce](https://github.com/chartdb/chartdb/commit/2118bce0f00d55eb19d22b9fa2d4964ba2533a09))
|
||||||
|
* **i18n:** translation/Ukrainian ([#529](https://github.com/chartdb/chartdb/issues/529)) ([ff3269e](https://github.com/chartdb/chartdb/commit/ff3269ec0510bbae4bc114e65a1ea86a656e8785))
|
||||||
|
* **open-diagram:** add arrow keys navigation in open diagram dialog ([#537](https://github.com/chartdb/chartdb/issues/537)) ([14f11c2](https://github.com/chartdb/chartdb/commit/14f11c27a7ad5b990131c8495148cabf12835082))
|
||||||
|
* **performance:** fix bundle size ([#551](https://github.com/chartdb/chartdb/issues/551)) ([4c93326](https://github.com/chartdb/chartdb/commit/4c93326bb6e3eaa143373c500a0c641e95a53fb9))
|
||||||
|
* **performance:** reduce bundle size ([#553](https://github.com/chartdb/chartdb/issues/553)) ([004d530](https://github.com/chartdb/chartdb/commit/004d530880a50dea6e9786eb9ae63cf592a4d852))
|
||||||
|
* **performance:** resolve error on startup ([#552](https://github.com/chartdb/chartdb/issues/552)) ([fd2cc9f](https://github.com/chartdb/chartdb/commit/fd2cc9fcfc8f4a9f0bc79def47d89114159392fb))
|
||||||
|
* **psql-import:** remove typo for import command (psql) ([#546](https://github.com/chartdb/chartdb/issues/546)) ([eb9b41e](https://github.com/chartdb/chartdb/commit/eb9b41e4f656bec1451c45763f4ea5b547aeec5c))
|
||||||
|
* **scroll:** fix scroll area ([#550](https://github.com/chartdb/chartdb/issues/550)) ([ef3d7a8](https://github.com/chartdb/chartdb/commit/ef3d7a8b67431e923b75bf8287b86bbc8abe723b))
|
||||||
|
|
||||||
|
## [1.6.1](https://github.com/chartdb/chartdb/compare/v1.6.0...v1.6.1) (2025-01-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* change empty state image ([#531](https://github.com/chartdb/chartdb/issues/531)) ([42d4cba](https://github.com/chartdb/chartdb/commit/42d4cbac8ce352e0e4e155d7003bfb85296b897f))
|
||||||
|
* **chat-type:** remove typo of char datatype in examples ([#530](https://github.com/chartdb/chartdb/issues/530)) ([58231c9](https://github.com/chartdb/chartdb/commit/58231c91393de30ebff817f0ebc57a5c5579f106))
|
||||||
|
* **empty_state:** customize empty state ([#533](https://github.com/chartdb/chartdb/issues/533)) ([1643e7b](https://github.com/chartdb/chartdb/commit/1643e7bdeb1bbaf081ab064e871d102c87243c0a))
|
||||||
|
* **Image Export:** importing css rules error while download image ([#524](https://github.com/chartdb/chartdb/issues/524)) ([e9e2736](https://github.com/chartdb/chartdb/commit/e9e2736cb2203702d53df9afc30b8e989a8c9953))
|
||||||
|
* **shortcuts:** add zoom all shortcut ([#528](https://github.com/chartdb/chartdb/issues/528)) ([7452ca6](https://github.com/chartdb/chartdb/commit/7452ca6965b0332a93b686c397ddf51013e42506))
|
||||||
|
* **filter-tables:** show clean filter if no-results ([#532](https://github.com/chartdb/chartdb/issues/532)) ([c36cd33](https://github.com/chartdb/chartdb/commit/c36cd33180badaa9b7f9e27c765f19cb03a50ccd))
|
||||||
|
|
||||||
|
## [1.6.0](https://github.com/chartdb/chartdb/compare/v1.5.1...v1.6.0) (2025-01-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **view-menu:** add toggle for mini map visibility ([#496](https://github.com/chartdb/chartdb/issues/496)) ([#505](https://github.com/chartdb/chartdb/issues/505)) ([8abf2a7](https://github.com/chartdb/chartdb/commit/8abf2a7bfcc36d39e60ac133b0e5e569de1bbc72))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add loadDiagramFromData logic to chartdb provider ([#513](https://github.com/chartdb/chartdb/issues/513)) ([ee659ea](https://github.com/chartdb/chartdb/commit/ee659eaa038a94ee13801801e84152df4d79683d))
|
||||||
|
* **dependency:** upgrade react query to v7 - clean console warnings ([#504](https://github.com/chartdb/chartdb/issues/504)) ([7c5db08](https://github.com/chartdb/chartdb/commit/7c5db0848e49dfdb7e7120f77003d1e37f8d71b0))
|
||||||
|
* **i18n:** translation/Arabic ([#509](https://github.com/chartdb/chartdb/issues/509)) ([4b43f72](https://github.com/chartdb/chartdb/commit/4b43f720e90e49d5461e68d188e3865000f52497))
|
||||||
|
|
||||||
|
## [1.5.1](https://github.com/chartdb/chartdb/compare/v1.5.0...v1.5.1) (2024-12-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **export:** fix SQL server field.nullable type to boolean ([#486](https://github.com/chartdb/chartdb/issues/486)) ([a151f56](https://github.com/chartdb/chartdb/commit/a151f56b5d950e0b5cc54363684ada95889024b3))
|
||||||
|
* **readme:** Update README.md - add CockroachDB ([#482](https://github.com/chartdb/chartdb/issues/482)) ([2b6b733](https://github.com/chartdb/chartdb/commit/2b6b73326155f18d6d56779c0657a3506e2d2cde))
|
||||||
|
|
||||||
## [1.5.0](https://github.com/chartdb/chartdb/compare/v1.4.0...v1.5.0) (2024-12-11)
|
## [1.5.0](https://github.com/chartdb/chartdb/compare/v1.4.0...v1.5.0) (2024-12-11)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif
|
|||||||
- ✅ SQL Server
|
- ✅ SQL Server
|
||||||
- ✅ MariaDB
|
- ✅ MariaDB
|
||||||
- ✅ SQLite
|
- ✅ SQLite
|
||||||
|
- ✅ CockroachDB
|
||||||
- ✅ ClickHouse
|
- ✅ ClickHouse
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const compat = new FlatCompat({
|
|||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
ignores: ['**/dist', '**/.eslintrc.cjs', 'tailwind.config.js'],
|
ignores: ['**/dist', '**/.eslintrc.cjs', '**/tailwind.config.js'],
|
||||||
// files: ['**/*.ts', '**/*.tsx'],
|
// files: ['**/*.ts', '**/*.tsx'],
|
||||||
},
|
},
|
||||||
...fixupConfigRules(
|
...fixupConfigRules(
|
||||||
|
|||||||
4761
package-lock.json
generated
4761
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "chartdb",
|
"name": "chartdb",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.5.0",
|
"version": "1.7.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^0.0.51",
|
"@ai-sdk/openai": "^0.0.51",
|
||||||
|
"@dbml/core": "^3.9.5",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
@@ -28,10 +29,10 @@
|
|||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-menubar": "^1.1.1",
|
"@radix-ui/react-menubar": "^1.1.1",
|
||||||
"@radix-ui/react-popover": "^1.1.1",
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
"@radix-ui/react-scroll-area": "1.2.0",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
"react-i18next": "^15.0.1",
|
"react-i18next": "^15.0.1",
|
||||||
"react-resizable-panels": "^2.0.22",
|
"react-resizable-panels": "^2.0.22",
|
||||||
"react-responsive": "^10.0.0",
|
"react-responsive": "^10.0.0",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^7.1.1",
|
||||||
"react-use": "^17.5.1",
|
"react-use": "^17.5.1",
|
||||||
"tailwind-merge": "^2.4.0",
|
"tailwind-merge": "^2.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|||||||
BIN
public/buckle-animated.gif
Normal file
BIN
public/buckle-animated.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 404 KiB |
BIN
public/buckle.png
Normal file
BIN
public/buckle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 6.1 KiB |
BIN
src/assets/empty_state_dark.png
Normal file
BIN
src/assets/empty_state_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
@@ -12,6 +12,14 @@ import { DarkTheme } from './themes/dark';
|
|||||||
import { LightTheme } from './themes/light';
|
import { LightTheme } from './themes/light';
|
||||||
import './config.ts';
|
import './config.ts';
|
||||||
|
|
||||||
|
export const Editor = lazy(() =>
|
||||||
|
import('./code-editor').then((module) => ({
|
||||||
|
default: module.Editor,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
type EditorType = typeof Editor;
|
||||||
|
|
||||||
export interface CodeSnippetProps {
|
export interface CodeSnippetProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
code: string;
|
code: string;
|
||||||
@@ -19,14 +27,9 @@ export interface CodeSnippetProps {
|
|||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
autoScroll?: boolean;
|
autoScroll?: boolean;
|
||||||
isComplete?: boolean;
|
isComplete?: boolean;
|
||||||
|
editorProps?: React.ComponentProps<EditorType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Editor = lazy(() =>
|
|
||||||
import('./code-editor').then((module) => ({
|
|
||||||
default: module.Editor,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||||
({
|
({
|
||||||
className,
|
className,
|
||||||
@@ -35,6 +38,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
|||||||
language = 'sql',
|
language = 'sql',
|
||||||
autoScroll = false,
|
autoScroll = false,
|
||||||
isComplete = true,
|
isComplete = true,
|
||||||
|
editorProps,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const monaco = useMonaco();
|
const monaco = useMonaco();
|
||||||
@@ -144,27 +148,32 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
|||||||
language={language}
|
language={language}
|
||||||
loading={<Spinner />}
|
loading={<Spinner />}
|
||||||
theme={effectiveTheme}
|
theme={effectiveTheme}
|
||||||
|
{...editorProps}
|
||||||
options={{
|
options={{
|
||||||
minimap: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
scrollbar: {
|
|
||||||
vertical: 'hidden',
|
|
||||||
horizontal: 'hidden',
|
|
||||||
alwaysConsumeMouseWheel: false,
|
|
||||||
},
|
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
renderValidationDecorations: 'off',
|
renderValidationDecorations: 'off',
|
||||||
lineDecorationsWidth: 0,
|
lineDecorationsWidth: 0,
|
||||||
overviewRulerBorder: false,
|
overviewRulerBorder: false,
|
||||||
overviewRulerLanes: 0,
|
overviewRulerLanes: 0,
|
||||||
hideCursorInOverviewRuler: true,
|
hideCursorInOverviewRuler: true,
|
||||||
|
contextmenu: false,
|
||||||
|
...editorProps?.options,
|
||||||
guides: {
|
guides: {
|
||||||
indentation: false,
|
indentation: false,
|
||||||
|
...editorProps?.options?.guides,
|
||||||
|
},
|
||||||
|
scrollbar: {
|
||||||
|
vertical: 'hidden',
|
||||||
|
horizontal: 'hidden',
|
||||||
|
alwaysConsumeMouseWheel: false,
|
||||||
|
...editorProps?.options?.scrollbar,
|
||||||
|
},
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
...editorProps?.options?.minimap,
|
||||||
},
|
},
|
||||||
contextmenu: false,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!isComplete ? (
|
{!isComplete ? (
|
||||||
|
|||||||
54
src/components/code-snippet/languages/dbml-language.ts
Normal file
54
src/components/code-snippet/languages/dbml-language.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { Monaco } from '@monaco-editor/react';
|
||||||
|
import { dataTypes } from '@/lib/data/data-types/data-types';
|
||||||
|
|
||||||
|
export const setupDBMLLanguage = (monaco: Monaco) => {
|
||||||
|
monaco.languages.register({ id: 'dbml' });
|
||||||
|
|
||||||
|
// Define themes for DBML
|
||||||
|
monaco.editor.defineTheme('dbml-dark', {
|
||||||
|
base: 'vs-dark',
|
||||||
|
inherit: true,
|
||||||
|
rules: [
|
||||||
|
{ token: 'keyword', foreground: '569CD6' }, // Table, Ref keywords
|
||||||
|
{ token: 'string', foreground: 'CE9178' }, // Strings
|
||||||
|
{ token: 'annotation', foreground: '9CDCFE' }, // [annotations]
|
||||||
|
{ token: 'delimiter', foreground: 'D4D4D4' }, // Braces {}
|
||||||
|
{ token: 'operator', foreground: 'D4D4D4' }, // Operators
|
||||||
|
{ token: 'datatype', foreground: '4EC9B0' }, // Data types
|
||||||
|
],
|
||||||
|
colors: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
monaco.editor.defineTheme('dbml-light', {
|
||||||
|
base: 'vs',
|
||||||
|
inherit: true,
|
||||||
|
rules: [
|
||||||
|
{ token: 'keyword', foreground: '0000FF' }, // Table, Ref keywords
|
||||||
|
{ token: 'string', foreground: 'A31515' }, // Strings
|
||||||
|
{ token: 'annotation', foreground: '001080' }, // [annotations]
|
||||||
|
{ token: 'delimiter', foreground: '000000' }, // Braces {}
|
||||||
|
{ token: 'operator', foreground: '000000' }, // Operators
|
||||||
|
{ token: 'type', foreground: '267F99' }, // Data types
|
||||||
|
],
|
||||||
|
colors: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataTypesNames = dataTypes.map((dt) => dt.name);
|
||||||
|
const datatypePattern = dataTypesNames.join('|');
|
||||||
|
|
||||||
|
monaco.languages.setMonarchTokensProvider('dbml', {
|
||||||
|
keywords: ['Table', 'Ref', 'Indexes'],
|
||||||
|
datatypes: dataTypesNames,
|
||||||
|
tokenizer: {
|
||||||
|
root: [
|
||||||
|
[/\b(Table|Ref|Indexes)\b/, 'keyword'],
|
||||||
|
[/\[.*?\]/, 'annotation'],
|
||||||
|
[/".*?"/, 'string'],
|
||||||
|
[/'.*?'/, 'string'],
|
||||||
|
[/[{}]/, 'delimiter'],
|
||||||
|
[/[<>]/, 'operator'],
|
||||||
|
[new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,30 +1,66 @@
|
|||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import EmptyStateImage from '@/assets/empty_state.png';
|
import EmptyStateImage from '@/assets/empty_state.png';
|
||||||
|
import EmptyStateImageDark from '@/assets/empty_state_dark.png';
|
||||||
import { Label } from '@/components/label/label';
|
import { Label } from '@/components/label/label';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
|
||||||
export interface EmptyStateProps {
|
export interface EmptyStateProps {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
imageClassName?: string;
|
||||||
|
titleClassName?: string;
|
||||||
|
descriptionClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmptyState = forwardRef<
|
export const EmptyState = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement> & EmptyStateProps
|
React.HTMLAttributes<HTMLDivElement> & EmptyStateProps
|
||||||
>(({ title, description, className }, ref) => (
|
>(
|
||||||
<div
|
(
|
||||||
ref={ref}
|
{
|
||||||
className={cn(
|
title,
|
||||||
'flex flex-1 flex-col items-center justify-center space-y-1',
|
description,
|
||||||
className
|
className,
|
||||||
)}
|
titleClassName,
|
||||||
>
|
descriptionClassName,
|
||||||
<img src={EmptyStateImage} alt="Empty state" className="w-32" />
|
imageClassName,
|
||||||
<Label className="text-base">{title}</Label>
|
},
|
||||||
<Label className="text-sm font-normal text-muted-foreground">
|
ref
|
||||||
{description}
|
) => {
|
||||||
</Label>
|
const { effectiveTheme } = useTheme();
|
||||||
</div>
|
|
||||||
));
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-1 flex-col items-center justify-center space-y-1',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
effectiveTheme === 'dark'
|
||||||
|
? EmptyStateImageDark
|
||||||
|
: EmptyStateImage
|
||||||
|
}
|
||||||
|
alt="Empty state"
|
||||||
|
className={cn('mb-2 w-20', imageClassName)}
|
||||||
|
/>
|
||||||
|
<Label className={cn('text-base', titleClassName)}>
|
||||||
|
{title}
|
||||||
|
</Label>
|
||||||
|
<Label
|
||||||
|
className={cn(
|
||||||
|
'text-sm font-normal text-muted-foreground',
|
||||||
|
descriptionClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
EmptyState.displayName = 'EmptyState';
|
EmptyState.displayName = 'EmptyState';
|
||||||
|
|||||||
15
src/context/alert-context/alert-context.tsx
Normal file
15
src/context/alert-context/alert-context.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import { emptyFn } from '@/lib/utils';
|
||||||
|
import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
||||||
|
|
||||||
|
export interface AlertContext {
|
||||||
|
showAlert: (params: BaseAlertDialogProps) => void;
|
||||||
|
closeAlert: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const alertContext = createContext<AlertContext>({
|
||||||
|
closeAlert: emptyFn,
|
||||||
|
showAlert: emptyFn,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useAlert = () => useContext(alertContext);
|
||||||
36
src/context/alert-context/alert-provider.tsx
Normal file
36
src/context/alert-context/alert-provider.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import type { AlertContext } from './alert-context';
|
||||||
|
import { alertContext } from './alert-context';
|
||||||
|
import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
||||||
|
import { BaseAlertDialog } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
||||||
|
|
||||||
|
export const AlertProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [showAlert, setShowAlert] = useState(false);
|
||||||
|
const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
const showAlertHandler: AlertContext['showAlert'] = useCallback(
|
||||||
|
(params) => {
|
||||||
|
setAlertParams(params);
|
||||||
|
setShowAlert(true);
|
||||||
|
},
|
||||||
|
[setShowAlert, setAlertParams]
|
||||||
|
);
|
||||||
|
const closeAlertHandler = useCallback(() => {
|
||||||
|
setShowAlert(false);
|
||||||
|
}, [setShowAlert]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<alertContext.Provider
|
||||||
|
value={{
|
||||||
|
showAlert: showAlertHandler,
|
||||||
|
closeAlert: closeAlertHandler,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<BaseAlertDialog dialog={{ open: showAlert }} {...alertParams} />
|
||||||
|
</alertContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
22
src/context/canvas-context/canvas-context.tsx
Normal file
22
src/context/canvas-context/canvas-context.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import { emptyFn } from '@/lib/utils';
|
||||||
|
import type { Graph } from '@/lib/graph';
|
||||||
|
import { createGraph } from '@/lib/graph';
|
||||||
|
|
||||||
|
export interface CanvasContext {
|
||||||
|
reorderTables: (options?: { updateHistory?: boolean }) => void;
|
||||||
|
fitView: (options?: {
|
||||||
|
duration?: number;
|
||||||
|
padding?: number;
|
||||||
|
maxZoom?: number;
|
||||||
|
}) => void;
|
||||||
|
setOverlapGraph: (graph: Graph<string>) => void;
|
||||||
|
overlapGraph: Graph<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canvasContext = createContext<CanvasContext>({
|
||||||
|
reorderTables: emptyFn,
|
||||||
|
fitView: emptyFn,
|
||||||
|
setOverlapGraph: emptyFn,
|
||||||
|
overlapGraph: createGraph(),
|
||||||
|
});
|
||||||
85
src/context/canvas-context/canvas-provider.tsx
Normal file
85
src/context/canvas-context/canvas-provider.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
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 { useReactFlow } from '@xyflow/react';
|
||||||
|
import { findOverlappingTables } from '@/pages/editor-page/canvas/canvas-utils';
|
||||||
|
import type { Graph } from '@/lib/graph';
|
||||||
|
import { createGraph } from '@/lib/graph';
|
||||||
|
|
||||||
|
interface CanvasProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
||||||
|
const { tables, relationships, updateTablesState, filteredSchemas } =
|
||||||
|
useChartDB();
|
||||||
|
const { fitView } = useReactFlow();
|
||||||
|
const [overlapGraph, setOverlapGraph] =
|
||||||
|
useState<Graph<string>>(createGraph());
|
||||||
|
|
||||||
|
const reorderTables = useCallback(
|
||||||
|
(
|
||||||
|
options: { updateHistory?: boolean } = {
|
||||||
|
updateHistory: true,
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const newTables = adjustTablePositions({
|
||||||
|
relationships,
|
||||||
|
tables: tables.filter((table) =>
|
||||||
|
shouldShowTablesBySchemaFilter(table, filteredSchemas)
|
||||||
|
),
|
||||||
|
mode: 'all', // Use 'all' mode for manual reordering
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedOverlapGraph = findOverlappingTables({
|
||||||
|
tables: newTables,
|
||||||
|
});
|
||||||
|
|
||||||
|
updateTablesState(
|
||||||
|
(currentTables) =>
|
||||||
|
currentTables.map((table) => {
|
||||||
|
const newTable = newTables.find(
|
||||||
|
(t) => t.id === table.id
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: table.id,
|
||||||
|
x: newTable?.x ?? table.x,
|
||||||
|
y: newTable?.y ?? table.y,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
updateHistory: options.updateHistory ?? true,
|
||||||
|
forceOverride: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setOverlapGraph(updatedOverlapGraph);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
fitView({
|
||||||
|
duration: 500,
|
||||||
|
padding: 0.2,
|
||||||
|
maxZoom: 0.8,
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
[filteredSchemas, relationships, tables, updateTablesState, fitView]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvasContext.Provider
|
||||||
|
value={{
|
||||||
|
reorderTables,
|
||||||
|
fitView,
|
||||||
|
setOverlapGraph,
|
||||||
|
overlapGraph,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</canvasContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -84,6 +84,7 @@ export interface ChartDBContext {
|
|||||||
options?: { updateHistory: boolean }
|
options?: { updateHistory: boolean }
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
loadDiagram: (diagramId: string) => Promise<Diagram | undefined>;
|
loadDiagram: (diagramId: string) => Promise<Diagram | undefined>;
|
||||||
|
loadDiagramFromData: (diagram: Diagram) => void;
|
||||||
updateDiagramUpdatedAt: () => Promise<void>;
|
updateDiagramUpdatedAt: () => Promise<void>;
|
||||||
clearDiagramData: () => Promise<void>;
|
clearDiagramData: () => Promise<void>;
|
||||||
deleteDiagram: () => Promise<void>;
|
deleteDiagram: () => Promise<void>;
|
||||||
@@ -246,6 +247,7 @@ export const chartDBContext = createContext<ChartDBContext>({
|
|||||||
updateDiagramName: emptyFn,
|
updateDiagramName: emptyFn,
|
||||||
updateDiagramUpdatedAt: emptyFn,
|
updateDiagramUpdatedAt: emptyFn,
|
||||||
loadDiagram: emptyFn,
|
loadDiagram: emptyFn,
|
||||||
|
loadDiagramFromData: emptyFn,
|
||||||
clearDiagramData: emptyFn,
|
clearDiagramData: emptyFn,
|
||||||
deleteDiagram: emptyFn,
|
deleteDiagram: emptyFn,
|
||||||
|
|
||||||
|
|||||||
@@ -1336,15 +1336,9 @@ export const ChartDBProvider: React.FC<
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
|
const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
|
||||||
async (diagramId: string) => {
|
useCallback(
|
||||||
const diagram = await db.getDiagram(diagramId, {
|
async (diagram) => {
|
||||||
includeRelationships: true,
|
|
||||||
includeTables: true,
|
|
||||||
includeDependencies: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (diagram) {
|
|
||||||
setDiagramId(diagram.id);
|
setDiagramId(diagram.id);
|
||||||
setDiagramName(diagram.name);
|
setDiagramName(diagram.name);
|
||||||
setDatabaseType(diagram.databaseType);
|
setDatabaseType(diagram.databaseType);
|
||||||
@@ -1356,23 +1350,36 @@ export const ChartDBProvider: React.FC<
|
|||||||
setDiagramUpdatedAt(diagram.updatedAt);
|
setDiagramUpdatedAt(diagram.updatedAt);
|
||||||
|
|
||||||
events.emit({ action: 'load_diagram', data: { diagram } });
|
events.emit({ action: 'load_diagram', data: { diagram } });
|
||||||
|
},
|
||||||
|
[
|
||||||
|
setDiagramId,
|
||||||
|
setDiagramName,
|
||||||
|
setDatabaseType,
|
||||||
|
setDatabaseEdition,
|
||||||
|
setTables,
|
||||||
|
setRelationships,
|
||||||
|
setDependencies,
|
||||||
|
setDiagramCreatedAt,
|
||||||
|
setDiagramUpdatedAt,
|
||||||
|
events,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
|
||||||
|
async (diagramId: string) => {
|
||||||
|
const diagram = await db.getDiagram(diagramId, {
|
||||||
|
includeRelationships: true,
|
||||||
|
includeTables: true,
|
||||||
|
includeDependencies: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diagram) {
|
||||||
|
loadDiagramFromData(diagram);
|
||||||
}
|
}
|
||||||
|
|
||||||
return diagram;
|
return diagram;
|
||||||
},
|
},
|
||||||
[
|
[db, loadDiagramFromData]
|
||||||
db,
|
|
||||||
setDiagramId,
|
|
||||||
setDiagramName,
|
|
||||||
setDatabaseType,
|
|
||||||
setDatabaseEdition,
|
|
||||||
setTables,
|
|
||||||
setRelationships,
|
|
||||||
setDependencies,
|
|
||||||
setDiagramCreatedAt,
|
|
||||||
setDiagramUpdatedAt,
|
|
||||||
events,
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1393,6 +1400,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
updateDiagramId,
|
updateDiagramId,
|
||||||
updateDiagramName,
|
updateDiagramName,
|
||||||
loadDiagram,
|
loadDiagram,
|
||||||
|
loadDiagramFromData,
|
||||||
updateDatabaseType,
|
updateDatabaseType,
|
||||||
updateDatabaseEdition,
|
updateDatabaseEdition,
|
||||||
clearDiagramData,
|
clearDiagramData,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
import { emptyFn } from '@/lib/utils';
|
import { emptyFn } from '@/lib/utils';
|
||||||
import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
|
||||||
import type { TableSchemaDialogProps } from '@/dialogs/table-schema-dialog/table-schema-dialog';
|
import type { TableSchemaDialogProps } from '@/dialogs/table-schema-dialog/table-schema-dialog';
|
||||||
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
|
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
|
||||||
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
||||||
@@ -21,10 +20,6 @@ export interface DialogContext {
|
|||||||
openExportSQLDialog: (params: Omit<ExportSQLDialogProps, 'dialog'>) => void;
|
openExportSQLDialog: (params: Omit<ExportSQLDialogProps, 'dialog'>) => void;
|
||||||
closeExportSQLDialog: () => void;
|
closeExportSQLDialog: () => void;
|
||||||
|
|
||||||
// Alert dialog
|
|
||||||
showAlert: (params: BaseAlertDialogProps) => void;
|
|
||||||
closeAlert: () => void;
|
|
||||||
|
|
||||||
// Create relationship dialog
|
// Create relationship dialog
|
||||||
openCreateRelationshipDialog: () => void;
|
openCreateRelationshipDialog: () => void;
|
||||||
closeCreateRelationshipDialog: () => void;
|
closeCreateRelationshipDialog: () => void;
|
||||||
@@ -45,6 +40,10 @@ export interface DialogContext {
|
|||||||
openStarUsDialog: () => void;
|
openStarUsDialog: () => void;
|
||||||
closeStarUsDialog: () => void;
|
closeStarUsDialog: () => void;
|
||||||
|
|
||||||
|
// Buckle dialog
|
||||||
|
openBuckleDialog: () => void;
|
||||||
|
closeBuckleDialog: () => void;
|
||||||
|
|
||||||
// Export image dialog
|
// Export image dialog
|
||||||
openExportImageDialog: (
|
openExportImageDialog: (
|
||||||
params: Omit<ExportImageDialogProps, 'dialog'>
|
params: Omit<ExportImageDialogProps, 'dialog'>
|
||||||
@@ -62,6 +61,10 @@ export interface DialogContext {
|
|||||||
params: Omit<ImportDiagramDialogProps, 'dialog'>
|
params: Omit<ImportDiagramDialogProps, 'dialog'>
|
||||||
) => void;
|
) => void;
|
||||||
closeImportDiagramDialog: () => void;
|
closeImportDiagramDialog: () => void;
|
||||||
|
|
||||||
|
// Import DBML dialog
|
||||||
|
openImportDBMLDialog: () => void;
|
||||||
|
closeImportDBMLDialog: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dialogContext = createContext<DialogContext>({
|
export const dialogContext = createContext<DialogContext>({
|
||||||
@@ -71,8 +74,6 @@ export const dialogContext = createContext<DialogContext>({
|
|||||||
closeOpenDiagramDialog: emptyFn,
|
closeOpenDiagramDialog: emptyFn,
|
||||||
openExportSQLDialog: emptyFn,
|
openExportSQLDialog: emptyFn,
|
||||||
closeExportSQLDialog: emptyFn,
|
closeExportSQLDialog: emptyFn,
|
||||||
closeAlert: emptyFn,
|
|
||||||
showAlert: emptyFn,
|
|
||||||
closeCreateRelationshipDialog: emptyFn,
|
closeCreateRelationshipDialog: emptyFn,
|
||||||
openCreateRelationshipDialog: emptyFn,
|
openCreateRelationshipDialog: emptyFn,
|
||||||
openImportDatabaseDialog: emptyFn,
|
openImportDatabaseDialog: emptyFn,
|
||||||
@@ -87,4 +88,8 @@ export const dialogContext = createContext<DialogContext>({
|
|||||||
closeExportDiagramDialog: emptyFn,
|
closeExportDiagramDialog: emptyFn,
|
||||||
openImportDiagramDialog: emptyFn,
|
openImportDiagramDialog: emptyFn,
|
||||||
closeImportDiagramDialog: emptyFn,
|
closeImportDiagramDialog: emptyFn,
|
||||||
|
openBuckleDialog: emptyFn,
|
||||||
|
closeBuckleDialog: emptyFn,
|
||||||
|
openImportDBMLDialog: emptyFn,
|
||||||
|
closeImportDBMLDialog: emptyFn,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-di
|
|||||||
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
||||||
import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
|
||||||
import { BaseAlertDialog } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
|
||||||
import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
|
import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
|
||||||
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
|
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
|
||||||
import { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog';
|
import { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog';
|
||||||
@@ -19,6 +17,8 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
|
|||||||
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
|
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
|
||||||
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
|
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
|
||||||
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
|
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
|
||||||
|
import { BuckleDialog } from '@/dialogs/buckle-dialog/buckle-dialog';
|
||||||
|
import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
||||||
|
|
||||||
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -29,6 +29,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] =
|
const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [openStarUsDialog, setOpenStarUsDialog] = useState(false);
|
const [openStarUsDialog, setOpenStarUsDialog] = useState(false);
|
||||||
|
const [openBuckleDialog, setOpenBuckleDialog] = useState(false);
|
||||||
|
|
||||||
// Export image dialog
|
// Export image dialog
|
||||||
const [openExportImageDialog, setOpenExportImageDialog] = useState(false);
|
const [openExportImageDialog, setOpenExportImageDialog] = useState(false);
|
||||||
@@ -88,7 +89,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
[setOpenTableSchemaDialog]
|
[setOpenTableSchemaDialog]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Export image dialog
|
// Export diagram dialog
|
||||||
const [openExportDiagramDialog, setOpenExportDiagramDialog] =
|
const [openExportDiagramDialog, setOpenExportDiagramDialog] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
@@ -96,21 +97,8 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const [openImportDiagramDialog, setOpenImportDiagramDialog] =
|
const [openImportDiagramDialog, setOpenImportDiagramDialog] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
// Alert dialog
|
// Import DBML dialog
|
||||||
const [showAlert, setShowAlert] = useState(false);
|
const [openImportDBMLDialog, setOpenImportDBMLDialog] = useState(false);
|
||||||
const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({
|
|
||||||
title: '',
|
|
||||||
});
|
|
||||||
const showAlertHandler: DialogContext['showAlert'] = useCallback(
|
|
||||||
(params) => {
|
|
||||||
setAlertParams(params);
|
|
||||||
setShowAlert(true);
|
|
||||||
},
|
|
||||||
[setShowAlert, setAlertParams]
|
|
||||||
);
|
|
||||||
const closeAlertHandler = useCallback(() => {
|
|
||||||
setShowAlert(false);
|
|
||||||
}, [setShowAlert]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dialogContext.Provider
|
<dialogContext.Provider
|
||||||
@@ -121,8 +109,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
|
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
|
||||||
openExportSQLDialog: openExportSQLDialogHandler,
|
openExportSQLDialog: openExportSQLDialogHandler,
|
||||||
closeExportSQLDialog: () => setOpenExportSQLDialog(false),
|
closeExportSQLDialog: () => setOpenExportSQLDialog(false),
|
||||||
showAlert: showAlertHandler,
|
|
||||||
closeAlert: closeAlertHandler,
|
|
||||||
openCreateRelationshipDialog: () =>
|
openCreateRelationshipDialog: () =>
|
||||||
setOpenCreateRelationshipDialog(true),
|
setOpenCreateRelationshipDialog(true),
|
||||||
closeCreateRelationshipDialog: () =>
|
closeCreateRelationshipDialog: () =>
|
||||||
@@ -134,6 +120,8 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
closeTableSchemaDialog: () => setOpenTableSchemaDialog(false),
|
closeTableSchemaDialog: () => setOpenTableSchemaDialog(false),
|
||||||
openStarUsDialog: () => setOpenStarUsDialog(true),
|
openStarUsDialog: () => setOpenStarUsDialog(true),
|
||||||
closeStarUsDialog: () => setOpenStarUsDialog(false),
|
closeStarUsDialog: () => setOpenStarUsDialog(false),
|
||||||
|
closeBuckleDialog: () => setOpenBuckleDialog(false),
|
||||||
|
openBuckleDialog: () => setOpenBuckleDialog(true),
|
||||||
closeExportImageDialog: () => setOpenExportImageDialog(false),
|
closeExportImageDialog: () => setOpenExportImageDialog(false),
|
||||||
openExportImageDialog: openExportImageDialogHandler,
|
openExportImageDialog: openExportImageDialogHandler,
|
||||||
openExportDiagramDialog: () => setOpenExportDiagramDialog(true),
|
openExportDiagramDialog: () => setOpenExportDiagramDialog(true),
|
||||||
@@ -142,6 +130,8 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
|
openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
|
||||||
closeImportDiagramDialog: () =>
|
closeImportDiagramDialog: () =>
|
||||||
setOpenImportDiagramDialog(false),
|
setOpenImportDiagramDialog(false),
|
||||||
|
openImportDBMLDialog: () => setOpenImportDBMLDialog(true),
|
||||||
|
closeImportDBMLDialog: () => setOpenImportDBMLDialog(false),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -151,7 +141,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
dialog={{ open: openExportSQLDialog }}
|
dialog={{ open: openExportSQLDialog }}
|
||||||
{...exportSQLDialogParams}
|
{...exportSQLDialogParams}
|
||||||
/>
|
/>
|
||||||
<BaseAlertDialog dialog={{ open: showAlert }} {...alertParams} />
|
|
||||||
<CreateRelationshipDialog
|
<CreateRelationshipDialog
|
||||||
dialog={{ open: openCreateRelationshipDialog }}
|
dialog={{ open: openCreateRelationshipDialog }}
|
||||||
/>
|
/>
|
||||||
@@ -170,6 +159,8 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
/>
|
/>
|
||||||
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
|
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
|
||||||
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
|
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
|
||||||
|
<BuckleDialog dialog={{ open: openBuckleDialog }} />
|
||||||
|
<ImportDBMLDialog dialog={{ open: openImportDBMLDialog }} />
|
||||||
</dialogContext.Provider>
|
</dialogContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
},
|
},
|
||||||
quality: 1,
|
quality: 1,
|
||||||
pixelRatio: scale,
|
pixelRatio: scale,
|
||||||
|
skipFonts: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
downloadImage(dataUrl, type);
|
downloadImage(dataUrl, type);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useHistory } from '@/hooks/use-history';
|
|||||||
import { useDialog } from '@/hooks/use-dialog';
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { useLayout } from '@/hooks/use-layout';
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
|
import { useReactFlow } from '@xyflow/react';
|
||||||
|
|
||||||
export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -17,6 +18,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const { openOpenDiagramDialog } = useDialog();
|
const { openOpenDiagramDialog } = useDialog();
|
||||||
const { updateDiagramUpdatedAt } = useChartDB();
|
const { updateDiagramUpdatedAt } = useChartDB();
|
||||||
const { toggleSidePanel } = useLayout();
|
const { toggleSidePanel } = useLayout();
|
||||||
|
const { fitView } = useReactFlow();
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
keyboardShortcutsForOS[KeyboardShortcutAction.REDO].keyCombination,
|
keyboardShortcutsForOS[KeyboardShortcutAction.REDO].keyCombination,
|
||||||
@@ -61,6 +63,20 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
},
|
},
|
||||||
[toggleSidePanel]
|
[toggleSidePanel]
|
||||||
);
|
);
|
||||||
|
useHotkeys(
|
||||||
|
keyboardShortcutsForOS[KeyboardShortcutAction.SHOW_ALL].keyCombination,
|
||||||
|
() => {
|
||||||
|
fitView({
|
||||||
|
duration: 500,
|
||||||
|
padding: 0.1,
|
||||||
|
maxZoom: 0.8,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[fitView]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<keyboardShortcutsContext.Provider value={{}}>
|
<keyboardShortcutsContext.Provider value={{}}>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export enum KeyboardShortcutAction {
|
|||||||
OPEN_DIAGRAM = 'open_diagram',
|
OPEN_DIAGRAM = 'open_diagram',
|
||||||
SAVE_DIAGRAM = 'save_diagram',
|
SAVE_DIAGRAM = 'save_diagram',
|
||||||
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
|
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
|
||||||
|
SHOW_ALL = 'show_all',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KeyboardShortcut {
|
export interface KeyboardShortcut {
|
||||||
@@ -55,6 +56,13 @@ export const keyboardShortcuts: Record<
|
|||||||
keyCombinationMac: 'meta+b',
|
keyCombinationMac: 'meta+b',
|
||||||
keyCombinationWin: 'ctrl+b',
|
keyCombinationWin: 'ctrl+b',
|
||||||
},
|
},
|
||||||
|
[KeyboardShortcutAction.SHOW_ALL]: {
|
||||||
|
action: KeyboardShortcutAction.SHOW_ALL,
|
||||||
|
keyCombinationLabelMac: '⌘0',
|
||||||
|
keyCombinationLabelWin: 'Ctrl+0',
|
||||||
|
keyCombinationMac: 'meta+0',
|
||||||
|
keyCombinationWin: 'ctrl+0',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface KeyboardShortcutForOS {
|
export interface KeyboardShortcutForOS {
|
||||||
|
|||||||
@@ -30,8 +30,17 @@ export interface LocalConfigContext {
|
|||||||
starUsDialogLastOpen: number;
|
starUsDialogLastOpen: number;
|
||||||
setStarUsDialogLastOpen: (lastOpen: number) => void;
|
setStarUsDialogLastOpen: (lastOpen: number) => void;
|
||||||
|
|
||||||
|
buckleWaitlistOpened: boolean;
|
||||||
|
setBuckleWaitlistOpened: (githubRepoOpened: boolean) => void;
|
||||||
|
|
||||||
|
buckleDialogLastOpen: number;
|
||||||
|
setBuckleDialogLastOpen: (lastOpen: number) => void;
|
||||||
|
|
||||||
showDependenciesOnCanvas: boolean;
|
showDependenciesOnCanvas: boolean;
|
||||||
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
|
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
|
||||||
|
|
||||||
|
showMiniMapOnCanvas: boolean;
|
||||||
|
setShowMiniMapOnCanvas: (showMiniMapOnCanvas: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LocalConfigContext = createContext<LocalConfigContext>({
|
export const LocalConfigContext = createContext<LocalConfigContext>({
|
||||||
@@ -56,6 +65,15 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
|||||||
starUsDialogLastOpen: 0,
|
starUsDialogLastOpen: 0,
|
||||||
setStarUsDialogLastOpen: emptyFn,
|
setStarUsDialogLastOpen: emptyFn,
|
||||||
|
|
||||||
|
buckleWaitlistOpened: false,
|
||||||
|
setBuckleWaitlistOpened: emptyFn,
|
||||||
|
|
||||||
|
buckleDialogLastOpen: 0,
|
||||||
|
setBuckleDialogLastOpen: emptyFn,
|
||||||
|
|
||||||
showDependenciesOnCanvas: false,
|
showDependenciesOnCanvas: false,
|
||||||
setShowDependenciesOnCanvas: emptyFn,
|
setShowDependenciesOnCanvas: emptyFn,
|
||||||
|
|
||||||
|
showMiniMapOnCanvas: false,
|
||||||
|
setShowMiniMapOnCanvas: emptyFn,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ const showCardinalityKey = 'show_cardinality';
|
|||||||
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
|
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
|
||||||
const githubRepoOpenedKey = 'github_repo_opened';
|
const githubRepoOpenedKey = 'github_repo_opened';
|
||||||
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
||||||
|
const buckleWaitlistOpenedKey = 'buckle_waitlist_opened';
|
||||||
|
const buckleDialogLastOpenKey = 'buckle_dialog_last_open';
|
||||||
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
|
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
|
||||||
|
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
|
||||||
|
|
||||||
export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -48,12 +51,28 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
|
parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [buckleWaitlistOpened, setBuckleWaitlistOpened] =
|
||||||
|
React.useState<boolean>(
|
||||||
|
(localStorage.getItem(buckleWaitlistOpenedKey) || 'false') ===
|
||||||
|
'true'
|
||||||
|
);
|
||||||
|
|
||||||
|
const [buckleDialogLastOpen, setBuckleDialogLastOpen] =
|
||||||
|
React.useState<number>(
|
||||||
|
parseInt(localStorage.getItem(buckleDialogLastOpenKey) || '0')
|
||||||
|
);
|
||||||
|
|
||||||
const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
|
const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
|
||||||
React.useState<boolean>(
|
React.useState<boolean>(
|
||||||
(localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
|
(localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
|
||||||
'true'
|
'true'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [showMiniMapOnCanvas, setShowMiniMapOnCanvas] =
|
||||||
|
React.useState<boolean>(
|
||||||
|
(localStorage.getItem(showMiniMapOnCanvasKey) || 'true') === 'true'
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
starUsDialogLastOpenKey,
|
starUsDialogLastOpenKey,
|
||||||
@@ -65,6 +84,20 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
|
localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
|
||||||
}, [githubRepoOpened]);
|
}, [githubRepoOpened]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
buckleDialogLastOpenKey,
|
||||||
|
buckleDialogLastOpen.toString()
|
||||||
|
);
|
||||||
|
}, [buckleDialogLastOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
buckleWaitlistOpenedKey,
|
||||||
|
buckleWaitlistOpened.toString()
|
||||||
|
);
|
||||||
|
}, [buckleWaitlistOpened]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
hideMultiSchemaNotificationKey,
|
hideMultiSchemaNotificationKey,
|
||||||
@@ -95,6 +128,13 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
);
|
);
|
||||||
}, [showDependenciesOnCanvas]);
|
}, [showDependenciesOnCanvas]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
showMiniMapOnCanvasKey,
|
||||||
|
showMiniMapOnCanvas.toString()
|
||||||
|
);
|
||||||
|
}, [showMiniMapOnCanvas]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocalConfigContext.Provider
|
<LocalConfigContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -114,6 +154,12 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setStarUsDialogLastOpen,
|
setStarUsDialogLastOpen,
|
||||||
showDependenciesOnCanvas,
|
showDependenciesOnCanvas,
|
||||||
setShowDependenciesOnCanvas,
|
setShowDependenciesOnCanvas,
|
||||||
|
setBuckleDialogLastOpen,
|
||||||
|
buckleDialogLastOpen,
|
||||||
|
buckleWaitlistOpened,
|
||||||
|
setBuckleWaitlistOpened,
|
||||||
|
showMiniMapOnCanvas,
|
||||||
|
setShowMiniMapOnCanvas,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -134,6 +134,20 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
config: '++id, defaultDiagramId',
|
config: '++id, defaultDiagramId',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
db.version(9).upgrade((tx) =>
|
||||||
|
tx
|
||||||
|
.table<DBTable & { diagramId: string }>('db_tables')
|
||||||
|
.toCollection()
|
||||||
|
.modify((table) => {
|
||||||
|
for (const field of table.fields) {
|
||||||
|
if (typeof field.nullable === 'string') {
|
||||||
|
field.nullable =
|
||||||
|
(field.nullable as string).toLowerCase() === 'true';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
db.on('ready', async () => {
|
db.on('ready', async () => {
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/alert-dialog/alert-dialog';
|
} from '@/components/alert-dialog/alert-dialog';
|
||||||
import type { AlertDialogProps } from '@radix-ui/react-alert-dialog';
|
import type { AlertDialogProps } from '@radix-ui/react-alert-dialog';
|
||||||
import { useDialog } from '@/hooks/use-dialog';
|
import { useAlert } from '@/context/alert-context/alert-context';
|
||||||
|
|
||||||
export interface BaseAlertDialogProps {
|
export interface BaseAlertDialogProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -33,7 +33,7 @@ export const BaseAlertDialog: React.FC<BaseAlertDialogProps> = ({
|
|||||||
content,
|
content,
|
||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const { closeAlert } = useDialog();
|
const { closeAlert } = useAlert();
|
||||||
|
|
||||||
const closeAlertHandler = useCallback(() => {
|
const closeAlertHandler = useCallback(() => {
|
||||||
onClose?.();
|
onClose?.();
|
||||||
|
|||||||
80
src/dialogs/buckle-dialog/buckle-dialog.tsx
Normal file
80
src/dialogs/buckle-dialog/buckle-dialog.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/dialog/dialog';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
|
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||||
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
|
||||||
|
export interface BuckleDialogProps extends BaseDialogProps {}
|
||||||
|
|
||||||
|
export const BuckleDialog: React.FC<BuckleDialogProps> = ({ dialog }) => {
|
||||||
|
const { setBuckleWaitlistOpened } = useLocalConfig();
|
||||||
|
const { effectiveTheme } = useTheme();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dialog.open) return;
|
||||||
|
}, [dialog.open]);
|
||||||
|
const { closeBuckleDialog } = useDialog();
|
||||||
|
|
||||||
|
const handleConfirm = useCallback(() => {
|
||||||
|
setBuckleWaitlistOpened(true);
|
||||||
|
window.open('https://waitlist.buckle.dev', '_blank');
|
||||||
|
}, [setBuckleWaitlistOpened]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
{...dialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
closeBuckleDialog();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent
|
||||||
|
className="flex flex-col"
|
||||||
|
showClose={false}
|
||||||
|
onInteractOutside={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="hidden" />
|
||||||
|
<DialogDescription className="hidden" />
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex w-full flex-col items-center">
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
effectiveTheme === 'light'
|
||||||
|
? '/buckle-animated.gif'
|
||||||
|
: '/buckle.png'
|
||||||
|
}
|
||||||
|
className="h-16"
|
||||||
|
/>
|
||||||
|
<div className="mt-6 text-center text-base">
|
||||||
|
We've been working on something big -{' '}
|
||||||
|
<span className="font-semibold">Ready to explore?</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="flex gap-1 md:justify-between">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="secondary">Not now</Button>
|
||||||
|
</DialogClose>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button onClick={handleConfirm}>
|
||||||
|
Try ChartDB v2.0!
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -12,6 +12,7 @@ import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
|
|||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { useReactFlow } from '@xyflow/react';
|
import { useReactFlow } from '@xyflow/react';
|
||||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
|
import { useAlert } from '@/context/alert-context/alert-context';
|
||||||
|
|
||||||
export interface ImportDatabaseDialogProps extends BaseDialogProps {
|
export interface ImportDatabaseDialogProps extends BaseDialogProps {
|
||||||
databaseType: DatabaseType;
|
databaseType: DatabaseType;
|
||||||
@@ -21,7 +22,8 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
|
|||||||
dialog,
|
dialog,
|
||||||
databaseType,
|
databaseType,
|
||||||
}) => {
|
}) => {
|
||||||
const { closeImportDatabaseDialog, showAlert } = useDialog();
|
const { closeImportDatabaseDialog } = useDialog();
|
||||||
|
const { showAlert } = useAlert();
|
||||||
const {
|
const {
|
||||||
tables,
|
tables,
|
||||||
relationships,
|
relationships,
|
||||||
|
|||||||
276
src/dialogs/import-dbml-dialog/import-dbml-dialog.tsx
Normal file
276
src/dialogs/import-dbml-dialog/import-dbml-dialog.tsx
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
import React, { useCallback, useEffect, useState, Suspense } from 'react';
|
||||||
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogInternalContent,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/dialog/dialog';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Editor } from '@/components/code-snippet/code-snippet';
|
||||||
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
import { AlertCircle } from 'lucide-react';
|
||||||
|
import { importDBMLToDiagram } from '@/lib/dbml-import';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import { Parser } from '@dbml/core';
|
||||||
|
import { useCanvas } from '@/hooks/use-canvas';
|
||||||
|
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
|
||||||
|
import { useToast } from '@/components/toast/use-toast';
|
||||||
|
import { Spinner } from '@/components/spinner/spinner';
|
||||||
|
|
||||||
|
export interface ImportDBMLDialogProps extends BaseDialogProps {}
|
||||||
|
|
||||||
|
export const ImportDBMLDialog: React.FC<ImportDBMLDialogProps> = ({
|
||||||
|
dialog,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const initialDBML = `// Use DBML to define your database structure
|
||||||
|
// Simple Blog System with Comments Example
|
||||||
|
|
||||||
|
Table users {
|
||||||
|
id integer [primary key]
|
||||||
|
name varchar
|
||||||
|
email varchar
|
||||||
|
}
|
||||||
|
|
||||||
|
Table posts {
|
||||||
|
id integer [primary key]
|
||||||
|
title varchar
|
||||||
|
content text
|
||||||
|
user_id integer
|
||||||
|
created_at timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
Table comments {
|
||||||
|
id integer [primary key]
|
||||||
|
content text
|
||||||
|
post_id integer
|
||||||
|
user_id integer
|
||||||
|
created_at timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
Ref: posts.user_id > users.id // Each post belongs to one user
|
||||||
|
Ref: comments.post_id > posts.id // Each comment belongs to one post
|
||||||
|
Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||||
|
|
||||||
|
const [dbmlContent, setDBMLContent] = useState<string>(initialDBML);
|
||||||
|
const { closeImportDBMLDialog } = useDialog();
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | undefined>();
|
||||||
|
const { effectiveTheme } = useTheme();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const {
|
||||||
|
addTables,
|
||||||
|
addRelationships,
|
||||||
|
tables,
|
||||||
|
relationships,
|
||||||
|
removeTables,
|
||||||
|
removeRelationships,
|
||||||
|
} = useChartDB();
|
||||||
|
const { reorderTables } = useCanvas();
|
||||||
|
const [reorder, setReorder] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (reorder) {
|
||||||
|
reorderTables({
|
||||||
|
updateHistory: false,
|
||||||
|
});
|
||||||
|
setReorder(false);
|
||||||
|
}
|
||||||
|
}, [reorder, reorderTables]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dialog.open) return;
|
||||||
|
setErrorMessage(undefined);
|
||||||
|
setDBMLContent(initialDBML);
|
||||||
|
}, [dialog.open, initialDBML]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const validateDBML = async () => {
|
||||||
|
if (!dbmlContent.trim()) {
|
||||||
|
setErrorMessage(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parser = new Parser();
|
||||||
|
parser.parse(dbmlContent, 'dbml');
|
||||||
|
setErrorMessage(undefined);
|
||||||
|
} catch (e) {
|
||||||
|
setErrorMessage(
|
||||||
|
e instanceof Error
|
||||||
|
? e.message
|
||||||
|
: t('import_dbml_dialog.error.description')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
validateDBML();
|
||||||
|
}, [dbmlContent, t]);
|
||||||
|
|
||||||
|
const handleImport = useCallback(async () => {
|
||||||
|
if (!dbmlContent.trim() || errorMessage) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const importedDiagram = await importDBMLToDiagram(dbmlContent);
|
||||||
|
const tableIdsToRemove = tables
|
||||||
|
.filter((table) =>
|
||||||
|
importedDiagram.tables?.some(
|
||||||
|
(t) =>
|
||||||
|
t.name === table.name && t.schema === table.schema
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((table) => table.id);
|
||||||
|
// Find relationships that need to be removed
|
||||||
|
const relationshipIdsToRemove = relationships
|
||||||
|
.filter((relationship) => {
|
||||||
|
const sourceTable = tables.find(
|
||||||
|
(table) => table.id === relationship.sourceTableId
|
||||||
|
);
|
||||||
|
const targetTable = tables.find(
|
||||||
|
(table) => table.id === relationship.targetTableId
|
||||||
|
);
|
||||||
|
if (!sourceTable || !targetTable) return true;
|
||||||
|
const replacementSourceTable = importedDiagram.tables?.find(
|
||||||
|
(table) =>
|
||||||
|
table.name === sourceTable.name &&
|
||||||
|
table.schema === sourceTable.schema
|
||||||
|
);
|
||||||
|
const replacementTargetTable = importedDiagram.tables?.find(
|
||||||
|
(table) =>
|
||||||
|
table.name === targetTable.name &&
|
||||||
|
table.schema === targetTable.schema
|
||||||
|
);
|
||||||
|
return replacementSourceTable || replacementTargetTable;
|
||||||
|
})
|
||||||
|
.map((relationship) => relationship.id);
|
||||||
|
|
||||||
|
// Remove existing items
|
||||||
|
await Promise.all([
|
||||||
|
removeTables(tableIdsToRemove, { updateHistory: false }),
|
||||||
|
removeRelationships(relationshipIdsToRemove, {
|
||||||
|
updateHistory: false,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add new items
|
||||||
|
await Promise.all([
|
||||||
|
addTables(importedDiagram.tables ?? [], {
|
||||||
|
updateHistory: false,
|
||||||
|
}),
|
||||||
|
addRelationships(importedDiagram.relationships ?? [], {
|
||||||
|
updateHistory: false,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
setReorder(true);
|
||||||
|
closeImportDBMLDialog();
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
title: t('import_dbml_dialog.error.title'),
|
||||||
|
variant: 'destructive',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
<div>{t('import_dbml_dialog.error.description')}</div>
|
||||||
|
{e instanceof Error ? <div>{e.message}</div> : null}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
dbmlContent,
|
||||||
|
closeImportDBMLDialog,
|
||||||
|
tables,
|
||||||
|
relationships,
|
||||||
|
removeTables,
|
||||||
|
removeRelationships,
|
||||||
|
addTables,
|
||||||
|
addRelationships,
|
||||||
|
errorMessage,
|
||||||
|
toast,
|
||||||
|
setReorder,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
{...dialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
closeImportDBMLDialog();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent
|
||||||
|
className="flex h-[80vh] max-h-screen flex-col"
|
||||||
|
showClose
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('import_dbml_dialog.title')}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t('import_dbml_dialog.description')}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogInternalContent>
|
||||||
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<Editor
|
||||||
|
value={dbmlContent}
|
||||||
|
onChange={(value) => setDBMLContent(value || '')}
|
||||||
|
language="dbml"
|
||||||
|
theme={
|
||||||
|
effectiveTheme === 'dark'
|
||||||
|
? 'dbml-dark'
|
||||||
|
: 'dbml-light'
|
||||||
|
}
|
||||||
|
beforeMount={setupDBMLLanguage}
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: false },
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
automaticLayout: true,
|
||||||
|
scrollbar: {
|
||||||
|
vertical: 'visible',
|
||||||
|
horizontal: 'visible',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
className="size-full"
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</DialogInternalContent>
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="secondary">
|
||||||
|
{t('import_dbml_dialog.cancel')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
{errorMessage ? (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<AlertCircle className="size-4 text-destructive" />
|
||||||
|
|
||||||
|
<span className="text-xs text-destructive">
|
||||||
|
{errorMessage ||
|
||||||
|
t(
|
||||||
|
'import_dbml_dialog.error.description'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleImport}
|
||||||
|
disabled={!dbmlContent.trim() || !!errorMessage}
|
||||||
|
>
|
||||||
|
{t('import_dbml_dialog.import')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -22,10 +22,11 @@ import { useConfig } from '@/hooks/use-config';
|
|||||||
import { useDialog } from '@/hooks/use-dialog';
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
import { useStorage } from '@/hooks/use-storage';
|
import { useStorage } from '@/hooks/use-storage';
|
||||||
import type { Diagram } from '@/lib/domain/diagram';
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
|
import { useDebounce } from '@/hooks/use-debounce';
|
||||||
|
|
||||||
export interface OpenDiagramDialogProps extends BaseDialogProps {}
|
export interface OpenDiagramDialogProps extends BaseDialogProps {}
|
||||||
|
|
||||||
@@ -58,12 +59,65 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
|||||||
fetchDiagrams();
|
fetchDiagrams();
|
||||||
}, [listDiagrams, setDiagrams, dialog.open]);
|
}, [listDiagrams, setDiagrams, dialog.open]);
|
||||||
|
|
||||||
const openDiagram = (diagramId: string) => {
|
const openDiagram = useCallback(
|
||||||
if (diagramId) {
|
(diagramId: string) => {
|
||||||
updateConfig({ defaultDiagramId: diagramId });
|
if (diagramId) {
|
||||||
navigate(`/diagrams/${diagramId}`);
|
updateConfig({ defaultDiagramId: diagramId });
|
||||||
}
|
navigate(`/diagrams/${diagramId}`);
|
||||||
};
|
}
|
||||||
|
},
|
||||||
|
[updateConfig, navigate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRowKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLTableRowElement>) => {
|
||||||
|
const element = e.target as HTMLElement;
|
||||||
|
const diagramId = element.getAttribute('data-diagram-id');
|
||||||
|
const selectionIndexAttr = element.getAttribute(
|
||||||
|
'data-selection-index'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!diagramId || !selectionIndexAttr) return;
|
||||||
|
|
||||||
|
const selectionIndex = parseInt(selectionIndexAttr, 10);
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case 'Enter':
|
||||||
|
case ' ':
|
||||||
|
e.preventDefault();
|
||||||
|
openDiagram(diagramId);
|
||||||
|
closeOpenDiagramDialog();
|
||||||
|
break;
|
||||||
|
case 'ArrowDown': {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
(
|
||||||
|
document.querySelector(
|
||||||
|
`[data-selection-index="${selectionIndex + 1}"]`
|
||||||
|
) as HTMLElement
|
||||||
|
)?.focus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ArrowUp': {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
(
|
||||||
|
document.querySelector(
|
||||||
|
`[data-selection-index="${selectionIndex - 1}"]`
|
||||||
|
) as HTMLElement
|
||||||
|
)?.focus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[openDiagram, closeOpenDiagramDialog]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFocusHandler = useDebounce(
|
||||||
|
(diagramId: string) => setSelectedDiagramId(diagramId),
|
||||||
|
50
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
{...dialog}
|
{...dialog}
|
||||||
@@ -74,7 +128,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="flex h-[30rem] max-h-screen w-[90vw] flex-col overflow-y-auto md:w-screen xl:min-w-[55vw]"
|
className="flex h-[30rem] max-h-screen flex-col overflow-y-auto md:min-w-[80vw] xl:min-w-[55vw]"
|
||||||
showClose
|
showClose
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -112,10 +166,17 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{diagrams.map((diagram) => (
|
{diagrams.map((diagram, index) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={diagram.id}
|
key={diagram.id}
|
||||||
data-state={`${selectedDiagramId === diagram.id ? 'selected' : ''}`}
|
data-state={`${selectedDiagramId === diagram.id ? 'selected' : ''}`}
|
||||||
|
data-diagram-id={diagram.id}
|
||||||
|
data-selection-index={index}
|
||||||
|
tabIndex={0}
|
||||||
|
onFocus={() =>
|
||||||
|
onFocusHandler(diagram.id)
|
||||||
|
}
|
||||||
|
className="focus:bg-accent focus:outline-none"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
switch (e.detail) {
|
switch (e.detail) {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -133,6 +194,7 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={handleRowKeyDown}
|
||||||
>
|
>
|
||||||
<TableCell className="table-cell">
|
<TableCell className="table-cell">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
|
|||||||
@@ -73,3 +73,64 @@
|
|||||||
@apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] rounded-md cursor-pointer;
|
@apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] rounded-md cursor-pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gradient-background {
|
||||||
|
/* Fallback: Set a background color. */
|
||||||
|
background-color: #f46b24;
|
||||||
|
|
||||||
|
/* Create the gradient. */
|
||||||
|
background-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
#2e6579 20%,
|
||||||
|
#4fafca 20%,
|
||||||
|
#4fafca 40%,
|
||||||
|
#6dc630 40%,
|
||||||
|
#6dc630 60%,
|
||||||
|
#f9dc3a 60%,
|
||||||
|
#f9dc3a 80%,
|
||||||
|
#f46b24 80%
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Set the background size and repeat properties. */
|
||||||
|
background-size: 100%;
|
||||||
|
background-repeat: repeat;
|
||||||
|
|
||||||
|
/* Use the text as a mask for the background. */
|
||||||
|
/* This will show the gradient as a text color rather than element bg. */
|
||||||
|
/* -webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent; */
|
||||||
|
|
||||||
|
/* Animate the text when loading the element. */
|
||||||
|
/* This animates it on page load and when hovering out. */
|
||||||
|
animation: rainbow-text-simple-animation-rev 0.75s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-background:hover {
|
||||||
|
animation: rainbow-text-simple-animation 0.5s ease-in forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rainbow-text-simple-animation-rev {
|
||||||
|
0% {
|
||||||
|
background-size: 650%;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
background-size: 650%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-size: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Move the background and make it larger. */
|
||||||
|
/* Animation shown when hovering over the text. */
|
||||||
|
@keyframes rainbow-text-simple-animation {
|
||||||
|
0% {
|
||||||
|
background-size: 100%;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
background-size: 650%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-size: 650%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
4
src/hooks/use-canvas.ts
Normal file
4
src/hooks/use-canvas.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { canvasContext } from '@/context/canvas-context/canvas-context';
|
||||||
|
|
||||||
|
export const useCanvas = () => useContext(canvasContext);
|
||||||
21
src/hooks/use-debounce.ts
Normal file
21
src/hooks/use-debounce.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type AnyFunction = (...args: any[]) => any;
|
||||||
|
|
||||||
|
export const useDebounce = <T extends AnyFunction>(
|
||||||
|
func: T,
|
||||||
|
delay: number
|
||||||
|
): ((...args: Parameters<T>) => void) => {
|
||||||
|
const inDebounce = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
|
const debounce = useCallback(
|
||||||
|
(...args: Parameters<T>) => {
|
||||||
|
clearTimeout(inDebounce.current);
|
||||||
|
inDebounce.current = setTimeout(() => func(...args), delay);
|
||||||
|
},
|
||||||
|
[func, delay]
|
||||||
|
);
|
||||||
|
|
||||||
|
return debounce;
|
||||||
|
};
|
||||||
@@ -22,6 +22,7 @@ import { te, teMetadata } from './locales/te';
|
|||||||
import { bn, bnMetadata } from './locales/bn';
|
import { bn, bnMetadata } from './locales/bn';
|
||||||
import { gu, guMetadata } from './locales/gu';
|
import { gu, guMetadata } from './locales/gu';
|
||||||
import { vi, viMetadata } from './locales/vi';
|
import { vi, viMetadata } from './locales/vi';
|
||||||
|
import { ar, arMetadata } from './locales/ar';
|
||||||
|
|
||||||
export const languages: LanguageMetadata[] = [
|
export const languages: LanguageMetadata[] = [
|
||||||
enMetadata,
|
enMetadata,
|
||||||
@@ -44,6 +45,7 @@ export const languages: LanguageMetadata[] = [
|
|||||||
bnMetadata,
|
bnMetadata,
|
||||||
guMetadata,
|
guMetadata,
|
||||||
viMetadata,
|
viMetadata,
|
||||||
|
arMetadata,
|
||||||
];
|
];
|
||||||
|
|
||||||
const resources = {
|
const resources = {
|
||||||
@@ -67,6 +69,7 @@ const resources = {
|
|||||||
bn,
|
bn,
|
||||||
gu,
|
gu,
|
||||||
vi,
|
vi,
|
||||||
|
ar,
|
||||||
};
|
};
|
||||||
|
|
||||||
i18n.use(LanguageDetector)
|
i18n.use(LanguageDetector)
|
||||||
|
|||||||
423
src/i18n/locales/ar.ts
Normal file
423
src/i18n/locales/ar.ts
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
import type { LanguageMetadata, LanguageTranslation } from '../types';
|
||||||
|
|
||||||
|
export const ar: LanguageTranslation = {
|
||||||
|
translation: {
|
||||||
|
menu: {
|
||||||
|
file: {
|
||||||
|
file: 'ملف',
|
||||||
|
new: 'جديد',
|
||||||
|
open: 'فتح',
|
||||||
|
save: 'حفظ',
|
||||||
|
import: 'استيراد قاعدة بيانات',
|
||||||
|
export_sql: 'SQL تصدير',
|
||||||
|
export_as: 'تصدير كـ',
|
||||||
|
delete_diagram: 'حذف الرسم البياني',
|
||||||
|
exit: 'خروج',
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
edit: 'تحرير',
|
||||||
|
undo: 'تراجع',
|
||||||
|
redo: 'إعادة',
|
||||||
|
clear: 'مسح',
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
view: 'عرض',
|
||||||
|
show_sidebar: 'إظهار الشريط الجانبي',
|
||||||
|
hide_sidebar: 'إخفاء الشريط الجانبي',
|
||||||
|
hide_cardinality: 'إخفاء الكاردينالية',
|
||||||
|
show_cardinality: 'إظهار الكاردينالية',
|
||||||
|
zoom_on_scroll: 'تكبير/تصغير عند التمرير',
|
||||||
|
theme: 'المظهر',
|
||||||
|
show_dependencies: 'إظهار الاعتمادات',
|
||||||
|
hide_dependencies: 'إخفاء الاعتمادات',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
share: 'مشاركة',
|
||||||
|
export_diagram: 'تصدير المخطط',
|
||||||
|
import_diagram: 'استيراد المخطط',
|
||||||
|
},
|
||||||
|
help: {
|
||||||
|
help: 'مساعدة',
|
||||||
|
visit_website: 'ChartDB قم بزيارة',
|
||||||
|
join_discord: 'Discord انضم إلينا على',
|
||||||
|
schedule_a_call: '!تحدث معنا',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
delete_diagram_alert: {
|
||||||
|
title: 'حذف المخطط',
|
||||||
|
description:
|
||||||
|
'.لا يمكن التراجع عن هذا الإجراء. سيتم حذف الرسم البياني بشكل دائم',
|
||||||
|
cancel: 'إلغاء',
|
||||||
|
delete: 'حذف',
|
||||||
|
},
|
||||||
|
|
||||||
|
clear_diagram_alert: {
|
||||||
|
title: 'مسح الرسم البياني',
|
||||||
|
description:
|
||||||
|
'.لا يمكن التراجع عن هذا الاجراء. سيتم حذف جميع البيانات في الرسم البياني بشكل دائم',
|
||||||
|
cancel: 'إلغاء',
|
||||||
|
clear: 'مسح',
|
||||||
|
},
|
||||||
|
|
||||||
|
reorder_diagram_alert: {
|
||||||
|
title: 'إعادة ترتيب الرسم البياني',
|
||||||
|
description:
|
||||||
|
'هذا الإجراء سيقوم بإعادة ترتيب الجداول في المخطط بشكل تلقائي. هل تريد المتابعة؟',
|
||||||
|
reorder: 'إعادة ترتيب',
|
||||||
|
cancel: 'إلغاء',
|
||||||
|
},
|
||||||
|
|
||||||
|
multiple_schemas_alert: {
|
||||||
|
title: 'مخططات متعددة',
|
||||||
|
description:
|
||||||
|
'{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
|
||||||
|
dont_show_again: 'لا تظهره مجدداً',
|
||||||
|
change_schema: 'تغيير',
|
||||||
|
none: 'لا شيء',
|
||||||
|
},
|
||||||
|
|
||||||
|
copy_to_clipboard_toast: {
|
||||||
|
unsupported: {
|
||||||
|
title: 'فشل النسخ',
|
||||||
|
description: '.الحافظة غير مدعومة',
|
||||||
|
},
|
||||||
|
failed: {
|
||||||
|
title: 'فشل النسخ',
|
||||||
|
description: 'حدث خطأ أثناء النسخ. حاول مجدداً',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
system: 'النظام',
|
||||||
|
light: 'فاتح',
|
||||||
|
dark: 'داكن',
|
||||||
|
},
|
||||||
|
|
||||||
|
zoom: {
|
||||||
|
on: 'تشغيل',
|
||||||
|
off: 'إيقاف',
|
||||||
|
},
|
||||||
|
|
||||||
|
last_saved: 'آخر حفظ',
|
||||||
|
saved: 'تم الحفظ',
|
||||||
|
loading_diagram: '...جارِ تحميل الرسم البياني',
|
||||||
|
deselect_all: 'إلغاء تحديد الكل',
|
||||||
|
select_all: 'تحديد الكل',
|
||||||
|
clear: 'مسح',
|
||||||
|
show_more: 'عرض المزيد',
|
||||||
|
show_less: 'عرض أقل',
|
||||||
|
copy_to_clipboard: 'نسخ إلى الحافظة',
|
||||||
|
copied: '!تم النسخ',
|
||||||
|
|
||||||
|
side_panel: {
|
||||||
|
schema: ':المخطط',
|
||||||
|
filter_by_schema: 'تصفية حسب المخطط',
|
||||||
|
search_schema: '...بحث في المخطط',
|
||||||
|
no_schemas_found: '.لم يتم العثور على مخططات',
|
||||||
|
view_all_options: '...عرض جميع الخيارات',
|
||||||
|
tables_section: {
|
||||||
|
tables: 'الجداول',
|
||||||
|
add_table: 'إضافة جدول',
|
||||||
|
filter: 'تصفية',
|
||||||
|
collapse: 'طي الكل',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
|
table: {
|
||||||
|
fields: 'الحقول',
|
||||||
|
nullable: 'يمكن ان يكون فارغاً؟',
|
||||||
|
primary_key: 'المفتاح الأساسي',
|
||||||
|
indexes: 'الفهارس',
|
||||||
|
comments: 'تعليقات',
|
||||||
|
no_comments: 'لا توجد تعليقات',
|
||||||
|
add_field: 'إضافة حقل',
|
||||||
|
add_index: 'إضافة فهرس',
|
||||||
|
index_select_fields: 'حدد الحقول',
|
||||||
|
no_types_found: 'لا يوجد أنواع',
|
||||||
|
field_name: 'الإسم',
|
||||||
|
field_type: 'النوع',
|
||||||
|
field_actions: {
|
||||||
|
title: 'خصائص الحقل',
|
||||||
|
unique: 'فريد',
|
||||||
|
comments: 'تعليقات',
|
||||||
|
no_comments: 'لا يوجد تعليقات',
|
||||||
|
delete_field: 'حذف الحقل',
|
||||||
|
},
|
||||||
|
index_actions: {
|
||||||
|
title: 'خصائص الفهرس',
|
||||||
|
name: 'الإسم',
|
||||||
|
unique: 'فريد',
|
||||||
|
delete_index: 'حذف الفهرس',
|
||||||
|
},
|
||||||
|
table_actions: {
|
||||||
|
title: 'إجراءات الجدول',
|
||||||
|
change_schema: 'تغيير المخطط',
|
||||||
|
add_field: 'إضافة حقل',
|
||||||
|
add_index: 'إضافة فهرس',
|
||||||
|
duplicate_table: 'نسخ الجدول',
|
||||||
|
delete_table: 'حذف الجدول',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
empty_state: {
|
||||||
|
title: 'لا توجد جداول',
|
||||||
|
description: 'أنشئ جدولاً للبدء',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relationships_section: {
|
||||||
|
relationships: 'العلاقات',
|
||||||
|
filter: 'تصفية',
|
||||||
|
add_relationship: 'إضافة علاقة',
|
||||||
|
collapse: 'طي الكل',
|
||||||
|
relationship: {
|
||||||
|
primary: 'الجدول الأساسي',
|
||||||
|
foreign: 'الجدول المرتبط',
|
||||||
|
cardinality: 'الكاردينالية',
|
||||||
|
delete_relationship: 'حذف',
|
||||||
|
relationship_actions: {
|
||||||
|
title: 'إجراءات',
|
||||||
|
delete_relationship: 'حذف',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
empty_state: {
|
||||||
|
title: 'لا توجد علاقات',
|
||||||
|
description: 'إنشئ علاقة لربط الجداول',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dependencies_section: {
|
||||||
|
dependencies: 'الاعتمادات',
|
||||||
|
filter: 'تصفية',
|
||||||
|
collapse: 'طي الكل',
|
||||||
|
dependency: {
|
||||||
|
table: 'الجدول',
|
||||||
|
dependent_table: 'عرض الاعتمادات',
|
||||||
|
delete_dependency: 'حذف',
|
||||||
|
dependency_actions: {
|
||||||
|
title: 'إجراءات',
|
||||||
|
delete_dependency: 'حذف',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
empty_state: {
|
||||||
|
title: 'لا توجد اعتمادات',
|
||||||
|
description: 'إنشاء اعتماد للبدء',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
toolbar: {
|
||||||
|
zoom_in: 'تكبير',
|
||||||
|
zoom_out: 'تصغير',
|
||||||
|
save: 'حفظ',
|
||||||
|
show_all: 'عرض الكل',
|
||||||
|
undo: 'تراجع',
|
||||||
|
redo: 'إعادة',
|
||||||
|
reorder_diagram: 'إعادة ترتيب الرسم البياني',
|
||||||
|
highlight_overlapping_tables: 'تمييز الجداول المتداخلة',
|
||||||
|
},
|
||||||
|
|
||||||
|
new_diagram_dialog: {
|
||||||
|
database_selection: {
|
||||||
|
title: 'ما هو نوع قاعدة البيانات الخاصة بك؟',
|
||||||
|
description:
|
||||||
|
'تتمتع كل قاعدة بيانات بمميزاتها وقدراتها الفريدة.',
|
||||||
|
check_examples_long: 'ألقي نظرة على الأمثلة',
|
||||||
|
check_examples_short: 'أمثلة',
|
||||||
|
},
|
||||||
|
|
||||||
|
import_database: {
|
||||||
|
title: 'إسترد قاعدة بياناتك',
|
||||||
|
database_edition: ':إصدار قاعدة البيانات',
|
||||||
|
step_1: ':قم بتشغيل هذا البرنامج النصي في قاعدة بياناتك',
|
||||||
|
step_2: ':إلصق نتيجة البرنامج النصي هنا',
|
||||||
|
script_results_placeholder: '...نتيجة البرنامج النصي هنا',
|
||||||
|
ssms_instructions: {
|
||||||
|
button_text: 'SSMS تعليمات',
|
||||||
|
title: 'تعليمات',
|
||||||
|
step_1: 'SQL SERVER < انتقل إلى الأدوات > الخيارات > نتائح الاستعلام',
|
||||||
|
step_2: '(اضبطها على 9999999) XML اذا كنت تستخدم "نتائج إلى الشبكة"، قم بتغيير الحد الاقصى للاحرف المستردة للبيانات غير',
|
||||||
|
},
|
||||||
|
instructions_link: 'تحتاج مساعدة؟ شاهد الفيديو',
|
||||||
|
check_script_result: 'تحقق من نتيجة البرنامج النصي',
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel: 'إلغاء',
|
||||||
|
import_from_file: 'استيراد من ملف',
|
||||||
|
back: 'رجوع',
|
||||||
|
empty_diagram: 'مخطط فارغ',
|
||||||
|
continue: 'متابعة',
|
||||||
|
import: 'استيراد',
|
||||||
|
},
|
||||||
|
|
||||||
|
open_diagram_dialog: {
|
||||||
|
title: 'فتح مخطط',
|
||||||
|
description: 'اختر مخططًا لفتحه من القائمة ادناه',
|
||||||
|
table_columns: {
|
||||||
|
name: 'الإسم',
|
||||||
|
created_at: 'تاريخ الإنشاء',
|
||||||
|
last_modified: 'آخر تعديل',
|
||||||
|
tables_count: 'الجداول',
|
||||||
|
},
|
||||||
|
cancel: 'إلغاء',
|
||||||
|
open: 'فتح',
|
||||||
|
},
|
||||||
|
|
||||||
|
export_sql_dialog: {
|
||||||
|
title: 'SQL تصدير',
|
||||||
|
description:
|
||||||
|
'{{databaseType}} صدّر مخطط الرسم البياني إلى برنامج نصي لـ',
|
||||||
|
close: 'إغلاق',
|
||||||
|
loading: {
|
||||||
|
text: '...{{databaseType}} ل SQL يقوم الذكاء الاصطناعي بإنشاء',
|
||||||
|
description: 'هذا قد يستغرق 30 ثانية',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
message:
|
||||||
|
'النصي. يرجى المحاولة مرة اخرى لاحقاً او <0>اتصل بنا</0> SQL خطأ في إنشاء برنامج',
|
||||||
|
description:
|
||||||
|
' الخاصة بك. راجع الدليل <0>هنا</0> OPENAI_TOKEN لا تتردد في استخدام',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
create_relationship_dialog: {
|
||||||
|
title: 'إنشاء علاقة',
|
||||||
|
primary_table: 'الجدول الأساسي',
|
||||||
|
primary_field: 'الحقل الأساسي',
|
||||||
|
referenced_table: 'الجدول المرتبط',
|
||||||
|
referenced_field: 'الحقل المرتبط',
|
||||||
|
primary_table_placeholder: 'حدد الجدول',
|
||||||
|
primary_field_placeholder: 'حدد الحقل',
|
||||||
|
referenced_table_placeholder: 'حدد الجدول',
|
||||||
|
referenced_field_placeholder: 'حدد الحقل',
|
||||||
|
no_tables_found: 'لم يتم العثور على جداول',
|
||||||
|
no_fields_found: 'لم يتم العثور على حقول',
|
||||||
|
create: 'إنشاء',
|
||||||
|
cancel: 'إلغاء',
|
||||||
|
},
|
||||||
|
|
||||||
|
import_database_dialog: {
|
||||||
|
title: 'استيراد إلى المخطط الحالي',
|
||||||
|
override_alert: {
|
||||||
|
title: 'استيراد قاعدة بيانات',
|
||||||
|
content: {
|
||||||
|
alert: 'سيؤدي استيراد هذا المخطط إلى التأثير على الجداول والعلاقات الحالية.',
|
||||||
|
new_tables:
|
||||||
|
'جداول جديدة <bold>{{newTablesNumber}}</bold> سيتم إضافة',
|
||||||
|
new_relationships:
|
||||||
|
'علاقات جديدة <bold>{{newRelationshipsNumber}}</bold> سيتم إنشاء',
|
||||||
|
tables_override:
|
||||||
|
'جداول <bold>{{tablesOverrideNumber}}</bold> سيتم تعديل',
|
||||||
|
proceed: 'هل تريد المتابعة؟',
|
||||||
|
},
|
||||||
|
import: 'استيراد',
|
||||||
|
cancel: 'إلغاء',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
export_image_dialog: {
|
||||||
|
title: 'تصدير الصورة',
|
||||||
|
description: ':اختر عامل المقياس للتصدير',
|
||||||
|
scale_1x: '1x عادي',
|
||||||
|
scale_2x: '2x (موصى به)',
|
||||||
|
scale_3x: '3x',
|
||||||
|
scale_4x: '4x',
|
||||||
|
cancel: 'إلغاء',
|
||||||
|
export: 'تصدير',
|
||||||
|
},
|
||||||
|
|
||||||
|
new_table_schema_dialog: {
|
||||||
|
title: 'اختر مخططاً',
|
||||||
|
description:
|
||||||
|
'.يتم حالياً عرض مخططات متعددة. اختر واحداً للجدول الجديد',
|
||||||
|
cancel: 'إلغاء',
|
||||||
|
confirm: 'تأكيد',
|
||||||
|
},
|
||||||
|
|
||||||
|
update_table_schema_dialog: {
|
||||||
|
title: 'تغيير المخطط',
|
||||||
|
description: '"{{tableName}}" تحديث مخطط الجدول',
|
||||||
|
cancel: 'إلغاء',
|
||||||
|
confirm: 'تغيير',
|
||||||
|
},
|
||||||
|
|
||||||
|
star_us_dialog: {
|
||||||
|
title: '!ساعدنا على التحسن',
|
||||||
|
description: '؟! إنها مجرد نقرة واحدةGITHUB هل ترغب في تقييمنا على',
|
||||||
|
close: 'ليس الآن',
|
||||||
|
confirm: '!بالتأكيد',
|
||||||
|
},
|
||||||
|
export_diagram_dialog: {
|
||||||
|
title: 'تصدير المخطط',
|
||||||
|
description: ':اختر التنسيق للتصدير',
|
||||||
|
format_json: 'JSON',
|
||||||
|
cancel: 'إلغاء',
|
||||||
|
export: 'تصدير',
|
||||||
|
error: {
|
||||||
|
title: 'حدث خطأ أثناء التصدير',
|
||||||
|
description:
|
||||||
|
'chartdb.io@gmail.com حدث خطأ ما. هل تحتاج إلى مساعدة؟',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
import_diagram_dialog: {
|
||||||
|
title: 'استيراد الرسم البياني',
|
||||||
|
description: ':للرسم البياني ادناه JSON قم بلصق',
|
||||||
|
cancel: 'إلغاء',
|
||||||
|
import: 'استيراد',
|
||||||
|
error: {
|
||||||
|
title: 'حدث خطأ أثناء الاستيراد',
|
||||||
|
description:
|
||||||
|
'chartdb.io@gmail.com و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
import_dbml_dialog: {
|
||||||
|
// TODO: Translate
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
relationship_type: {
|
||||||
|
one_to_one: 'واحد إلى واحد',
|
||||||
|
one_to_many: 'واحد إلى متعدد',
|
||||||
|
many_to_one: 'متعدد إلى واحد',
|
||||||
|
many_to_many: 'متعدد إلى متعدد',
|
||||||
|
},
|
||||||
|
|
||||||
|
canvas_context_menu: {
|
||||||
|
new_table: 'جدول جديد',
|
||||||
|
new_relationship: 'علاقة جديدة',
|
||||||
|
},
|
||||||
|
|
||||||
|
table_node_context_menu: {
|
||||||
|
edit_table: 'تعديل الجدول',
|
||||||
|
duplicate_table: 'نسخ الجدول',
|
||||||
|
delete_table: 'حذف الجدول',
|
||||||
|
},
|
||||||
|
|
||||||
|
snap_to_grid_tooltip: '({{key}} مغنظة الشبكة (اضغط مع الاستمرار على',
|
||||||
|
|
||||||
|
tool_tips: {
|
||||||
|
double_click_to_edit: 'انقر مرتين للتعديل',
|
||||||
|
},
|
||||||
|
|
||||||
|
language_select: {
|
||||||
|
change_language: 'اللغة',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const arMetadata: LanguageMetadata = {
|
||||||
|
name: 'Arabic',
|
||||||
|
nativeName: 'العربية',
|
||||||
|
code: 'ar',
|
||||||
|
};
|
||||||
@@ -8,7 +8,7 @@ export const bn: LanguageTranslation = {
|
|||||||
new: 'নতুন',
|
new: 'নতুন',
|
||||||
open: 'খুলুন',
|
open: 'খুলুন',
|
||||||
save: 'সংরক্ষণ করুন',
|
save: 'সংরক্ষণ করুন',
|
||||||
import_database: 'ডাটাবেস আমদানি করুন',
|
import: 'ডাটাবেস আমদানি করুন',
|
||||||
export_sql: 'SQL রপ্তানি করুন',
|
export_sql: 'SQL রপ্তানি করুন',
|
||||||
export_as: 'রূপে রপ্তানি করুন',
|
export_as: 'রূপে রপ্তানি করুন',
|
||||||
delete_diagram: 'ডায়াগ্রাম মুছুন',
|
delete_diagram: 'ডায়াগ্রাম মুছুন',
|
||||||
@@ -30,6 +30,9 @@ export const bn: LanguageTranslation = {
|
|||||||
theme: 'থিম',
|
theme: 'থিম',
|
||||||
show_dependencies: 'নির্ভরতাগুলি দেখান',
|
show_dependencies: 'নির্ভরতাগুলি দেখান',
|
||||||
hide_dependencies: 'নির্ভরতাগুলি লুকান',
|
hide_dependencies: 'নির্ভরতাগুলি লুকান',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
|
|
||||||
share: {
|
share: {
|
||||||
@@ -102,7 +105,6 @@ export const bn: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: 'সর্বশেষ সংরক্ষণ',
|
last_saved: 'সর্বশেষ সংরক্ষণ',
|
||||||
saved: 'সংরক্ষিত',
|
saved: 'সংরক্ষিত',
|
||||||
diagrams: 'ডায়াগ্রাম',
|
|
||||||
loading_diagram: 'ডায়াগ্রাম লোড হচ্ছে...',
|
loading_diagram: 'ডায়াগ্রাম লোড হচ্ছে...',
|
||||||
deselect_all: 'সব নির্বাচন সরান',
|
deselect_all: 'সব নির্বাচন সরান',
|
||||||
select_all: 'সব নির্বাচন করুন',
|
select_all: 'সব নির্বাচন করুন',
|
||||||
@@ -123,6 +125,12 @@ export const bn: LanguageTranslation = {
|
|||||||
add_table: 'টেবিল যোগ করুন',
|
add_table: 'টেবিল যোগ করুন',
|
||||||
filter: 'ফিল্টার',
|
filter: 'ফিল্টার',
|
||||||
collapse: 'সব ভাঁজ করুন',
|
collapse: 'সব ভাঁজ করুন',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'ফিল্ড',
|
fields: 'ফিল্ড',
|
||||||
@@ -371,6 +379,17 @@ export const bn: LanguageTranslation = {
|
|||||||
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
|
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'এক থেকে এক',
|
one_to_one: 'এক থেকে এক',
|
||||||
one_to_many: 'এক থেকে অনেক',
|
one_to_many: 'এক থেকে অনেক',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const de: LanguageTranslation = {
|
|||||||
new: 'Neu',
|
new: 'Neu',
|
||||||
open: 'Öffnen',
|
open: 'Öffnen',
|
||||||
save: 'Speichern',
|
save: 'Speichern',
|
||||||
import_database: 'Datenbank importieren',
|
import: 'Datenbank importieren',
|
||||||
export_sql: 'SQL exportieren',
|
export_sql: 'SQL exportieren',
|
||||||
export_as: 'Exportieren als',
|
export_as: 'Exportieren als',
|
||||||
delete_diagram: 'Diagramm löschen',
|
delete_diagram: 'Diagramm löschen',
|
||||||
@@ -30,6 +30,9 @@ export const de: LanguageTranslation = {
|
|||||||
theme: 'Stil',
|
theme: 'Stil',
|
||||||
show_dependencies: 'Abhängigkeiten anzeigen',
|
show_dependencies: 'Abhängigkeiten anzeigen',
|
||||||
hide_dependencies: 'Abhängigkeiten ausblenden',
|
hide_dependencies: 'Abhängigkeiten ausblenden',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
share: {
|
share: {
|
||||||
@@ -103,7 +106,6 @@ export const de: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: 'Zuletzt gespeichert',
|
last_saved: 'Zuletzt gespeichert',
|
||||||
saved: 'Gespeichert',
|
saved: 'Gespeichert',
|
||||||
diagrams: 'Diagramme',
|
|
||||||
loading_diagram: 'Diagramm wird geladen...',
|
loading_diagram: 'Diagramm wird geladen...',
|
||||||
deselect_all: 'Alles abwählen',
|
deselect_all: 'Alles abwählen',
|
||||||
select_all: 'Alles auswählen',
|
select_all: 'Alles auswählen',
|
||||||
@@ -124,6 +126,12 @@ export const de: LanguageTranslation = {
|
|||||||
add_table: 'Tabelle hinzufügen',
|
add_table: 'Tabelle hinzufügen',
|
||||||
filter: 'Filter',
|
filter: 'Filter',
|
||||||
collapse: 'Alle einklappen',
|
collapse: 'Alle einklappen',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Felder',
|
fields: 'Felder',
|
||||||
@@ -374,6 +382,17 @@ export const de: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Ein zu Eins (1:1)',
|
one_to_one: 'Ein zu Eins (1:1)',
|
||||||
one_to_many: 'Ein zu Viele (1:n)',
|
one_to_many: 'Ein zu Viele (1:n)',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const en = {
|
|||||||
new: 'New',
|
new: 'New',
|
||||||
open: 'Open',
|
open: 'Open',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
import_database: 'Import Database',
|
import: 'Import',
|
||||||
export_sql: 'Export SQL',
|
export_sql: 'Export SQL',
|
||||||
export_as: 'Export as',
|
export_as: 'Export as',
|
||||||
delete_diagram: 'Delete Diagram',
|
delete_diagram: 'Delete Diagram',
|
||||||
@@ -30,6 +30,8 @@ export const en = {
|
|||||||
theme: 'Theme',
|
theme: 'Theme',
|
||||||
show_dependencies: 'Show Dependencies',
|
show_dependencies: 'Show Dependencies',
|
||||||
hide_dependencies: 'Hide Dependencies',
|
hide_dependencies: 'Hide Dependencies',
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
share: 'Share',
|
share: 'Share',
|
||||||
@@ -101,7 +103,6 @@ export const en = {
|
|||||||
|
|
||||||
last_saved: 'Last saved',
|
last_saved: 'Last saved',
|
||||||
saved: 'Saved',
|
saved: 'Saved',
|
||||||
diagrams: 'Diagrams',
|
|
||||||
loading_diagram: 'Loading diagram...',
|
loading_diagram: 'Loading diagram...',
|
||||||
deselect_all: 'Deselect All',
|
deselect_all: 'Deselect All',
|
||||||
select_all: 'Select All',
|
select_all: 'Select All',
|
||||||
@@ -122,6 +123,10 @@ export const en = {
|
|||||||
add_table: 'Add Table',
|
add_table: 'Add Table',
|
||||||
filter: 'Filter',
|
filter: 'Filter',
|
||||||
collapse: 'Collapse All',
|
collapse: 'Collapse All',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Fields',
|
fields: 'Fields',
|
||||||
@@ -360,7 +365,7 @@ export const en = {
|
|||||||
|
|
||||||
import_diagram_dialog: {
|
import_diagram_dialog: {
|
||||||
title: 'Import Diagram',
|
title: 'Import Diagram',
|
||||||
description: 'Paste the diagram JSON below:',
|
description: 'Import a diagram from a JSON file.',
|
||||||
cancel: 'Cancel',
|
cancel: 'Cancel',
|
||||||
import: 'Import',
|
import: 'Import',
|
||||||
error: {
|
error: {
|
||||||
@@ -369,6 +374,17 @@ export const en = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error importing DBML',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'One to One',
|
one_to_one: 'One to One',
|
||||||
one_to_many: 'One to Many',
|
one_to_many: 'One to Many',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const es: LanguageTranslation = {
|
|||||||
new: 'Nuevo',
|
new: 'Nuevo',
|
||||||
open: 'Abrir',
|
open: 'Abrir',
|
||||||
save: 'Guardar',
|
save: 'Guardar',
|
||||||
import_database: 'Importar Base de Datos',
|
import: 'Importar Base de Datos',
|
||||||
export_sql: 'Exportar SQL',
|
export_sql: 'Exportar SQL',
|
||||||
export_as: 'Exportar como',
|
export_as: 'Exportar como',
|
||||||
delete_diagram: 'Eliminar Diagrama',
|
delete_diagram: 'Eliminar Diagrama',
|
||||||
@@ -30,6 +30,9 @@ export const es: LanguageTranslation = {
|
|||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
show_dependencies: 'Mostrar dependencias',
|
show_dependencies: 'Mostrar dependencias',
|
||||||
hide_dependencies: 'Ocultar dependencias',
|
hide_dependencies: 'Ocultar dependencias',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
share: {
|
share: {
|
||||||
@@ -93,7 +96,6 @@ export const es: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: 'Último guardado',
|
last_saved: 'Último guardado',
|
||||||
saved: 'Guardado',
|
saved: 'Guardado',
|
||||||
diagrams: 'Diagramas',
|
|
||||||
loading_diagram: 'Cargando diagrama...',
|
loading_diagram: 'Cargando diagrama...',
|
||||||
deselect_all: 'Deseleccionar todo',
|
deselect_all: 'Deseleccionar todo',
|
||||||
select_all: 'Seleccionar todo',
|
select_all: 'Seleccionar todo',
|
||||||
@@ -114,6 +116,12 @@ export const es: LanguageTranslation = {
|
|||||||
add_table: 'Agregar Tabla',
|
add_table: 'Agregar Tabla',
|
||||||
filter: 'Filtrar',
|
filter: 'Filtrar',
|
||||||
collapse: 'Colapsar Todo',
|
collapse: 'Colapsar Todo',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Campos',
|
fields: 'Campos',
|
||||||
@@ -373,6 +381,17 @@ export const es: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Uno a Uno',
|
one_to_one: 'Uno a Uno',
|
||||||
one_to_many: 'Uno a Muchos',
|
one_to_many: 'Uno a Muchos',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const fr: LanguageTranslation = {
|
|||||||
new: 'Nouveau',
|
new: 'Nouveau',
|
||||||
open: 'Ouvrir',
|
open: 'Ouvrir',
|
||||||
save: 'Enregistrer',
|
save: 'Enregistrer',
|
||||||
import_database: 'Importer Base de Données',
|
import: 'Importer Base de Données',
|
||||||
export_sql: 'Exporter SQL',
|
export_sql: 'Exporter SQL',
|
||||||
export_as: 'Exporter en tant que',
|
export_as: 'Exporter en tant que',
|
||||||
delete_diagram: 'Supprimer le Diagramme',
|
delete_diagram: 'Supprimer le Diagramme',
|
||||||
@@ -30,6 +30,9 @@ export const fr: LanguageTranslation = {
|
|||||||
theme: 'Thème',
|
theme: 'Thème',
|
||||||
show_dependencies: 'Afficher les Dépendances',
|
show_dependencies: 'Afficher les Dépendances',
|
||||||
hide_dependencies: 'Masquer les Dépendances',
|
hide_dependencies: 'Masquer les Dépendances',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
share: 'Partage',
|
share: 'Partage',
|
||||||
@@ -92,7 +95,6 @@ export const fr: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: 'Dernière sauvegarde',
|
last_saved: 'Dernière sauvegarde',
|
||||||
saved: 'Enregistré',
|
saved: 'Enregistré',
|
||||||
diagrams: 'Diagrammes',
|
|
||||||
loading_diagram: 'Chargement du diagramme...',
|
loading_diagram: 'Chargement du diagramme...',
|
||||||
deselect_all: 'Tout désélectionner',
|
deselect_all: 'Tout désélectionner',
|
||||||
select_all: 'Tout sélectionner',
|
select_all: 'Tout sélectionner',
|
||||||
@@ -114,6 +116,12 @@ export const fr: LanguageTranslation = {
|
|||||||
add_table: 'Ajouter une Table',
|
add_table: 'Ajouter une Table',
|
||||||
filter: 'Filtrer',
|
filter: 'Filtrer',
|
||||||
collapse: 'Réduire Tout',
|
collapse: 'Réduire Tout',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Champs',
|
fields: 'Champs',
|
||||||
@@ -375,6 +383,17 @@ export const fr: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Un à Un',
|
one_to_one: 'Un à Un',
|
||||||
one_to_many: 'Un à Plusieurs',
|
one_to_many: 'Un à Plusieurs',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const gu: LanguageTranslation = {
|
|||||||
new: 'નવું',
|
new: 'નવું',
|
||||||
open: 'ખોલો',
|
open: 'ખોલો',
|
||||||
save: 'સાચવો',
|
save: 'સાચવો',
|
||||||
import_database: 'ડેટાબેસ આયાત કરો',
|
import: 'ડેટાબેસ આયાત કરો',
|
||||||
export_sql: 'SQL નિકાસ કરો',
|
export_sql: 'SQL નિકાસ કરો',
|
||||||
export_as: 'રૂપે નિકાસ કરો',
|
export_as: 'રૂપે નિકાસ કરો',
|
||||||
delete_diagram: 'ડાયાગ્રામ કાઢી નાખો',
|
delete_diagram: 'ડાયાગ્રામ કાઢી નાખો',
|
||||||
@@ -30,6 +30,9 @@ export const gu: LanguageTranslation = {
|
|||||||
theme: 'થિમ',
|
theme: 'થિમ',
|
||||||
show_dependencies: 'નિર્ભરતાઓ બતાવો',
|
show_dependencies: 'નિર્ભરતાઓ બતાવો',
|
||||||
hide_dependencies: 'નિર્ભરતાઓ છુપાવો',
|
hide_dependencies: 'નિર્ભરતાઓ છુપાવો',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
|
|
||||||
share: {
|
share: {
|
||||||
@@ -102,7 +105,6 @@ export const gu: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: 'છેલ્લે સાચવ્યું',
|
last_saved: 'છેલ્લે સાચવ્યું',
|
||||||
saved: 'સાચવ્યું',
|
saved: 'સાચવ્યું',
|
||||||
diagrams: 'ડાયાગ્રામ',
|
|
||||||
loading_diagram: 'ડાયાગ્રામ લોડ થઈ રહ્યું છે...',
|
loading_diagram: 'ડાયાગ્રામ લોડ થઈ રહ્યું છે...',
|
||||||
deselect_all: 'બધાને ડીસેલેક્ટ કરો',
|
deselect_all: 'બધાને ડીસેલેક્ટ કરો',
|
||||||
select_all: 'બધા પસંદ કરો',
|
select_all: 'બધા પસંદ કરો',
|
||||||
@@ -123,6 +125,12 @@ export const gu: LanguageTranslation = {
|
|||||||
add_table: 'ટેબલ ઉમેરો',
|
add_table: 'ટેબલ ઉમેરો',
|
||||||
filter: 'ફિલ્ટર',
|
filter: 'ફિલ્ટર',
|
||||||
collapse: 'બધાને સકુચિત કરો',
|
collapse: 'બધાને સકુચિત કરો',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'ફીલ્ડ્સ',
|
fields: 'ફીલ્ડ્સ',
|
||||||
@@ -371,6 +379,17 @@ export const gu: LanguageTranslation = {
|
|||||||
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
|
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'એકથી એક',
|
one_to_one: 'એકથી એક',
|
||||||
one_to_many: 'એકથી ઘણા',
|
one_to_many: 'એકથી ઘણા',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const hi: LanguageTranslation = {
|
|||||||
new: 'नया',
|
new: 'नया',
|
||||||
open: 'खोलें',
|
open: 'खोलें',
|
||||||
save: 'सहेजें',
|
save: 'सहेजें',
|
||||||
import_database: 'डेटाबेस आयात करें',
|
import: 'डेटाबेस आयात करें',
|
||||||
export_sql: 'SQL निर्यात करें',
|
export_sql: 'SQL निर्यात करें',
|
||||||
export_as: 'के रूप में निर्यात करें',
|
export_as: 'के रूप में निर्यात करें',
|
||||||
delete_diagram: 'आरेख हटाएँ',
|
delete_diagram: 'आरेख हटाएँ',
|
||||||
@@ -30,6 +30,9 @@ export const hi: LanguageTranslation = {
|
|||||||
theme: 'थीम',
|
theme: 'थीम',
|
||||||
show_dependencies: 'निर्भरता दिखाएँ',
|
show_dependencies: 'निर्भरता दिखाएँ',
|
||||||
hide_dependencies: 'निर्भरता छिपाएँ',
|
hide_dependencies: 'निर्भरता छिपाएँ',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
share: {
|
share: {
|
||||||
@@ -102,7 +105,6 @@ export const hi: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: 'अंतिम सहेजा गया',
|
last_saved: 'अंतिम सहेजा गया',
|
||||||
saved: 'सहेजा गया',
|
saved: 'सहेजा गया',
|
||||||
diagrams: 'आरेख',
|
|
||||||
loading_diagram: 'आरेख लोड हो रहा है...',
|
loading_diagram: 'आरेख लोड हो रहा है...',
|
||||||
deselect_all: 'सभी को अचयनित करें',
|
deselect_all: 'सभी को अचयनित करें',
|
||||||
select_all: 'सभी को चुनें',
|
select_all: 'सभी को चुनें',
|
||||||
@@ -124,6 +126,12 @@ export const hi: LanguageTranslation = {
|
|||||||
add_table: 'तालिका जोड़ें',
|
add_table: 'तालिका जोड़ें',
|
||||||
filter: 'फ़िल्टर',
|
filter: 'फ़िल्टर',
|
||||||
collapse: 'सभी को संक्षिप्त करें',
|
collapse: 'सभी को संक्षिप्त करें',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'फ़ील्ड्स',
|
fields: 'फ़ील्ड्स',
|
||||||
@@ -375,6 +383,17 @@ export const hi: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'एक से एक',
|
one_to_one: 'एक से एक',
|
||||||
one_to_many: 'एक से कई',
|
one_to_many: 'एक से कई',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const id_ID: LanguageTranslation = {
|
|||||||
new: 'Buat Baru',
|
new: 'Buat Baru',
|
||||||
open: 'Buka',
|
open: 'Buka',
|
||||||
save: 'Simpan',
|
save: 'Simpan',
|
||||||
import_database: 'Impor Database',
|
import: 'Impor Database',
|
||||||
export_sql: 'Ekspor SQL',
|
export_sql: 'Ekspor SQL',
|
||||||
export_as: 'Ekspor Sebagai',
|
export_as: 'Ekspor Sebagai',
|
||||||
delete_diagram: 'Hapus Diagram',
|
delete_diagram: 'Hapus Diagram',
|
||||||
@@ -30,6 +30,9 @@ export const id_ID: LanguageTranslation = {
|
|||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
show_dependencies: 'Tampilkan Dependensi',
|
show_dependencies: 'Tampilkan Dependensi',
|
||||||
hide_dependencies: 'Sembunyikan Dependensi',
|
hide_dependencies: 'Sembunyikan Dependensi',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
share: 'Bagikan',
|
share: 'Bagikan',
|
||||||
@@ -101,7 +104,6 @@ export const id_ID: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: 'Terakhir disimpan',
|
last_saved: 'Terakhir disimpan',
|
||||||
saved: 'Tersimpan',
|
saved: 'Tersimpan',
|
||||||
diagrams: 'Diagram',
|
|
||||||
loading_diagram: 'Memuat diagram...',
|
loading_diagram: 'Memuat diagram...',
|
||||||
deselect_all: 'Batalkan Semua',
|
deselect_all: 'Batalkan Semua',
|
||||||
select_all: 'Pilih Semua',
|
select_all: 'Pilih Semua',
|
||||||
@@ -122,6 +124,12 @@ export const id_ID: LanguageTranslation = {
|
|||||||
add_table: 'Tambah Tabel',
|
add_table: 'Tambah Tabel',
|
||||||
filter: 'Saring',
|
filter: 'Saring',
|
||||||
collapse: 'Lipat Semua',
|
collapse: 'Lipat Semua',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Kolom',
|
fields: 'Kolom',
|
||||||
@@ -369,6 +377,17 @@ export const id_ID: LanguageTranslation = {
|
|||||||
'Diagram JSON tidak valid. Silakan cek JSON dan coba lagi. Butuh bantuan? chartdb.io@gmail.com',
|
'Diagram JSON tidak valid. Silakan cek JSON dan coba lagi. Butuh bantuan? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Satu ke Satu',
|
one_to_one: 'Satu ke Satu',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const ja: LanguageTranslation = {
|
|||||||
new: '新規',
|
new: '新規',
|
||||||
open: '開く',
|
open: '開く',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
import_database: 'データベースをインポート',
|
import: 'データベースをインポート',
|
||||||
export_sql: 'SQLをエクスポート',
|
export_sql: 'SQLをエクスポート',
|
||||||
export_as: '形式を指定してエクスポート',
|
export_as: '形式を指定してエクスポート',
|
||||||
delete_diagram: 'ダイアグラムを削除',
|
delete_diagram: 'ダイアグラムを削除',
|
||||||
@@ -31,6 +31,9 @@ export const ja: LanguageTranslation = {
|
|||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
show_dependencies: 'Show Dependencies',
|
show_dependencies: 'Show Dependencies',
|
||||||
hide_dependencies: 'Hide Dependencies',
|
hide_dependencies: 'Hide Dependencies',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
share: {
|
share: {
|
||||||
@@ -104,7 +107,6 @@ export const ja: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: '最後に保存された',
|
last_saved: '最後に保存された',
|
||||||
saved: '保存されました',
|
saved: '保存されました',
|
||||||
diagrams: 'ダイアグラム',
|
|
||||||
loading_diagram: 'ダイアグラムを読み込み中...',
|
loading_diagram: 'ダイアグラムを読み込み中...',
|
||||||
deselect_all: 'すべての選択を解除',
|
deselect_all: 'すべての選択を解除',
|
||||||
select_all: 'すべてを選択',
|
select_all: 'すべてを選択',
|
||||||
@@ -126,6 +128,12 @@ export const ja: LanguageTranslation = {
|
|||||||
add_table: 'テーブルを追加',
|
add_table: 'テーブルを追加',
|
||||||
filter: 'フィルタ',
|
filter: 'フィルタ',
|
||||||
collapse: 'すべて折りたたむ',
|
collapse: 'すべて折りたたむ',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'フィールド',
|
fields: 'フィールド',
|
||||||
@@ -378,6 +386,17 @@ export const ja: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: '1対1',
|
one_to_one: '1対1',
|
||||||
one_to_many: '1対多',
|
one_to_many: '1対多',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
new: '새 다이어그램',
|
new: '새 다이어그램',
|
||||||
open: '열기',
|
open: '열기',
|
||||||
save: '저장',
|
save: '저장',
|
||||||
import_database: '데이터베이스 가져오기',
|
import: '데이터베이스 가져오기',
|
||||||
export_sql: 'SQL로 저장',
|
export_sql: 'SQL로 저장',
|
||||||
export_as: '다른 형식으로 저장',
|
export_as: '다른 형식으로 저장',
|
||||||
delete_diagram: '다이어그램 삭제',
|
delete_diagram: '다이어그램 삭제',
|
||||||
@@ -30,6 +30,9 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
theme: '테마',
|
theme: '테마',
|
||||||
show_dependencies: '종속성 보이기',
|
show_dependencies: '종속성 보이기',
|
||||||
hide_dependencies: '종속성 숨기기',
|
hide_dependencies: '종속성 숨기기',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
share: '공유',
|
share: '공유',
|
||||||
@@ -101,7 +104,6 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: '최근 저장일시: ',
|
last_saved: '최근 저장일시: ',
|
||||||
saved: '저장됨',
|
saved: '저장됨',
|
||||||
diagrams: '다이어그램',
|
|
||||||
loading_diagram: '다이어그램 로딩중...',
|
loading_diagram: '다이어그램 로딩중...',
|
||||||
deselect_all: '모두 선택 해제',
|
deselect_all: '모두 선택 해제',
|
||||||
select_all: '모두 선택',
|
select_all: '모두 선택',
|
||||||
@@ -122,6 +124,12 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
add_table: '테이블 추가',
|
add_table: '테이블 추가',
|
||||||
filter: '필터',
|
filter: '필터',
|
||||||
collapse: '모두 접기',
|
collapse: '모두 접기',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: '필드',
|
fields: '필드',
|
||||||
@@ -367,6 +375,17 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
|
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: '일대일 (1:1)',
|
one_to_one: '일대일 (1:1)',
|
||||||
one_to_many: '일대다 (1:N)',
|
one_to_many: '일대다 (1:N)',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const mr: LanguageTranslation = {
|
|||||||
new: 'नवीन',
|
new: 'नवीन',
|
||||||
open: 'उघडा',
|
open: 'उघडा',
|
||||||
save: 'जतन करा',
|
save: 'जतन करा',
|
||||||
import_database: 'डेटाबेस इम्पोर्ट करा',
|
import: 'डेटाबेस इम्पोर्ट करा',
|
||||||
export_sql: 'SQL एक्स्पोर्ट करा',
|
export_sql: 'SQL एक्स्पोर्ट करा',
|
||||||
export_as: 'म्हणून एक्स्पोर्ट करा',
|
export_as: 'म्हणून एक्स्पोर्ट करा',
|
||||||
delete_diagram: 'आरेख हटवा',
|
delete_diagram: 'आरेख हटवा',
|
||||||
@@ -30,6 +30,9 @@ export const mr: LanguageTranslation = {
|
|||||||
theme: 'थीम',
|
theme: 'थीम',
|
||||||
show_dependencies: 'डिपेंडेन्सि दाखवा',
|
show_dependencies: 'डिपेंडेन्सि दाखवा',
|
||||||
hide_dependencies: 'डिपेंडेन्सि लपवा',
|
hide_dependencies: 'डिपेंडेन्सि लपवा',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
// TODO: Add translations
|
// TODO: Add translations
|
||||||
@@ -102,7 +105,6 @@ export const mr: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: 'शेवटचे जतन केले',
|
last_saved: 'शेवटचे जतन केले',
|
||||||
saved: 'जतन केले',
|
saved: 'जतन केले',
|
||||||
diagrams: 'आरेख',
|
|
||||||
loading_diagram: 'आरेख लोड करत आहे...',
|
loading_diagram: 'आरेख लोड करत आहे...',
|
||||||
deselect_all: 'सर्व निवड रद्द करा',
|
deselect_all: 'सर्व निवड रद्द करा',
|
||||||
select_all: 'सर्व निवडा',
|
select_all: 'सर्व निवडा',
|
||||||
@@ -125,6 +127,12 @@ export const mr: LanguageTranslation = {
|
|||||||
add_table: 'टेबल जोडा',
|
add_table: 'टेबल जोडा',
|
||||||
filter: 'फिल्टर',
|
filter: 'फिल्टर',
|
||||||
collapse: 'सर्व संकुचित करा',
|
collapse: 'सर्व संकुचित करा',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'फील्ड्स',
|
fields: 'फील्ड्स',
|
||||||
@@ -379,6 +387,17 @@ export const mr: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'एक ते एक',
|
one_to_one: 'एक ते एक',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const ne: LanguageTranslation = {
|
|||||||
new: 'नयाँ',
|
new: 'नयाँ',
|
||||||
open: 'खोल्नुहोस्',
|
open: 'खोल्नुहोस्',
|
||||||
save: 'सुरक्षित गर्नुहोस्',
|
save: 'सुरक्षित गर्नुहोस्',
|
||||||
import_database: 'डाटाबेस आयात गर्नुहोस्',
|
import: 'डाटाबेस आयात गर्नुहोस्',
|
||||||
export_sql: 'SQL निर्यात गर्नुहोस्',
|
export_sql: 'SQL निर्यात गर्नुहोस्',
|
||||||
export_as: 'निर्यात गर्नुहोस्',
|
export_as: 'निर्यात गर्नुहोस्',
|
||||||
delete_diagram: 'डायाग्राम हटाउनुहोस्',
|
delete_diagram: 'डायाग्राम हटाउनुहोस्',
|
||||||
@@ -30,6 +30,9 @@ export const ne: LanguageTranslation = {
|
|||||||
theme: 'थिम',
|
theme: 'थिम',
|
||||||
show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
|
show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
|
||||||
hide_dependencies: 'डिपेन्डेन्सीहरू लुकाउनुहोस्',
|
hide_dependencies: 'डिपेन्डेन्सीहरू लुकाउनुहोस्',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
share: 'शेयर गर्नुहोस्',
|
share: 'शेयर गर्नुहोस्',
|
||||||
@@ -101,7 +104,6 @@ export const ne: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: 'अन्तिम सुरक्षित',
|
last_saved: 'अन्तिम सुरक्षित',
|
||||||
saved: 'सुरक्षित',
|
saved: 'सुरक्षित',
|
||||||
diagrams: 'डायाग्रामहरू',
|
|
||||||
loading_diagram: 'डायाग्राम लोड हुँदैछ...',
|
loading_diagram: 'डायाग्राम लोड हुँदैछ...',
|
||||||
deselect_all: 'सबै चयन हटाउनुहोस्',
|
deselect_all: 'सबै चयन हटाउनुहोस्',
|
||||||
select_all: 'सबै चयन गर्नुहोस्',
|
select_all: 'सबै चयन गर्नुहोस्',
|
||||||
@@ -122,6 +124,12 @@ export const ne: LanguageTranslation = {
|
|||||||
add_table: 'तालिका थप्नुहोस्',
|
add_table: 'तालिका थप्नुहोस्',
|
||||||
filter: 'फिल्टर',
|
filter: 'फिल्टर',
|
||||||
collapse: 'सबै लुकाउनुहोस्',
|
collapse: 'सबै लुकाउनुहोस्',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'क्षेत्रहरू',
|
fields: 'क्षेत्रहरू',
|
||||||
@@ -372,6 +380,17 @@ export const ne: LanguageTranslation = {
|
|||||||
'डायाग्राम JSON अमान्य छ। कृपया JSON जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्। मद्दत चाहिन्छ? chartdb.io@gmail.com मा सम्पर्क गर्नुहोस्',
|
'डायाग्राम JSON अमान्य छ। कृपया JSON जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्। मद्दत चाहिन्छ? chartdb.io@gmail.com मा सम्पर्क गर्नुहोस्',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'एक देखि एक',
|
one_to_one: 'एक देखि एक',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
new: 'Novo',
|
new: 'Novo',
|
||||||
open: 'Abrir',
|
open: 'Abrir',
|
||||||
save: 'Salvar',
|
save: 'Salvar',
|
||||||
import_database: 'Importar Banco de Dados',
|
import: 'Importar Banco de Dados',
|
||||||
export_sql: 'Exportar SQL',
|
export_sql: 'Exportar SQL',
|
||||||
export_as: 'Exportar como',
|
export_as: 'Exportar como',
|
||||||
delete_diagram: 'Excluir Diagrama',
|
delete_diagram: 'Excluir Diagrama',
|
||||||
@@ -30,6 +30,9 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
show_dependencies: 'Mostrar Dependências',
|
show_dependencies: 'Mostrar Dependências',
|
||||||
hide_dependencies: 'Ocultar Dependências',
|
hide_dependencies: 'Ocultar Dependências',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
share: {
|
share: {
|
||||||
@@ -102,7 +105,6 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: 'Última vez salvo',
|
last_saved: 'Última vez salvo',
|
||||||
saved: 'Salvo',
|
saved: 'Salvo',
|
||||||
diagrams: 'Diagramas',
|
|
||||||
loading_diagram: 'Carregando diagrama...',
|
loading_diagram: 'Carregando diagrama...',
|
||||||
deselect_all: 'Desmarcar Todos',
|
deselect_all: 'Desmarcar Todos',
|
||||||
select_all: 'Selecionar Todos',
|
select_all: 'Selecionar Todos',
|
||||||
@@ -123,6 +125,12 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
add_table: 'Adicionar Tabela',
|
add_table: 'Adicionar Tabela',
|
||||||
filter: 'Filtrar',
|
filter: 'Filtrar',
|
||||||
collapse: 'Colapsar Todas',
|
collapse: 'Colapsar Todas',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Campos',
|
fields: 'Campos',
|
||||||
@@ -372,6 +380,17 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Um para Um',
|
one_to_one: 'Um para Um',
|
||||||
one_to_many: 'Um para Muitos',
|
one_to_many: 'Um para Muitos',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const ru: LanguageTranslation = {
|
|||||||
new: 'Создать',
|
new: 'Создать',
|
||||||
open: 'Открыть',
|
open: 'Открыть',
|
||||||
save: 'Сохранить',
|
save: 'Сохранить',
|
||||||
import_database: 'Импортировать базу данных',
|
import: 'Импортировать базу данных',
|
||||||
export_sql: 'Экспорт SQL',
|
export_sql: 'Экспорт SQL',
|
||||||
export_as: 'Экспортировать как',
|
export_as: 'Экспортировать как',
|
||||||
delete_diagram: 'Удалить диаграмму',
|
delete_diagram: 'Удалить диаграмму',
|
||||||
@@ -30,6 +30,9 @@ export const ru: LanguageTranslation = {
|
|||||||
theme: 'Тема',
|
theme: 'Тема',
|
||||||
show_dependencies: 'Показать зависимости',
|
show_dependencies: 'Показать зависимости',
|
||||||
hide_dependencies: 'Скрыть зависимости',
|
hide_dependencies: 'Скрыть зависимости',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
share: 'Поделиться',
|
share: 'Поделиться',
|
||||||
@@ -102,7 +105,6 @@ export const ru: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: 'Последнее сохранение',
|
last_saved: 'Последнее сохранение',
|
||||||
saved: 'Сохранено',
|
saved: 'Сохранено',
|
||||||
diagrams: 'Диаграммы',
|
|
||||||
loading_diagram: 'Загрузка диаграммы...',
|
loading_diagram: 'Загрузка диаграммы...',
|
||||||
deselect_all: 'Отменить выбор всех',
|
deselect_all: 'Отменить выбор всех',
|
||||||
select_all: 'Выбрать все',
|
select_all: 'Выбрать все',
|
||||||
@@ -121,6 +123,12 @@ export const ru: LanguageTranslation = {
|
|||||||
add_table: 'Добавить таблицу',
|
add_table: 'Добавить таблицу',
|
||||||
filter: 'Фильтр',
|
filter: 'Фильтр',
|
||||||
collapse: 'Свернуть все',
|
collapse: 'Свернуть все',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Поля',
|
fields: 'Поля',
|
||||||
@@ -368,6 +376,17 @@ export const ru: LanguageTranslation = {
|
|||||||
'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: chartdb.io@gmail.com',
|
'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Один к одному',
|
one_to_one: 'Один к одному',
|
||||||
one_to_many: 'Один ко многим',
|
one_to_many: 'Один ко многим',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const te: LanguageTranslation = {
|
|||||||
new: 'కొత్తది',
|
new: 'కొత్తది',
|
||||||
open: 'తెరవు',
|
open: 'తెరవు',
|
||||||
save: 'సేవ్',
|
save: 'సేవ్',
|
||||||
import_database: 'డేటాబేస్ను దిగుమతి చేసుకోండి',
|
import: 'డేటాబేస్ను దిగుమతి చేసుకోండి',
|
||||||
export_sql: 'SQL ఎగుమతి',
|
export_sql: 'SQL ఎగుమతి',
|
||||||
export_as: 'వగా ఎగుమతి చేయండి',
|
export_as: 'వగా ఎగుమతి చేయండి',
|
||||||
delete_diagram: 'చిత్రాన్ని తొలగించండి',
|
delete_diagram: 'చిత్రాన్ని తొలగించండి',
|
||||||
@@ -30,6 +30,9 @@ export const te: LanguageTranslation = {
|
|||||||
theme: 'థీమ్',
|
theme: 'థీమ్',
|
||||||
show_dependencies: 'ఆధారాలు చూపించండి',
|
show_dependencies: 'ఆధారాలు చూపించండి',
|
||||||
hide_dependencies: 'ఆధారాలను దాచండి',
|
hide_dependencies: 'ఆధారాలను దాచండి',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
share: {
|
share: {
|
||||||
@@ -102,7 +105,6 @@ export const te: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: 'చివరిగా సేవ్ చేయబడిన',
|
last_saved: 'చివరిగా సేవ్ చేయబడిన',
|
||||||
saved: 'సేవ్ చేయబడింది',
|
saved: 'సేవ్ చేయబడింది',
|
||||||
diagrams: 'చిత్రాలు',
|
|
||||||
loading_diagram: 'చిత్రం లోడ్ అవుతోంది...',
|
loading_diagram: 'చిత్రం లోడ్ అవుతోంది...',
|
||||||
deselect_all: 'అన్ని ఎంచుకోకుండా ఉంచు',
|
deselect_all: 'అన్ని ఎంచుకోకుండా ఉంచు',
|
||||||
select_all: 'అన్ని ఎంచుకోండి',
|
select_all: 'అన్ని ఎంచుకోండి',
|
||||||
@@ -123,6 +125,12 @@ export const te: LanguageTranslation = {
|
|||||||
add_table: 'పట్టికను జోడించు',
|
add_table: 'పట్టికను జోడించు',
|
||||||
filter: 'ఫిల్టర్',
|
filter: 'ఫిల్టర్',
|
||||||
collapse: 'అన్ని కూల్ చేయి',
|
collapse: 'అన్ని కూల్ చేయి',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'ఫీల్డులు',
|
fields: 'ఫీల్డులు',
|
||||||
@@ -375,6 +383,17 @@ export const te: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'ఒకటి_కీ_ఒకటి',
|
one_to_one: 'ఒకటి_కీ_ఒకటి',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const tr: LanguageTranslation = {
|
|||||||
new: 'Yeni',
|
new: 'Yeni',
|
||||||
open: 'Aç',
|
open: 'Aç',
|
||||||
save: 'Kaydet',
|
save: 'Kaydet',
|
||||||
import_database: 'Veritabanı İçe Aktar',
|
import: 'Veritabanı İçe Aktar',
|
||||||
export_sql: 'SQL Olarak Dışa Aktar',
|
export_sql: 'SQL Olarak Dışa Aktar',
|
||||||
export_as: 'Olarak Dışa Aktar',
|
export_as: 'Olarak Dışa Aktar',
|
||||||
delete_diagram: 'Diyagramı Sil',
|
delete_diagram: 'Diyagramı Sil',
|
||||||
@@ -30,6 +30,9 @@ export const tr: LanguageTranslation = {
|
|||||||
theme: 'Tema',
|
theme: 'Tema',
|
||||||
show_dependencies: 'Bağımlılıkları Göster',
|
show_dependencies: 'Bağımlılıkları Göster',
|
||||||
hide_dependencies: 'Bağımlılıkları Gizle',
|
hide_dependencies: 'Bağımlılıkları Gizle',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
share: {
|
share: {
|
||||||
@@ -102,8 +105,6 @@ export const tr: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: 'Son kaydedilen',
|
last_saved: 'Son kaydedilen',
|
||||||
saved: 'Kaydedildi',
|
saved: 'Kaydedildi',
|
||||||
diagrams: 'Diyagramlar',
|
|
||||||
|
|
||||||
loading_diagram: 'Diyagram yükleniyor...',
|
loading_diagram: 'Diyagram yükleniyor...',
|
||||||
deselect_all: 'Hepsini Seçme',
|
deselect_all: 'Hepsini Seçme',
|
||||||
select_all: 'Hepsini Seç',
|
select_all: 'Hepsini Seç',
|
||||||
@@ -123,6 +124,13 @@ export const tr: LanguageTranslation = {
|
|||||||
add_table: 'Tablo Ekle',
|
add_table: 'Tablo Ekle',
|
||||||
filter: 'Filtrele',
|
filter: 'Filtrele',
|
||||||
collapse: 'Hepsini Daralt',
|
collapse: 'Hepsini Daralt',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Alanlar',
|
fields: 'Alanlar',
|
||||||
nullable: 'Boş Bırakılabilir?',
|
nullable: 'Boş Bırakılabilir?',
|
||||||
@@ -362,6 +370,17 @@ export const tr: LanguageTranslation = {
|
|||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Bir Bir',
|
one_to_one: 'Bir Bir',
|
||||||
one_to_many: 'Bir Çok',
|
one_to_many: 'Bir Çok',
|
||||||
|
|||||||
@@ -4,24 +4,24 @@ export const uk: LanguageTranslation = {
|
|||||||
translation: {
|
translation: {
|
||||||
menu: {
|
menu: {
|
||||||
file: {
|
file: {
|
||||||
file: 'файл',
|
file: 'Файл',
|
||||||
new: 'новий',
|
new: 'Новий',
|
||||||
open: 'відкрити',
|
open: 'Відкрити',
|
||||||
save: 'зберегти',
|
save: 'Зберегти',
|
||||||
import_database: 'Імпорт бази даних',
|
import: 'Імпорт бази даних',
|
||||||
export_sql: 'Експорт SQL',
|
export_sql: 'Експорт SQL',
|
||||||
export_as: 'Експортувати як',
|
export_as: 'Експортувати як',
|
||||||
delete_diagram: 'Видалити діаграму',
|
delete_diagram: 'Видалити діаграму',
|
||||||
exit: 'вийти',
|
exit: 'Вийти',
|
||||||
},
|
},
|
||||||
edit: {
|
edit: {
|
||||||
edit: 'редагувати',
|
edit: 'Редагувати',
|
||||||
undo: 'Скасувати',
|
undo: 'Скасувати',
|
||||||
redo: 'Повторити',
|
redo: 'Повторити',
|
||||||
clear: 'очистити',
|
clear: 'Очистити',
|
||||||
},
|
},
|
||||||
view: {
|
view: {
|
||||||
view: 'переглянути',
|
view: 'Перегляд',
|
||||||
show_sidebar: 'Показати бічну панель',
|
show_sidebar: 'Показати бічну панель',
|
||||||
hide_sidebar: 'Приховати бічну панель',
|
hide_sidebar: 'Приховати бічну панель',
|
||||||
hide_cardinality: 'Приховати потужність',
|
hide_cardinality: 'Приховати потужність',
|
||||||
@@ -30,18 +30,19 @@ export const uk: LanguageTranslation = {
|
|||||||
theme: 'Тема',
|
theme: 'Тема',
|
||||||
show_dependencies: 'Показати залежності',
|
show_dependencies: 'Показати залежності',
|
||||||
hide_dependencies: 'Приховати залежності',
|
hide_dependencies: 'Приховати залежності',
|
||||||
|
show_minimap: 'Показати мінімапу',
|
||||||
|
hide_minimap: 'Приховати мінімапу',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
|
||||||
share: {
|
share: {
|
||||||
share: 'Share',
|
share: 'Поширити',
|
||||||
export_diagram: 'Export Diagram',
|
export_diagram: 'Експорт діаграми',
|
||||||
import_diagram: 'Import Diagram',
|
import_diagram: 'Імпорт діаграми',
|
||||||
},
|
},
|
||||||
help: {
|
help: {
|
||||||
help: 'Допомога',
|
help: 'Довідка',
|
||||||
visit_website: 'Відвідайте ChartDB',
|
visit_website: 'Сайт ChartDB',
|
||||||
join_discord: 'Приєднуйтесь до нас в Діскорд',
|
join_discord: 'Приєднуйтесь до нас в Діскорд',
|
||||||
schedule_a_call: 'Поговоріть з нами!',
|
schedule_a_call: 'Забронювати зустріч!',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -54,18 +55,18 @@ export const uk: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
clear_diagram_alert: {
|
clear_diagram_alert: {
|
||||||
title: 'Чітка діаграма',
|
title: 'Очистити діаграму',
|
||||||
description:
|
description:
|
||||||
'Цю дію не можна скасувати. Це назавжди видалить усі дані на діаграмі.',
|
'Цю дію не можна скасувати. Це назавжди видалить усі дані на діаграмі.',
|
||||||
cancel: 'Скасувати',
|
cancel: 'Скасувати',
|
||||||
clear: 'очистити',
|
clear: 'Очистити',
|
||||||
},
|
},
|
||||||
|
|
||||||
reorder_diagram_alert: {
|
reorder_diagram_alert: {
|
||||||
title: 'Діаграма зміни порядку',
|
title: 'Перевпорядкувати діаграму',
|
||||||
description:
|
description:
|
||||||
'Ця дія перевпорядкує всі таблиці на діаграмі. Хочете продовжити?',
|
'Ця дія перевпорядкує всі таблиці на діаграмі. Хочете продовжити?',
|
||||||
reorder: 'Змінити порядок',
|
reorder: 'Перевпорядкувати',
|
||||||
cancel: 'Скасувати',
|
cancel: 'Скасувати',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -90,24 +91,23 @@ export const uk: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
theme: {
|
theme: {
|
||||||
system: 'система',
|
system: 'Системна',
|
||||||
light: 'світлий',
|
light: 'Світла',
|
||||||
dark: 'Темний',
|
dark: 'Темна',
|
||||||
},
|
},
|
||||||
|
|
||||||
zoom: {
|
zoom: {
|
||||||
on: 'увімкнути',
|
on: 'Увімкнути',
|
||||||
off: 'вимкнути',
|
off: 'Вимкнути',
|
||||||
},
|
},
|
||||||
|
|
||||||
last_saved: 'Востаннє збережено',
|
last_saved: 'Востаннє збережено',
|
||||||
saved: 'Збережено',
|
saved: 'Збережено',
|
||||||
diagrams: 'Діаграми',
|
loading_diagram: 'Завантаження діаграми…',
|
||||||
loading_diagram: 'Діаграма завантаження...',
|
deselect_all: 'Зняти виділення з усіх',
|
||||||
deselect_all: 'Зняти вибір із усіх',
|
|
||||||
select_all: 'Вибрати усі',
|
select_all: 'Вибрати усі',
|
||||||
clear: 'Очистити',
|
clear: 'Очистити',
|
||||||
show_more: 'показати більше',
|
show_more: 'Показати більше',
|
||||||
show_less: 'Показати менше',
|
show_less: 'Показати менше',
|
||||||
copy_to_clipboard: 'Копіювати в буфер обміну',
|
copy_to_clipboard: 'Копіювати в буфер обміну',
|
||||||
copied: 'Скопійовано!',
|
copied: 'Скопійовано!',
|
||||||
@@ -115,47 +115,53 @@ export const uk: LanguageTranslation = {
|
|||||||
side_panel: {
|
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_section: {
|
||||||
tables: 'Таблиці',
|
tables: 'Таблиці',
|
||||||
add_table: 'Додати таблицю',
|
add_table: 'Додати таблицю',
|
||||||
filter: 'фільтр',
|
filter: 'Фільтр',
|
||||||
collapse: 'Згорнути все',
|
collapse: 'Згорнути все',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'поля',
|
fields: 'Поля',
|
||||||
nullable: 'Зведений нанівець?',
|
nullable: 'Може бути Null?',
|
||||||
primary_key: 'Первинний ключ',
|
primary_key: 'Первинний ключ',
|
||||||
indexes: 'Індекси',
|
indexes: 'Індекси',
|
||||||
comments: 'Коментарі',
|
comments: 'Коментарі',
|
||||||
no_comments: 'Без коментарів',
|
no_comments: 'Немає коментарів',
|
||||||
add_field: 'Додати поле',
|
add_field: 'Додати поле',
|
||||||
add_index: 'Додати індекс',
|
add_index: 'Додати індекс',
|
||||||
index_select_fields: 'Виберіть поля',
|
index_select_fields: 'Виберіть поля',
|
||||||
no_types_found: 'Типи не знайдено',
|
no_types_found: 'Типи не знайдено',
|
||||||
field_name: "Ім'я",
|
field_name: 'Назва поля',
|
||||||
field_type: 'Тип',
|
field_type: 'Тип',
|
||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'Атрибути полів',
|
title: 'Атрибути полів',
|
||||||
unique: 'Унікальний',
|
unique: 'Унікальне',
|
||||||
comments: 'Коментарі',
|
comments: 'Коментарі',
|
||||||
no_comments: 'Без коментарів',
|
no_comments: 'Немає коментарів',
|
||||||
delete_field: 'Видалити поле',
|
delete_field: 'Видалити поле',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Атрибути індексу',
|
title: 'Атрибути індексу',
|
||||||
name: "Ім'я",
|
name: 'Назва індекса',
|
||||||
unique: 'Унікальний',
|
unique: 'Унікальний',
|
||||||
delete_index: 'Видалити індекс',
|
delete_index: 'Видалити індекс',
|
||||||
},
|
},
|
||||||
table_actions: {
|
table_actions: {
|
||||||
title: 'Дії таблиці',
|
title: 'Дії з таблицею',
|
||||||
change_schema: 'Змінити схему',
|
change_schema: 'Змінити схему',
|
||||||
add_field: 'Додати поле',
|
add_field: 'Додати поле',
|
||||||
add_index: 'Додати індекс',
|
add_index: 'Додати індекс',
|
||||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
duplicate_table: 'Дублювати таблицю',
|
||||||
delete_table: 'Видалити таблицю',
|
delete_table: 'Видалити таблицю',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -165,14 +171,14 @@ export const uk: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationships_section: {
|
relationships_section: {
|
||||||
relationships: 'стосунки',
|
relationships: 'Звʼязки',
|
||||||
filter: 'фільтр',
|
filter: 'Фільтр',
|
||||||
add_relationship: "Додати зв'язок",
|
add_relationship: 'Додати звʼязок',
|
||||||
collapse: 'Згорнути все',
|
collapse: 'Згорнути все',
|
||||||
relationship: {
|
relationship: {
|
||||||
primary: 'Первинна таблиця',
|
primary: 'Первинна таблиця',
|
||||||
foreign: 'Посилання на таблицю',
|
foreign: 'Посилання на таблицю',
|
||||||
cardinality: 'Кардинальність',
|
cardinality: 'Звʼязок',
|
||||||
delete_relationship: 'Видалити',
|
delete_relationship: 'Видалити',
|
||||||
relationship_actions: {
|
relationship_actions: {
|
||||||
title: 'Дії',
|
title: 'Дії',
|
||||||
@@ -180,17 +186,17 @@ export const uk: LanguageTranslation = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
empty_state: {
|
empty_state: {
|
||||||
title: 'Жодних стосунків',
|
title: 'Звʼязків немає',
|
||||||
description: 'Створіть зв’язок для з’єднання таблиць',
|
description: 'Створіть звʼязок для зʼєднання таблиць',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dependencies_section: {
|
dependencies_section: {
|
||||||
dependencies: 'Залежності',
|
dependencies: 'Залежності',
|
||||||
filter: 'фільтр',
|
filter: 'Фільтр',
|
||||||
collapse: 'Згорнути все',
|
collapse: 'Згорнути все',
|
||||||
dependency: {
|
dependency: {
|
||||||
table: 'Таблиця',
|
table: 'Таблиця',
|
||||||
dependent_table: 'Залежний вид',
|
dependent_table: 'Залежне подання',
|
||||||
delete_dependency: 'Видалити',
|
delete_dependency: 'Видалити',
|
||||||
dependency_actions: {
|
dependency_actions: {
|
||||||
title: 'Дії',
|
title: 'Дії',
|
||||||
@@ -207,34 +213,34 @@ export const uk: LanguageTranslation = {
|
|||||||
toolbar: {
|
toolbar: {
|
||||||
zoom_in: 'Збільшити',
|
zoom_in: 'Збільшити',
|
||||||
zoom_out: 'Зменшити',
|
zoom_out: 'Зменшити',
|
||||||
save: 'зберегти',
|
save: 'Зберегти',
|
||||||
show_all: 'Показати все',
|
show_all: 'Показати все',
|
||||||
undo: 'Скасувати',
|
undo: 'Скасувати',
|
||||||
redo: 'Повторити',
|
redo: 'Повторити',
|
||||||
reorder_diagram: 'Діаграма зміни порядку',
|
reorder_diagram: 'Перевпорядкувати діаграму',
|
||||||
highlight_overlapping_tables: 'Виділіть таблиці, що перекриваються',
|
highlight_overlapping_tables: 'Показати таблиці, що перекриваються',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_diagram_dialog: {
|
new_diagram_dialog: {
|
||||||
database_selection: {
|
database_selection: {
|
||||||
title: 'Що таке ваша база даних?',
|
title: 'Яка у вас база даних?',
|
||||||
description:
|
description:
|
||||||
'Кожна база даних має свої унікальні особливості та можливості.',
|
'Кожна база даних має свої унікальні особливості та можливості.',
|
||||||
check_examples_long: 'Перевірте приклади',
|
check_examples_long: 'Подивіться приклади',
|
||||||
check_examples_short: 'Приклади',
|
check_examples_short: 'Приклади',
|
||||||
},
|
},
|
||||||
|
|
||||||
import_database: {
|
import_database: {
|
||||||
title: 'Імпортуйте вашу базу даних',
|
title: 'Імпортуйте вашу базу даних',
|
||||||
database_edition: 'Редакція бази даних:',
|
database_edition: 'Варіант бази даних:',
|
||||||
step_1: 'Запустіть цей сценарій у своїй базі даних:',
|
step_1: 'Запустіть цей сценарій у своїй базі даних:',
|
||||||
step_2: 'Вставте сюди результат сценарію:',
|
step_2: 'Вставте сюди результат сценарію:',
|
||||||
script_results_placeholder: 'Результати сценарію тут...',
|
script_results_placeholder: 'Результати сценарію має бути тут…',
|
||||||
ssms_instructions: {
|
ssms_instructions: {
|
||||||
button_text: 'SSMS Інструкції',
|
button_text: 'SSMS Інструкції',
|
||||||
title: 'Інструкції',
|
title: 'Інструкції',
|
||||||
step_1: 'Перейдіть до Інструменти > Опції > Результати запиту > SQL Сервер.',
|
step_1: 'Перейдіть до Інструменти > Опції > Результати запиту > SQL Сервер.',
|
||||||
step_2: 'Якщо ви використовуєте «Результати в сітку», змініть максимальну кількість символів, отриманих для даних, що не є XML (встановіть на 9999999).',
|
step_2: 'Якщо ви використовуєте «Results to Grid», змініть максимальну кількість символів, отриманих для даних, що не є XML (встановіть на 9999999).',
|
||||||
},
|
},
|
||||||
instructions_link: 'Потрібна допомога? Подивіться як',
|
instructions_link: 'Потрібна допомога? Подивіться як',
|
||||||
check_script_result: 'Перевірте результат сценарію',
|
check_script_result: 'Перевірте результат сценарію',
|
||||||
@@ -242,20 +248,19 @@ export const uk: LanguageTranslation = {
|
|||||||
|
|
||||||
cancel: 'Скасувати',
|
cancel: 'Скасувати',
|
||||||
back: 'Назад',
|
back: 'Назад',
|
||||||
// TODO: Translate
|
import_from_file: 'Імпортувати з файлу',
|
||||||
import_from_file: 'Import from File',
|
|
||||||
empty_diagram: 'Порожня діаграма',
|
empty_diagram: 'Порожня діаграма',
|
||||||
continue: 'Продовжити',
|
continue: 'Продовжити',
|
||||||
import: 'Імпорт',
|
import: 'Імпорт',
|
||||||
},
|
},
|
||||||
|
|
||||||
open_diagram_dialog: {
|
open_diagram_dialog: {
|
||||||
title: 'Відкрита діаграма',
|
title: 'Відкрити діаграму',
|
||||||
description:
|
description:
|
||||||
'Виберіть діаграму, яку потрібно відкрити, зі списку нижче.',
|
'Виберіть діаграму, яку потрібно відкрити, зі списку нижче.',
|
||||||
table_columns: {
|
table_columns: {
|
||||||
name: "Ім'я",
|
name: 'Назва',
|
||||||
created_at: 'Створено в',
|
created_at: 'Створено0',
|
||||||
last_modified: 'Востаннє змінено',
|
last_modified: 'Востаннє змінено',
|
||||||
tables_count: 'Таблиці',
|
tables_count: 'Таблиці',
|
||||||
},
|
},
|
||||||
@@ -269,23 +274,23 @@ export const uk: LanguageTranslation = {
|
|||||||
'Експортуйте свою схему діаграми в {{databaseType}} сценарій',
|
'Експортуйте свою схему діаграми в {{databaseType}} сценарій',
|
||||||
close: 'Закрити',
|
close: 'Закрити',
|
||||||
loading: {
|
loading: {
|
||||||
text: 'ШІ створює SQL для {{databaseType}}...',
|
text: 'ШІ створює SQL для {{databaseType}}…',
|
||||||
description: 'Це має зайняти до 30 секунд.',
|
description: 'Це має зайняти до 30 секунд.',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
message:
|
message:
|
||||||
"Помилка створення сценарію SQL. Спробуйте пізніше або <0>зв'яжіться з нами</0>.",
|
'Помилка створення сценарію SQL. Спробуйте пізніше або <0>звʼяжіться з нами</0>.',
|
||||||
description:
|
description:
|
||||||
'Не соромтеся використовувати свій OPENAI_TOKEN, дивіться посібник <0>тут</0>.',
|
'Не соромтеся використовувати свій OPENAI_TOKEN, дивіться посібник <0>тут</0>.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
create_relationship_dialog: {
|
create_relationship_dialog: {
|
||||||
title: 'Створити відносини',
|
title: 'Створити звʼязок',
|
||||||
primary_table: 'Первинна таблиця',
|
primary_table: 'Первинна таблиця',
|
||||||
primary_field: 'Первинне поле',
|
primary_field: 'Первинне поле',
|
||||||
referenced_table: 'Посилання на таблицю',
|
referenced_table: 'Звʼязана таблиця',
|
||||||
referenced_field: 'Поле посилання',
|
referenced_field: 'Повʼязане поле',
|
||||||
primary_table_placeholder: 'Виберіть таблицю',
|
primary_table_placeholder: 'Виберіть таблицю',
|
||||||
primary_field_placeholder: 'Виберіть поле',
|
primary_field_placeholder: 'Виберіть поле',
|
||||||
referenced_table_placeholder: 'Виберіть таблицю',
|
referenced_table_placeholder: 'Виберіть таблицю',
|
||||||
@@ -305,12 +310,12 @@ export const uk: LanguageTranslation = {
|
|||||||
new_tables:
|
new_tables:
|
||||||
'<bold>{{newTablesNumber}}</bold> будуть додані нові таблиці.',
|
'<bold>{{newTablesNumber}}</bold> будуть додані нові таблиці.',
|
||||||
new_relationships:
|
new_relationships:
|
||||||
'<bold>{{newRelationshipsNumber}}</bold> будуть створені нові відносини.',
|
'<bold>{{newRelationshipsNumber}}</bold> будуть створені нові звʼязки.',
|
||||||
tables_override:
|
tables_override:
|
||||||
'<bold>{{tablesOverrideNumber}}</bold> таблиці будуть перезаписані.',
|
'<bold>{{tablesOverrideNumber}}</bold> таблиці будуть перезаписані.',
|
||||||
proceed: 'Ви хочете продовжити?',
|
proceed: 'Ви хочете продовжити?',
|
||||||
},
|
},
|
||||||
import: 'Імпорт',
|
import: 'Імпортувати',
|
||||||
cancel: 'Скасувати',
|
cancel: 'Скасувати',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -318,83 +323,91 @@ export const uk: LanguageTranslation = {
|
|||||||
export_image_dialog: {
|
export_image_dialog: {
|
||||||
title: 'Експорт зображення',
|
title: 'Експорт зображення',
|
||||||
description: 'Виберіть коефіцієнт масштабування для експорту:',
|
description: 'Виберіть коефіцієнт масштабування для експорту:',
|
||||||
scale_1x: '1x Регулярний',
|
scale_1x: '1x Звичайний',
|
||||||
scale_2x: '2x (Рекомендовано)',
|
scale_2x: '2x (Рекомендовано)',
|
||||||
scale_3x: '3x',
|
scale_3x: '3x',
|
||||||
scale_4x: '4x',
|
scale_4x: '4x',
|
||||||
cancel: 'Скасувати',
|
cancel: 'Скасувати',
|
||||||
export: 'Експорт',
|
export: 'Експортувати',
|
||||||
},
|
},
|
||||||
|
|
||||||
new_table_schema_dialog: {
|
new_table_schema_dialog: {
|
||||||
title: 'Виберіть Схему',
|
title: 'Виберіть Схему',
|
||||||
description:
|
description:
|
||||||
'Наразі відображається кілька схем. Виберіть один для нової таблиці.',
|
'Наразі показується кілька схем. Виберіть одну для нової таблиці.',
|
||||||
cancel: 'Скасувати',
|
cancel: 'Скасувати',
|
||||||
confirm: 'Підтвердити',
|
confirm: 'Підтвердити',
|
||||||
},
|
},
|
||||||
|
|
||||||
update_table_schema_dialog: {
|
update_table_schema_dialog: {
|
||||||
title: 'Змінити схему',
|
title: 'Змінити схему',
|
||||||
description: 'Оновити таблицю "{{tableName}}" схему',
|
description: 'Оновити схему таблиці "{{tableName}}"',
|
||||||
cancel: 'Скасувати',
|
cancel: 'Скасувати',
|
||||||
confirm: 'Змінити',
|
confirm: 'Змінити',
|
||||||
},
|
},
|
||||||
|
|
||||||
star_us_dialog: {
|
star_us_dialog: {
|
||||||
title: 'Допоможіть нам покращитися!',
|
title: 'Допоможіть нам покращитися!',
|
||||||
description: 'Хочете позначити нас на Ґітхаб? Це лише один клік!',
|
description: 'Поставне на зірку на GitHub? Це лише один клік!',
|
||||||
close: 'Не зараз',
|
close: 'Не зараз',
|
||||||
confirm: 'звичайно!',
|
confirm: 'Звісно!',
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
|
||||||
export_diagram_dialog: {
|
export_diagram_dialog: {
|
||||||
title: 'Export Diagram',
|
title: 'Експорт Діаграми',
|
||||||
description: 'Choose the format for export:',
|
description: 'Оберіть формат експорту:',
|
||||||
format_json: 'JSON',
|
format_json: 'JSON',
|
||||||
cancel: 'Cancel',
|
cancel: 'Скасувати',
|
||||||
export: 'Export',
|
export: 'Експортувати',
|
||||||
error: {
|
error: {
|
||||||
title: 'Error exporting diagram',
|
title: 'Помилка експорут діаграми',
|
||||||
description:
|
description:
|
||||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
'Щось пішло не так. Потрібна допомога? chartdb.io@gmail.com',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
import_diagram_dialog: {
|
||||||
|
title: 'Імпорт Діаграми',
|
||||||
|
description: 'Вставте JSON діаграми нижче:',
|
||||||
|
cancel: 'Скасувати',
|
||||||
|
import: 'Імпортувати',
|
||||||
|
error: {
|
||||||
|
title: 'Помилка імпорту діаграми',
|
||||||
|
description:
|
||||||
|
'JSON діаграми є неправильним. Будь ласка, перевірте JSON і спробуйте ще раз. Потрібна допомога? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// TODO: Translate
|
// TODO: Translate
|
||||||
import_diagram_dialog: {
|
import_dbml_dialog: {
|
||||||
title: 'Import Diagram',
|
title: 'Import DBML',
|
||||||
description: 'Paste the diagram JSON below:',
|
description: 'Import a database schema from DBML format.',
|
||||||
cancel: 'Cancel',
|
|
||||||
import: 'Import',
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
error: {
|
error: {
|
||||||
title: 'Error importing diagram',
|
title: 'Error',
|
||||||
description:
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Один до одного',
|
one_to_one: 'Один до Одного',
|
||||||
one_to_many: 'Один до багатьох',
|
one_to_many: 'Один до Багатьох',
|
||||||
many_to_one: 'Багато до одного',
|
many_to_one: 'Багато до Одного',
|
||||||
many_to_many: 'Багато до багатьох',
|
many_to_many: 'Багато до Багатьох',
|
||||||
},
|
},
|
||||||
|
|
||||||
canvas_context_menu: {
|
canvas_context_menu: {
|
||||||
new_table: 'Нова таблиця',
|
new_table: 'Нова таблиця',
|
||||||
new_relationship: 'Нові стосунки',
|
new_relationship: 'Новий звʼязок',
|
||||||
},
|
},
|
||||||
|
|
||||||
table_node_context_menu: {
|
table_node_context_menu: {
|
||||||
edit_table: 'Редагувати таблицю',
|
edit_table: 'Редагувати таблицю',
|
||||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
duplicate_table: 'Дублювати таблицю',
|
||||||
delete_table: 'Видалити таблицю',
|
delete_table: 'Видалити таблицю',
|
||||||
},
|
},
|
||||||
|
|
||||||
// TODO: Add translations
|
snap_to_grid_tooltip: 'Вирівнювати за сіткою (Отримуйте {{key}})',
|
||||||
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
|
|
||||||
|
|
||||||
tool_tips: {
|
tool_tips: {
|
||||||
double_click_to_edit: 'Двойной клик для редактирования',
|
double_click_to_edit: 'Подвійне клацання для редагування',
|
||||||
},
|
},
|
||||||
|
|
||||||
language_select: {
|
language_select: {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const vi: LanguageTranslation = {
|
|||||||
new: 'Tạo mới',
|
new: 'Tạo mới',
|
||||||
open: 'Mở',
|
open: 'Mở',
|
||||||
save: 'Lưu',
|
save: 'Lưu',
|
||||||
import_database: 'Nhập cơ sở dữ liệu',
|
import: 'Nhập cơ sở dữ liệu',
|
||||||
export_sql: 'Xuất SQL',
|
export_sql: 'Xuất SQL',
|
||||||
export_as: 'Xuất thành',
|
export_as: 'Xuất thành',
|
||||||
delete_diagram: 'Xóa sơ đồ',
|
delete_diagram: 'Xóa sơ đồ',
|
||||||
@@ -30,6 +30,9 @@ export const vi: LanguageTranslation = {
|
|||||||
theme: 'Chủ đề',
|
theme: 'Chủ đề',
|
||||||
show_dependencies: 'Hiển thị các phụ thuộc',
|
show_dependencies: 'Hiển thị các phụ thuộc',
|
||||||
hide_dependencies: 'Ẩn các phụ thuộc',
|
hide_dependencies: 'Ẩn các phụ thuộc',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
share: 'Chia sẻ',
|
share: 'Chia sẻ',
|
||||||
@@ -101,7 +104,6 @@ export const vi: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: 'Đã lưu lần cuối',
|
last_saved: 'Đã lưu lần cuối',
|
||||||
saved: 'Đã lưu',
|
saved: 'Đã lưu',
|
||||||
diagrams: 'Sơ đồ',
|
|
||||||
loading_diagram: 'Đang tải sơ đồ...',
|
loading_diagram: 'Đang tải sơ đồ...',
|
||||||
deselect_all: 'Bỏ chọn tất cả',
|
deselect_all: 'Bỏ chọn tất cả',
|
||||||
select_all: 'Chọn tất cả',
|
select_all: 'Chọn tất cả',
|
||||||
@@ -122,6 +124,12 @@ export const vi: LanguageTranslation = {
|
|||||||
add_table: 'Thêm bảng',
|
add_table: 'Thêm bảng',
|
||||||
filter: 'Lọc',
|
filter: 'Lọc',
|
||||||
collapse: 'Thu gọn tất cả',
|
collapse: 'Thu gọn tất cả',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: 'Trường',
|
fields: 'Trường',
|
||||||
@@ -368,6 +376,17 @@ export const vi: LanguageTranslation = {
|
|||||||
'Sơ đồ ở dạng JSON không hợp lệ. Vui lòng kiểm tra JSON và thử lại. Bạn cần trợ giúp? chartdb.io@gmail.com',
|
'Sơ đồ ở dạng JSON không hợp lệ. Vui lòng kiểm tra JSON và thử lại. Bạn cần trợ giúp? chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: 'Quan hệ một-một',
|
one_to_one: 'Quan hệ một-một',
|
||||||
one_to_many: 'Quan hệ một-nhiều',
|
one_to_many: 'Quan hệ một-nhiều',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
new: '新建',
|
new: '新建',
|
||||||
open: '打开',
|
open: '打开',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
import_database: '导入数据库',
|
import: '导入数据库',
|
||||||
export_sql: '导出 SQL 语句',
|
export_sql: '导出 SQL 语句',
|
||||||
export_as: '导出为',
|
export_as: '导出为',
|
||||||
delete_diagram: '删除关系图',
|
delete_diagram: '删除关系图',
|
||||||
@@ -30,6 +30,9 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
theme: '主题',
|
theme: '主题',
|
||||||
show_dependencies: '展示依赖',
|
show_dependencies: '展示依赖',
|
||||||
hide_dependencies: '隐藏依赖',
|
hide_dependencies: '隐藏依赖',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
share: '分享',
|
share: '分享',
|
||||||
@@ -98,7 +101,6 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: '上次保存时间:',
|
last_saved: '上次保存时间:',
|
||||||
saved: '已保存',
|
saved: '已保存',
|
||||||
diagrams: '关系图',
|
|
||||||
loading_diagram: '加载关系图...',
|
loading_diagram: '加载关系图...',
|
||||||
deselect_all: '取消全选',
|
deselect_all: '取消全选',
|
||||||
select_all: '全选',
|
select_all: '全选',
|
||||||
@@ -119,6 +121,12 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
add_table: '添加表',
|
add_table: '添加表',
|
||||||
filter: '筛选',
|
filter: '筛选',
|
||||||
collapse: '全部折叠',
|
collapse: '全部折叠',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: '字段',
|
fields: '字段',
|
||||||
@@ -364,6 +372,17 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
'关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 chartdb.io@gmail.com',
|
'关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: '一对一',
|
one_to_one: '一对一',
|
||||||
one_to_many: '一对多',
|
one_to_many: '一对多',
|
||||||
@@ -395,7 +414,7 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const zh_CNMetadata: LanguageMetadata = {
|
export const zh_CNMetadata: LanguageMetadata = {
|
||||||
name: 'Chinese',
|
name: 'Chinese (Simplified)',
|
||||||
nativeName: '简体中文',
|
nativeName: '简体中文',
|
||||||
code: 'zh_CN',
|
code: 'zh_CN',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
new: '新增',
|
new: '新增',
|
||||||
open: '開啟',
|
open: '開啟',
|
||||||
save: '儲存',
|
save: '儲存',
|
||||||
import_database: '匯入資料庫',
|
import: '匯入資料庫',
|
||||||
export_sql: '匯出 SQL',
|
export_sql: '匯出 SQL',
|
||||||
export_as: '匯出為特定格式',
|
export_as: '匯出為特定格式',
|
||||||
delete_diagram: '刪除圖表',
|
delete_diagram: '刪除圖表',
|
||||||
@@ -30,6 +30,9 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
theme: '主題',
|
theme: '主題',
|
||||||
show_dependencies: '顯示相依性',
|
show_dependencies: '顯示相依性',
|
||||||
hide_dependencies: '隱藏相依性',
|
hide_dependencies: '隱藏相依性',
|
||||||
|
// TODO: Translate
|
||||||
|
show_minimap: 'Show Mini Map',
|
||||||
|
hide_minimap: 'Hide Mini Map',
|
||||||
},
|
},
|
||||||
share: {
|
share: {
|
||||||
share: '分享',
|
share: '分享',
|
||||||
@@ -98,7 +101,6 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
|
|
||||||
last_saved: '上次儲存於',
|
last_saved: '上次儲存於',
|
||||||
saved: '已儲存',
|
saved: '已儲存',
|
||||||
diagrams: '圖表',
|
|
||||||
loading_diagram: '正在載入圖表...',
|
loading_diagram: '正在載入圖表...',
|
||||||
deselect_all: '取消所有選取',
|
deselect_all: '取消所有選取',
|
||||||
select_all: '全選',
|
select_all: '全選',
|
||||||
@@ -119,6 +121,12 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
add_table: '新增表格',
|
add_table: '新增表格',
|
||||||
filter: '篩選',
|
filter: '篩選',
|
||||||
collapse: '全部摺疊',
|
collapse: '全部摺疊',
|
||||||
|
// TODO: Translate
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No tables found matching your filter.',
|
||||||
|
// TODO: Translate
|
||||||
|
show_list: 'Show Table List',
|
||||||
|
show_dbml: 'Show DBML Editor',
|
||||||
|
|
||||||
table: {
|
table: {
|
||||||
fields: '欄位',
|
fields: '欄位',
|
||||||
@@ -363,6 +371,17 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
'圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 chartdb.io@gmail.com',
|
'圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 chartdb.io@gmail.com',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
import_dbml_dialog: {
|
||||||
|
title: 'Import DBML',
|
||||||
|
description: 'Import a database schema from DBML format.',
|
||||||
|
import: 'Import',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
error: {
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to import DBML. Please check the syntax.',
|
||||||
|
},
|
||||||
|
},
|
||||||
relationship_type: {
|
relationship_type: {
|
||||||
one_to_one: '一對一',
|
one_to_one: '一對一',
|
||||||
one_to_many: '一對多',
|
one_to_many: '一對多',
|
||||||
@@ -395,6 +414,6 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
|
|
||||||
export const zh_TWMetadata: LanguageMetadata = {
|
export const zh_TWMetadata: LanguageMetadata = {
|
||||||
nativeName: '繁體中文',
|
nativeName: '繁體中文',
|
||||||
name: 'Traditional Chinese',
|
name: 'Chinese (Traditional)',
|
||||||
code: 'zh_TW',
|
code: 'zh_TW',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -62,3 +62,5 @@ export function areFieldTypesCompatible(
|
|||||||
dbCompatibleTypes[type2.id]?.includes(type1.id)
|
dbCompatibleTypes[type2.id]?.includes(type1.id)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const dataTypes = Object.values(dataTypeMap).flat();
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ FROM fk_info${databaseEdition ? '_' + databaseEdition : ''}, pk_info, cols, inde
|
|||||||
if (options.databaseClient === DatabaseClient.POSTGRESQL_PSQL) {
|
if (options.databaseClient === DatabaseClient.POSTGRESQL_PSQL) {
|
||||||
return `${psqlPreCommand}psql -h HOST_NAME -p PORT -U USER_NAME -d DATABASE_NAME -c "
|
return `${psqlPreCommand}psql -h HOST_NAME -p PORT -U USER_NAME -d DATABASE_NAME -c "
|
||||||
${query.replace(/"/g, '\\"').replace(/\\\\/g, '\\\\\\').replace(/\\x/g, '\\\\x')}
|
${query.replace(/"/g, '\\"').replace(/\\\\/g, '\\\\\\').replace(/\\x/g, '\\\\x')}
|
||||||
" -t -A | pbcopy; LG='\\033[0;32m'; NC='\\033[0m'; echo "You got the resultset ($(pbpaste | wc -c | xargs) characters) in Copy/Paste. \${LG}Go back & paste in ChartDB :)\${NC}";`;
|
" -t -A > output.json;`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
|
|||||||
221
src/lib/dbml-import.ts
Normal file
221
src/lib/dbml-import.ts
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import { Parser } from '@dbml/core';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
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 { randomColor } from '@/lib/colors';
|
||||||
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
|
||||||
|
interface DBMLTypeArgs {
|
||||||
|
length?: number;
|
||||||
|
precision?: number;
|
||||||
|
scale?: number;
|
||||||
|
values?: string[]; // For enum types
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DBMLField {
|
||||||
|
name: string;
|
||||||
|
type: {
|
||||||
|
type_name: string;
|
||||||
|
args?: DBMLTypeArgs;
|
||||||
|
};
|
||||||
|
unique?: boolean;
|
||||||
|
pk?: boolean;
|
||||||
|
not_null?: boolean;
|
||||||
|
increment?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DBMLTable {
|
||||||
|
name: string;
|
||||||
|
schema?: string | { name: string };
|
||||||
|
fields: DBMLField[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DBMLEndpoint {
|
||||||
|
tableName: string;
|
||||||
|
fieldNames: string[];
|
||||||
|
relation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DBMLRef {
|
||||||
|
endpoints: [DBMLEndpoint, DBMLEndpoint];
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDBMLTypeToGenericType = (dbmlType: string): DataType => {
|
||||||
|
const normalizedType = dbmlType.toLowerCase().replace(/\(.*\)/, '');
|
||||||
|
const matchedType = genericDataTypes.find((t) => t.id === normalizedType);
|
||||||
|
if (matchedType) return matchedType;
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
int: 'integer',
|
||||||
|
varchar: 'varchar',
|
||||||
|
bool: '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;
|
||||||
|
}
|
||||||
|
return genericDataTypes.find((t) => t.id === 'varchar')!;
|
||||||
|
};
|
||||||
|
|
||||||
|
const determineCardinality = (
|
||||||
|
field: DBField,
|
||||||
|
referencedField: DBField
|
||||||
|
): { sourceCardinality: string; targetCardinality: string } => {
|
||||||
|
const isSourceUnique = field.unique || field.primaryKey;
|
||||||
|
const isTargetUnique = referencedField.unique || referencedField.primaryKey;
|
||||||
|
if (isSourceUnique && isTargetUnique) {
|
||||||
|
return { sourceCardinality: 'one', targetCardinality: 'one' };
|
||||||
|
} else if (isSourceUnique) {
|
||||||
|
return { sourceCardinality: 'one', targetCardinality: 'many' };
|
||||||
|
} else if (isTargetUnique) {
|
||||||
|
return { sourceCardinality: 'many', targetCardinality: 'one' };
|
||||||
|
} else {
|
||||||
|
return { sourceCardinality: 'many', targetCardinality: 'many' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const importDBMLToDiagram = async (
|
||||||
|
dbmlContent: string
|
||||||
|
): Promise<Diagram> => {
|
||||||
|
try {
|
||||||
|
const parser = new Parser();
|
||||||
|
const parsedData = parser.parse(dbmlContent, 'dbml');
|
||||||
|
const dbmlData = parsedData.schemas[0];
|
||||||
|
|
||||||
|
// Extract only the necessary data from the parsed DBML
|
||||||
|
const extractedData = {
|
||||||
|
tables: dbmlData.tables.map((table: DBMLTable) => ({
|
||||||
|
name: table.name,
|
||||||
|
schema: table.schema,
|
||||||
|
fields: table.fields.map((field: DBMLField) => ({
|
||||||
|
name: field.name,
|
||||||
|
type: field.type,
|
||||||
|
unique: field.unique,
|
||||||
|
pk: field.pk,
|
||||||
|
not_null: field.not_null,
|
||||||
|
increment: field.increment,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
refs: (dbmlData.refs as unknown as DBMLRef[]).map((ref) => ({
|
||||||
|
endpoints: (ref.endpoints as [DBMLEndpoint, DBMLEndpoint]).map(
|
||||||
|
(endpoint) => ({
|
||||||
|
tableName: endpoint.tableName,
|
||||||
|
fieldNames: endpoint.fieldNames,
|
||||||
|
relation: endpoint.relation,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Convert DBML tables to ChartDB table objects
|
||||||
|
const tables: DBTable[] = extractedData.tables.map((table, index) => {
|
||||||
|
const row = Math.floor(index / 4);
|
||||||
|
const col = index % 4;
|
||||||
|
const tableSpacing = 300; // Increased spacing between tables
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: generateId(),
|
||||||
|
name: table.name.replace(/['"]/g, ''),
|
||||||
|
schema:
|
||||||
|
typeof table.schema === 'string'
|
||||||
|
? table.schema
|
||||||
|
: table.schema?.name || '',
|
||||||
|
order: index,
|
||||||
|
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(),
|
||||||
|
})),
|
||||||
|
x: col * tableSpacing,
|
||||||
|
y: row * tableSpacing,
|
||||||
|
indexes: [],
|
||||||
|
color: randomColor(),
|
||||||
|
isView: false,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create relationships using the refs
|
||||||
|
const relationships: DBRelationship[] = extractedData.refs.map(
|
||||||
|
(ref) => {
|
||||||
|
const [source, target] = ref.endpoints;
|
||||||
|
const sourceTable = tables.find(
|
||||||
|
(t) =>
|
||||||
|
t.name === source.tableName.replace(/['"]/g, '') &&
|
||||||
|
(!source.tableName.includes('.') ||
|
||||||
|
t.schema === source.tableName.split('.')[0])
|
||||||
|
);
|
||||||
|
const targetTable = tables.find(
|
||||||
|
(t) =>
|
||||||
|
t.name === target.tableName.replace(/['"]/g, '') &&
|
||||||
|
(!target.tableName.includes('.') ||
|
||||||
|
t.schema === target.tableName.split('.')[0])
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sourceTable || !targetTable) {
|
||||||
|
throw new Error('Invalid relationship: tables not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceField = sourceTable.fields.find(
|
||||||
|
(f) => f.name === source.fieldNames[0].replace(/['"]/g, '')
|
||||||
|
);
|
||||||
|
const targetField = targetTable.fields.find(
|
||||||
|
(f) => f.name === target.fieldNames[0].replace(/['"]/g, '')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!sourceField || !targetField) {
|
||||||
|
throw new Error('Invalid relationship: fields not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { sourceCardinality, targetCardinality } =
|
||||||
|
determineCardinality(sourceField, targetField);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: generateId(),
|
||||||
|
name: `${sourceTable.name}_${sourceField.name}_${targetTable.name}_${targetField.name}`,
|
||||||
|
sourceSchema: sourceTable.schema,
|
||||||
|
targetSchema: targetTable.schema,
|
||||||
|
sourceTableId: sourceTable.id,
|
||||||
|
targetTableId: targetTable.id,
|
||||||
|
sourceFieldId: sourceField.id,
|
||||||
|
targetFieldId: targetField.id,
|
||||||
|
sourceCardinality: sourceCardinality as Cardinality,
|
||||||
|
targetCardinality: targetCardinality as Cardinality,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: generateDiagramId(),
|
||||||
|
name: 'DBML Import',
|
||||||
|
databaseType: DatabaseType.GENERIC,
|
||||||
|
tables,
|
||||||
|
relationships,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DBML parsing error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -45,17 +45,13 @@ import { Badge } from '@/components/badge/badge';
|
|||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { DBTable } from '@/lib/domain/db-table';
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
import {
|
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
|
||||||
adjustTablePositions,
|
|
||||||
shouldShowTablesBySchemaFilter,
|
|
||||||
} from '@/lib/domain/db-table';
|
|
||||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
} from '@/components/tooltip/tooltip';
|
} from '@/components/tooltip/tooltip';
|
||||||
import { useDialog } from '@/hooks/use-dialog';
|
|
||||||
import { MarkerDefinitions } from './marker-definitions';
|
import { MarkerDefinitions } from './marker-definitions';
|
||||||
import { CanvasContextMenu } from './canvas-context-menu';
|
import { CanvasContextMenu } from './canvas-context-menu';
|
||||||
import { areFieldTypesCompatible } from '@/lib/data/data-types/data-types';
|
import { areFieldTypesCompatible } from '@/lib/data/data-types/data-types';
|
||||||
@@ -65,7 +61,7 @@ import {
|
|||||||
findTableOverlapping,
|
findTableOverlapping,
|
||||||
} from './canvas-utils';
|
} from './canvas-utils';
|
||||||
import type { Graph } from '@/lib/graph';
|
import type { Graph } from '@/lib/graph';
|
||||||
import { createGraph, removeVertex } from '@/lib/graph';
|
import { removeVertex } from '@/lib/graph';
|
||||||
import type { ChartDBEvent } from '@/context/chartdb-context/chartdb-context';
|
import type { ChartDBEvent } from '@/context/chartdb-context/chartdb-context';
|
||||||
import { cn, debounce, getOperatingSystem } from '@/lib/utils';
|
import { cn, debounce, getOperatingSystem } from '@/lib/utils';
|
||||||
import type { DependencyEdgeType } from './dependency-edge';
|
import type { DependencyEdgeType } from './dependency-edge';
|
||||||
@@ -76,6 +72,8 @@ import {
|
|||||||
TOP_SOURCE_HANDLE_ID_PREFIX,
|
TOP_SOURCE_HANDLE_ID_PREFIX,
|
||||||
} from './table-node/table-node-dependency-indicator';
|
} from './table-node/table-node-dependency-indicator';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
import { useAlert } from '@/context/alert-context/alert-context';
|
||||||
|
import { useCanvas } from '@/hooks/use-canvas';
|
||||||
|
|
||||||
export type EdgeType = RelationshipEdgeType | DependencyEdgeType;
|
export type EdgeType = RelationshipEdgeType | DependencyEdgeType;
|
||||||
|
|
||||||
@@ -109,8 +107,7 @@ export interface CanvasProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
||||||
const { getEdge, getInternalNode, fitView, getEdges, getNode } =
|
const { getEdge, getInternalNode, getEdges, getNode } = useReactFlow();
|
||||||
useReactFlow();
|
|
||||||
const [selectedTableIds, setSelectedTableIds] = useState<string[]>([]);
|
const [selectedTableIds, setSelectedTableIds] = useState<string[]>([]);
|
||||||
const [selectedRelationshipIds, setSelectedRelationshipIds] = useState<
|
const [selectedRelationshipIds, setSelectedRelationshipIds] = useState<
|
||||||
string[]
|
string[]
|
||||||
@@ -133,16 +130,17 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
|||||||
} = useChartDB();
|
} = useChartDB();
|
||||||
const { showSidePanel } = useLayout();
|
const { showSidePanel } = useLayout();
|
||||||
const { effectiveTheme } = useTheme();
|
const { effectiveTheme } = useTheme();
|
||||||
const { scrollAction, showDependenciesOnCanvas } = useLocalConfig();
|
const { scrollAction, showDependenciesOnCanvas, showMiniMapOnCanvas } =
|
||||||
const { showAlert } = useDialog();
|
useLocalConfig();
|
||||||
|
const { showAlert } = useAlert();
|
||||||
const { isMd: isDesktop } = useBreakpoint('md');
|
const { isMd: isDesktop } = useBreakpoint('md');
|
||||||
const nodeTypes = useMemo(() => ({ table: TableNode }), []);
|
const nodeTypes = useMemo(() => ({ table: TableNode }), []);
|
||||||
const [highlightOverlappingTables, setHighlightOverlappingTables] =
|
const [highlightOverlappingTables, setHighlightOverlappingTables] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const { reorderTables, fitView, setOverlapGraph, overlapGraph } =
|
||||||
|
useCanvas();
|
||||||
|
|
||||||
const [isInitialLoadingNodes, setIsInitialLoadingNodes] = useState(true);
|
const [isInitialLoadingNodes, setIsInitialLoadingNodes] = useState(true);
|
||||||
const [overlapGraph, setOverlapGraph] =
|
|
||||||
useState<Graph<string>>(createGraph());
|
|
||||||
|
|
||||||
const [nodes, setNodes, onNodesChange] = useNodesState<TableNodeType>(
|
const [nodes, setNodes, onNodesChange] = useNodesState<TableNodeType>(
|
||||||
initialTables.map((table) => tableToTableNode(table, filteredSchemas))
|
initialTables.map((table) => tableToTableNode(table, filteredSchemas))
|
||||||
@@ -344,7 +342,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
|||||||
}, 500)();
|
}, 500)();
|
||||||
prevFilteredSchemas.current = filteredSchemas;
|
prevFilteredSchemas.current = filteredSchemas;
|
||||||
}
|
}
|
||||||
}, [filteredSchemas, fitView, tables]);
|
}, [filteredSchemas, fitView, tables, setOverlapGraph]);
|
||||||
|
|
||||||
const onConnectHandler = useCallback(
|
const onConnectHandler = useCallback(
|
||||||
async (params: AddEdgeParams) => {
|
async (params: AddEdgeParams) => {
|
||||||
@@ -656,33 +654,6 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
|||||||
const isLoadingDOM =
|
const isLoadingDOM =
|
||||||
tables.length > 0 ? !getInternalNode(tables[0].id) : false;
|
tables.length > 0 ? !getInternalNode(tables[0].id) : false;
|
||||||
|
|
||||||
const reorderTables = useCallback(() => {
|
|
||||||
const newTables = adjustTablePositions({
|
|
||||||
relationships,
|
|
||||||
tables: tables.filter((table) =>
|
|
||||||
shouldShowTablesBySchemaFilter(table, filteredSchemas)
|
|
||||||
),
|
|
||||||
mode: 'all', // Use 'all' mode for manual reordering
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatedOverlapGraph = findOverlappingTables({
|
|
||||||
tables: newTables,
|
|
||||||
});
|
|
||||||
|
|
||||||
updateTablesState((currentTables) =>
|
|
||||||
currentTables.map((table) => {
|
|
||||||
const newTable = newTables.find((t) => t.id === table.id);
|
|
||||||
return {
|
|
||||||
id: table.id,
|
|
||||||
x: newTable?.x ?? table.x,
|
|
||||||
y: newTable?.y ?? table.y,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setOverlapGraph(updatedOverlapGraph);
|
|
||||||
}, [filteredSchemas, relationships, tables, updateTablesState]);
|
|
||||||
|
|
||||||
const showReorderConfirmation = useCallback(() => {
|
const showReorderConfirmation = useCallback(() => {
|
||||||
showAlert({
|
showAlert({
|
||||||
title: t('reorder_diagram_alert.title'),
|
title: t('reorder_diagram_alert.title'),
|
||||||
@@ -871,12 +842,14 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
|||||||
>
|
>
|
||||||
<Toolbar readonly={readonly} />
|
<Toolbar readonly={readonly} />
|
||||||
</Controls>
|
</Controls>
|
||||||
<MiniMap
|
{showMiniMapOnCanvas && (
|
||||||
style={{
|
<MiniMap
|
||||||
width: isDesktop ? 100 : 60,
|
style={{
|
||||||
height: isDesktop ? 100 : 60,
|
width: isDesktop ? 100 : 60,
|
||||||
}}
|
height: isDesktop ? 100 : 60,
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Background
|
<Background
|
||||||
variant={BackgroundVariant.Dots}
|
variant={BackgroundVariant.Dots}
|
||||||
gap={16}
|
gap={16}
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import React, { useEffect, useMemo, useRef } from 'react';
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import {
|
import {
|
||||||
Handle,
|
Handle,
|
||||||
Position,
|
Position,
|
||||||
@@ -6,11 +12,17 @@ import {
|
|||||||
useUpdateNodeInternals,
|
useUpdateNodeInternals,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import { Button } from '@/components/button/button';
|
import { Button } from '@/components/button/button';
|
||||||
import { KeyRound, Trash2 } from 'lucide-react';
|
import { Check, KeyRound, MessageCircleMore, Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
import type { DBField } from '@/lib/domain/db-field';
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/tooltip/tooltip';
|
||||||
|
import { useClickAway, useKeyPressEvent } from 'react-use';
|
||||||
|
import { Input } from '@/components/input/input';
|
||||||
|
|
||||||
export const LEFT_HANDLE_ID_PREFIX = 'left_rel_';
|
export const LEFT_HANDLE_ID_PREFIX = 'left_rel_';
|
||||||
export const RIGHT_HANDLE_ID_PREFIX = 'right_rel_';
|
export const RIGHT_HANDLE_ID_PREFIX = 'right_rel_';
|
||||||
@@ -27,7 +39,12 @@ export interface TableNodeFieldProps {
|
|||||||
|
|
||||||
export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||||
({ field, focused, tableNodeId, highlighted, visible, isConnectable }) => {
|
({ field, focused, tableNodeId, highlighted, visible, isConnectable }) => {
|
||||||
const { removeField, relationships, readonly } = useChartDB();
|
const { removeField, relationships, readonly, updateField } =
|
||||||
|
useChartDB();
|
||||||
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
const [fieldName, setFieldName] = useState(field.name);
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const updateNodeInternals = useUpdateNodeInternals();
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
const connection = useConnection();
|
const connection = useConnection();
|
||||||
const isTarget = useMemo(
|
const isTarget = useMemo(
|
||||||
@@ -61,6 +78,28 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
|||||||
}
|
}
|
||||||
}, [tableNodeId, updateNodeInternals, numberOfEdgesToField]);
|
}, [tableNodeId, updateNodeInternals, numberOfEdgesToField]);
|
||||||
|
|
||||||
|
const editFieldName = useCallback(() => {
|
||||||
|
if (!editMode) return;
|
||||||
|
if (fieldName.trim()) {
|
||||||
|
updateField(tableNodeId, field.id, { name: fieldName.trim() });
|
||||||
|
}
|
||||||
|
setEditMode(false);
|
||||||
|
}, [fieldName, field.id, updateField, editMode, tableNodeId]);
|
||||||
|
|
||||||
|
const abortEdit = useCallback(() => {
|
||||||
|
setEditMode(false);
|
||||||
|
setFieldName(field.name);
|
||||||
|
}, [field.name]);
|
||||||
|
|
||||||
|
useClickAway(inputRef, editFieldName);
|
||||||
|
useKeyPressEvent('Enter', editFieldName);
|
||||||
|
useKeyPressEvent('Escape', abortEdit);
|
||||||
|
|
||||||
|
const enterEditMode = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setEditMode(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800 ${
|
className={`group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800 ${
|
||||||
@@ -113,42 +152,100 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="block truncate text-left">{field.name}</div>
|
<div
|
||||||
<div className="flex max-w-[35%] justify-end gap-2 truncate hover:shrink-0">
|
className={cn(
|
||||||
{field.primaryKey ? (
|
'flex items-center gap-1 truncate text-left',
|
||||||
|
{
|
||||||
|
'font-semibold': field.primaryKey || field.unique,
|
||||||
|
'w-full': editMode,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{editMode ? (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
onBlur={editFieldName}
|
||||||
|
placeholder={field.name}
|
||||||
|
autoFocus
|
||||||
|
type="text"
|
||||||
|
value={fieldName}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onChange={(e) => setFieldName(e.target.value)}
|
||||||
|
className="h-5 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
||||||
|
onClick={editFieldName}
|
||||||
|
>
|
||||||
|
<Check className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
// <span
|
||||||
|
// className="truncate"
|
||||||
|
// onClick={readonly ? undefined : enterEditMode}
|
||||||
|
// >
|
||||||
|
// {field.name}
|
||||||
|
// </span>
|
||||||
|
<span
|
||||||
|
className="truncate"
|
||||||
|
onDoubleClick={enterEditMode}
|
||||||
|
>
|
||||||
|
{field.name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{/* <span className="truncate">{field.name}</span> */}
|
||||||
|
{field.comments && !editMode ? (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="shrink-0 cursor-pointer text-muted-foreground">
|
||||||
|
<MessageCircleMore size={14} />
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>{field.comments}</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{editMode ? null : (
|
||||||
|
<div className="flex max-w-[35%] justify-end gap-1.5 truncate hover:shrink-0">
|
||||||
|
{field.primaryKey ? (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'text-muted-foreground',
|
||||||
|
!readonly ? 'group-hover:hidden' : ''
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<KeyRound size={14} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground',
|
'content-center truncate text-right text-xs text-muted-foreground shrink-0',
|
||||||
!readonly ? 'group-hover:hidden' : ''
|
!readonly ? 'group-hover:hidden' : ''
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<KeyRound size={14} />
|
{field.type.name}
|
||||||
|
{field.nullable ? '?' : ''}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
{readonly ? null : (
|
||||||
|
<div className="hidden flex-row group-hover:flex">
|
||||||
<div
|
<Button
|
||||||
className={cn(
|
variant="ghost"
|
||||||
'content-center truncate text-right text-xs text-muted-foreground',
|
className="size-6 p-0 hover:bg-primary-foreground"
|
||||||
!readonly ? 'group-hover:hidden' : ''
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
removeField(tableNodeId, field.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3.5 text-red-700" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
{field.type.name}
|
|
||||||
</div>
|
</div>
|
||||||
{readonly ? null : (
|
)}
|
||||||
<div className="hidden flex-row group-hover:flex">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="size-6 p-0 hover:bg-primary-foreground"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
removeField(tableNodeId, field.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-3.5 text-red-700" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Table2,
|
Table2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
|
Check,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Label } from '@/components/label/label';
|
import { Label } from '@/components/label/label';
|
||||||
import type { DBTable } from '@/lib/domain/db-table';
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
@@ -22,6 +23,13 @@ import { TableNodeContextMenu } from './table-node-context-menu';
|
|||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { TableNodeDependencyIndicator } from './table-node-dependency-indicator';
|
import { TableNodeDependencyIndicator } from './table-node-dependency-indicator';
|
||||||
import type { EdgeType } from '../canvas';
|
import type { EdgeType } from '../canvas';
|
||||||
|
import { Input } from '@/components/input/input';
|
||||||
|
import { useClickAway, useKeyPressEvent } from 'react-use';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/tooltip/tooltip';
|
||||||
|
|
||||||
export type TableNodeType = Node<
|
export type TableNodeType = Node<
|
||||||
{
|
{
|
||||||
@@ -49,6 +57,9 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
|||||||
const { openTableFromSidebar, selectSidebarSection } = useLayout();
|
const { openTableFromSidebar, selectSidebarSection } = useLayout();
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const [editMode, setEditMode] = useState(false);
|
||||||
|
const [tableName, setTableName] = useState(table.name);
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const selectedRelEdges = edges.filter(
|
const selectedRelEdges = edges.filter(
|
||||||
(edge) =>
|
(edge) =>
|
||||||
@@ -125,6 +136,28 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
|||||||
].sort((a, b) => table.fields.indexOf(a) - table.fields.indexOf(b));
|
].sort((a, b) => table.fields.indexOf(a) - table.fields.indexOf(b));
|
||||||
}, [expanded, table.fields, isMustDisplayedField]);
|
}, [expanded, table.fields, isMustDisplayedField]);
|
||||||
|
|
||||||
|
const editTableName = useCallback(() => {
|
||||||
|
if (!editMode) return;
|
||||||
|
if (tableName.trim()) {
|
||||||
|
updateTable(table.id, { name: tableName.trim() });
|
||||||
|
}
|
||||||
|
setEditMode(false);
|
||||||
|
}, [tableName, table.id, updateTable, editMode]);
|
||||||
|
|
||||||
|
const abortEdit = useCallback(() => {
|
||||||
|
setEditMode(false);
|
||||||
|
setTableName(table.name);
|
||||||
|
}, [table.name]);
|
||||||
|
|
||||||
|
useClickAway(inputRef, editTableName);
|
||||||
|
useKeyPressEvent('Enter', editTableName);
|
||||||
|
useKeyPressEvent('Escape', abortEdit);
|
||||||
|
|
||||||
|
const enterEditMode = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setEditMode(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableNodeContextMenu table={table}>
|
<TableNodeContextMenu table={table}>
|
||||||
<div
|
<div
|
||||||
@@ -168,12 +201,47 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
|||||||
<div className="group flex h-9 items-center justify-between bg-slate-200 px-2 dark:bg-slate-900">
|
<div className="group flex h-9 items-center justify-between bg-slate-200 px-2 dark:bg-slate-900">
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||||
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
|
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
|
||||||
<Label className="truncate text-sm font-bold">
|
{editMode ? (
|
||||||
{table.name}
|
<>
|
||||||
</Label>
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
onBlur={editTableName}
|
||||||
|
placeholder={table.name}
|
||||||
|
autoFocus
|
||||||
|
type="text"
|
||||||
|
value={tableName}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onChange={(e) =>
|
||||||
|
setTableName(e.target.value)
|
||||||
|
}
|
||||||
|
className="h-6 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
||||||
|
onClick={editTableName}
|
||||||
|
>
|
||||||
|
<Check className="size-4" />
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label
|
||||||
|
className="text-editable truncate px-2 py-0.5 text-sm font-bold"
|
||||||
|
onDoubleClick={enterEditMode}
|
||||||
|
>
|
||||||
|
{table.name}
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{t('tool_tips.double_click_to_edit')}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden shrink-0 flex-row group-hover:flex">
|
<div className="hidden shrink-0 flex-row group-hover:flex">
|
||||||
{readonly ? null : (
|
{readonly || editMode ? null : (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
||||||
@@ -182,21 +250,23 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
|||||||
<Pencil className="size-4" />
|
<Pencil className="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button
|
{editMode ? null : (
|
||||||
variant="ghost"
|
<Button
|
||||||
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
variant="ghost"
|
||||||
onClick={
|
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
||||||
table.width !== MAX_TABLE_SIZE
|
onClick={
|
||||||
? expandTable
|
table.width !== MAX_TABLE_SIZE
|
||||||
: shrinkTable
|
? expandTable
|
||||||
}
|
: shrinkTable
|
||||||
>
|
}
|
||||||
{table.width !== MAX_TABLE_SIZE ? (
|
>
|
||||||
<ChevronsLeftRight className="size-4" />
|
{table.width !== MAX_TABLE_SIZE ? (
|
||||||
) : (
|
<ChevronsLeftRight className="size-4" />
|
||||||
<ChevronsRightLeft className="size-4" />
|
) : (
|
||||||
)}
|
<ChevronsRightLeft className="size-4" />
|
||||||
</Button>
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
} from '@/components/tooltip/tooltip';
|
} from '@/components/tooltip/tooltip';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from '@/components/button/button';
|
import { Button } from '@/components/button/button';
|
||||||
|
import { keyboardShortcutsForOS } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
|
||||||
|
import { KeyboardShortcutAction } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
|
||||||
|
|
||||||
const convertToPercentage = (value: number) => `${Math.round(value * 100)}%`;
|
const convertToPercentage = (value: number) => `${Math.round(value * 100)}%`;
|
||||||
|
|
||||||
@@ -75,6 +77,14 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{t('toolbar.save')}
|
{t('toolbar.save')}
|
||||||
|
<span className="ml-2 text-muted-foreground">
|
||||||
|
{
|
||||||
|
keyboardShortcutsForOS[
|
||||||
|
KeyboardShortcutAction
|
||||||
|
.SAVE_DIAGRAM
|
||||||
|
].keyCombinationLabel
|
||||||
|
}
|
||||||
|
</span>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Separator orientation="vertical" />
|
<Separator orientation="vertical" />
|
||||||
@@ -88,7 +98,16 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
|
|||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{t('toolbar.show_all')}</TooltipContent>
|
<TooltipContent>
|
||||||
|
{t('toolbar.show_all')}
|
||||||
|
<span className="ml-2 text-muted-foreground">
|
||||||
|
{
|
||||||
|
keyboardShortcutsForOS[
|
||||||
|
KeyboardShortcutAction.SHOW_ALL
|
||||||
|
].keyCombinationLabel
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Separator orientation="vertical" />
|
<Separator orientation="vertical" />
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
@@ -130,7 +149,16 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
|
|||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{t('toolbar.undo')}</TooltipContent>
|
<TooltipContent>
|
||||||
|
{t('toolbar.undo')}
|
||||||
|
<span className="ml-2 text-muted-foreground">
|
||||||
|
{
|
||||||
|
keyboardShortcutsForOS[
|
||||||
|
KeyboardShortcutAction.UNDO
|
||||||
|
].keyCombinationLabel
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
@@ -143,7 +171,16 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
|
|||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{t('toolbar.redo')}</TooltipContent>
|
<TooltipContent>
|
||||||
|
{t('toolbar.redo')}
|
||||||
|
<span className="ml-2 text-muted-foreground">
|
||||||
|
{
|
||||||
|
keyboardShortcutsForOS[
|
||||||
|
KeyboardShortcutAction.REDO
|
||||||
|
].keyCombinationLabel
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -36,10 +36,16 @@ import { KeyboardShortcutsProvider } from '@/context/keyboard-shortcuts-context/
|
|||||||
import { Spinner } from '@/components/spinner/spinner';
|
import { Spinner } from '@/components/spinner/spinner';
|
||||||
import { Helmet } from 'react-helmet-async';
|
import { Helmet } from 'react-helmet-async';
|
||||||
import { useStorage } from '@/hooks/use-storage';
|
import { useStorage } from '@/hooks/use-storage';
|
||||||
|
import { AlertProvider } from '@/context/alert-context/alert-provider';
|
||||||
|
import { CanvasProvider } from '@/context/canvas-context/canvas-provider';
|
||||||
|
|
||||||
const OPEN_STAR_US_AFTER_SECONDS = 30;
|
const OPEN_STAR_US_AFTER_SECONDS = 30;
|
||||||
const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1;
|
const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1;
|
||||||
|
|
||||||
|
const OPEN_BUCKLE_AFTER_SECONDS = 60;
|
||||||
|
const SHOW_BUCKLE_AGAIN_AFTER_DAYS = 1;
|
||||||
|
const SHOW_BUCKLE_AGAIN_OPENED_AFTER_DAYS = 7;
|
||||||
|
|
||||||
export const EditorDesktopLayoutLazy = React.lazy(
|
export const EditorDesktopLayoutLazy = React.lazy(
|
||||||
() => import('./editor-desktop-layout')
|
() => import('./editor-desktop-layout')
|
||||||
);
|
);
|
||||||
@@ -59,7 +65,8 @@ const EditorPageComponent: React.FC = () => {
|
|||||||
const { openSelectSchema, showSidePanel } = useLayout();
|
const { openSelectSchema, showSidePanel } = useLayout();
|
||||||
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
|
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
|
||||||
const { showLoader, hideLoader } = useFullScreenLoader();
|
const { showLoader, hideLoader } = useFullScreenLoader();
|
||||||
const { openCreateDiagramDialog, openStarUsDialog } = useDialog();
|
const { openCreateDiagramDialog, openStarUsDialog, openBuckleDialog } =
|
||||||
|
useDialog();
|
||||||
const { diagramId } = useParams<{ diagramId: string }>();
|
const { diagramId } = useParams<{ diagramId: string }>();
|
||||||
const { config, updateConfig } = useConfig();
|
const { config, updateConfig } = useConfig();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -71,6 +78,9 @@ const EditorPageComponent: React.FC = () => {
|
|||||||
starUsDialogLastOpen,
|
starUsDialogLastOpen,
|
||||||
setStarUsDialogLastOpen,
|
setStarUsDialogLastOpen,
|
||||||
githubRepoOpened,
|
githubRepoOpened,
|
||||||
|
setBuckleDialogLastOpen,
|
||||||
|
buckleDialogLastOpen,
|
||||||
|
buckleWaitlistOpened,
|
||||||
} = useLocalConfig();
|
} = useLocalConfig();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -163,6 +173,33 @@ const EditorPageComponent: React.FC = () => {
|
|||||||
starUsDialogLastOpen,
|
starUsDialogLastOpen,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentDiagram?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
new Date().getTime() - buckleDialogLastOpen >
|
||||||
|
1000 *
|
||||||
|
60 *
|
||||||
|
60 *
|
||||||
|
24 *
|
||||||
|
(buckleWaitlistOpened
|
||||||
|
? SHOW_BUCKLE_AGAIN_OPENED_AFTER_DAYS
|
||||||
|
: SHOW_BUCKLE_AGAIN_AFTER_DAYS)
|
||||||
|
) {
|
||||||
|
const lastOpen = new Date().getTime();
|
||||||
|
setBuckleDialogLastOpen(lastOpen);
|
||||||
|
setTimeout(openBuckleDialog, OPEN_BUCKLE_AFTER_SECONDS * 1000);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
currentDiagram?.id,
|
||||||
|
buckleWaitlistOpened,
|
||||||
|
openBuckleDialog,
|
||||||
|
setBuckleDialogLastOpen,
|
||||||
|
buckleDialogLastOpen,
|
||||||
|
]);
|
||||||
|
|
||||||
const lastDiagramId = useRef<string>('');
|
const lastDiagramId = useRef<string>('');
|
||||||
|
|
||||||
const handleChangeSchema = useCallback(async () => {
|
const handleChangeSchema = useCallback(async () => {
|
||||||
@@ -279,13 +316,17 @@ export const EditorPage: React.FC = () => (
|
|||||||
<ChartDBProvider>
|
<ChartDBProvider>
|
||||||
<HistoryProvider>
|
<HistoryProvider>
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<ExportImageProvider>
|
<CanvasProvider>
|
||||||
<DialogProvider>
|
<ExportImageProvider>
|
||||||
<KeyboardShortcutsProvider>
|
<AlertProvider>
|
||||||
<EditorPageComponent />
|
<DialogProvider>
|
||||||
</KeyboardShortcutsProvider>
|
<KeyboardShortcutsProvider>
|
||||||
</DialogProvider>
|
<EditorPageComponent />
|
||||||
</ExportImageProvider>
|
</KeyboardShortcutsProvider>
|
||||||
|
</DialogProvider>
|
||||||
|
</AlertProvider>
|
||||||
|
</ExportImageProvider>
|
||||||
|
</CanvasProvider>
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
</HistoryProvider>
|
</HistoryProvider>
|
||||||
</ChartDBProvider>
|
</ChartDBProvider>
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
|
||||||
|
import type { EffectiveTheme } from '@/context/theme-context/theme-context';
|
||||||
|
import { importer } from '@dbml/core';
|
||||||
|
import { exportBaseSQL } from '@/lib/data/export-metadata/export-sql-script';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import { useToast } from '@/components/toast/use-toast';
|
||||||
|
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
|
||||||
|
|
||||||
|
export interface TableDBMLProps {
|
||||||
|
filteredTables: DBTable[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const getEditorTheme = (theme: EffectiveTheme) => {
|
||||||
|
return theme === 'dark' ? 'dbml-dark' : 'dbml-light';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
|
||||||
|
const { currentDiagram } = useChartDB();
|
||||||
|
const { effectiveTheme } = useTheme();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const generateDBML = useMemo(() => {
|
||||||
|
const filteredDiagram: Diagram = {
|
||||||
|
...currentDiagram,
|
||||||
|
tables: filteredTables,
|
||||||
|
relationships:
|
||||||
|
currentDiagram.relationships?.filter((rel) => {
|
||||||
|
const sourceTable = filteredTables.find(
|
||||||
|
(t) => t.id === rel.sourceTableId
|
||||||
|
);
|
||||||
|
const targetTable = filteredTables.find(
|
||||||
|
(t) => t.id === rel.targetTableId
|
||||||
|
);
|
||||||
|
|
||||||
|
return sourceTable && targetTable;
|
||||||
|
}) ?? [],
|
||||||
|
} satisfies Diagram;
|
||||||
|
|
||||||
|
const filteredDiagramWithoutSpaces: Diagram = {
|
||||||
|
...filteredDiagram,
|
||||||
|
tables:
|
||||||
|
filteredDiagram.tables?.map((table) => ({
|
||||||
|
...table,
|
||||||
|
name: table.name.replace(/\s/g, '_'),
|
||||||
|
fields: table.fields.map((field) => ({
|
||||||
|
...field,
|
||||||
|
name: field.name.replace(/\s/g, '_'),
|
||||||
|
})),
|
||||||
|
indexes: table.indexes?.map((index) => ({
|
||||||
|
...index,
|
||||||
|
name: index.name.replace(/\s/g, '_'),
|
||||||
|
})),
|
||||||
|
})) ?? [],
|
||||||
|
} satisfies Diagram;
|
||||||
|
|
||||||
|
const baseScript = exportBaseSQL(filteredDiagramWithoutSpaces);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return importer.import(baseScript, 'postgres');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description:
|
||||||
|
'Failed to generate DBML. We would appreciate if you could report this issue!',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}, [currentDiagram, filteredTables, toast]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CodeSnippet
|
||||||
|
code={generateDBML}
|
||||||
|
className="my-0.5"
|
||||||
|
editorProps={{
|
||||||
|
height: '100%',
|
||||||
|
defaultLanguage: 'dbml',
|
||||||
|
beforeMount: setupDBMLLanguage,
|
||||||
|
loading: false,
|
||||||
|
theme: getEditorTheme(effectiveTheme),
|
||||||
|
options: {
|
||||||
|
wordWrap: 'off',
|
||||||
|
mouseWheelZoom: false,
|
||||||
|
domReadOnly: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -73,8 +73,14 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
|
|||||||
setEditMode(false);
|
setEditMode(false);
|
||||||
}, [tableName, table.id, updateTable, editMode]);
|
}, [tableName, table.id, updateTable, editMode]);
|
||||||
|
|
||||||
|
const abortEdit = useCallback(() => {
|
||||||
|
setEditMode(false);
|
||||||
|
setTableName(table.name);
|
||||||
|
}, [table.name]);
|
||||||
|
|
||||||
useClickAway(inputRef, editTableName);
|
useClickAway(inputRef, editTableName);
|
||||||
useKeyPressEvent('Enter', editTableName);
|
useKeyPressEvent('Enter', editTableName);
|
||||||
|
useKeyPressEvent('Escape', abortEdit);
|
||||||
|
|
||||||
const enterEditMode = (e: React.MouseEvent) => {
|
const enterEditMode = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { TableList } from './table-list/table-list';
|
import { TableList } from './table-list/table-list';
|
||||||
import { Button } from '@/components/button/button';
|
import { Button } from '@/components/button/button';
|
||||||
import { Table, ListCollapse } from 'lucide-react';
|
import { Table, List, X, Code } from 'lucide-react';
|
||||||
import { Input } from '@/components/input/input';
|
import { Input } from '@/components/input/input';
|
||||||
|
|
||||||
import type { DBTable } from '@/lib/domain/db-table';
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
|
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
@@ -18,6 +17,9 @@ import {
|
|||||||
} from '@/components/tooltip/tooltip';
|
} from '@/components/tooltip/tooltip';
|
||||||
import { useViewport } from '@xyflow/react';
|
import { useViewport } from '@xyflow/react';
|
||||||
import { useDialog } from '@/hooks/use-dialog';
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
|
import { TableDBML } from './table-dbml/table-dbml';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { getOperatingSystem } from '@/lib/utils';
|
||||||
|
|
||||||
export interface TablesSectionProps {}
|
export interface TablesSectionProps {}
|
||||||
|
|
||||||
@@ -26,8 +28,10 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
|
|||||||
const { openTableSchemaDialog } = useDialog();
|
const { openTableSchemaDialog } = useDialog();
|
||||||
const viewport = useViewport();
|
const viewport = useViewport();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { closeAllTablesInSidebar, openTableFromSidebar } = useLayout();
|
const { openTableFromSidebar } = useLayout();
|
||||||
const [filterText, setFilterText] = React.useState('');
|
const [filterText, setFilterText] = React.useState('');
|
||||||
|
const [showDBML, setShowDBML] = useState(false);
|
||||||
|
const filterInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const filteredTables = useMemo(() => {
|
const filteredTables = useMemo(() => {
|
||||||
const filterTableName: (table: DBTable) => boolean = (table) =>
|
const filterTableName: (table: DBTable) => boolean = (table) =>
|
||||||
@@ -88,6 +92,34 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
|
|||||||
setFilterText,
|
setFilterText,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const handleClearFilter = useCallback(() => {
|
||||||
|
setFilterText('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const operatingSystem = useMemo(() => getOperatingSystem(), []);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
operatingSystem === 'mac' ? 'meta+f' : 'ctrl+f',
|
||||||
|
() => {
|
||||||
|
filterInputRef.current?.focus();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[filterInputRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
operatingSystem === 'mac' ? 'meta+p' : 'ctrl+p',
|
||||||
|
() => {
|
||||||
|
setShowDBML((value) => !value);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[setShowDBML]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className="flex flex-1 flex-col overflow-hidden px-2"
|
className="flex flex-1 flex-col overflow-hidden px-2"
|
||||||
@@ -101,19 +133,29 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="size-8 p-0"
|
className="size-8 p-0"
|
||||||
onClick={closeAllTablesInSidebar}
|
onClick={() =>
|
||||||
|
setShowDBML((value) => !value)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<ListCollapse className="size-4" />
|
{showDBML ? (
|
||||||
|
<List className="size-4" />
|
||||||
|
) : (
|
||||||
|
<Code className="size-4" />
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{t('side_panel.tables_section.collapse')}
|
{showDBML
|
||||||
|
? t('side_panel.tables_section.show_list')
|
||||||
|
: t('side_panel.tables_section.show_dbml')}
|
||||||
|
{operatingSystem === 'mac' ? ' (⌘P)' : ' (Ctrl+P)'}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Input
|
<Input
|
||||||
|
ref={filterInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('side_panel.tables_section.filter')}
|
placeholder={t('side_panel.tables_section.filter')}
|
||||||
className="h-8 w-full focus-visible:ring-0"
|
className="h-8 w-full focus-visible:ring-0"
|
||||||
@@ -131,21 +173,40 @@ export const TablesSection: React.FC<TablesSectionProps> = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 flex-col overflow-hidden">
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
<ScrollArea className="h-full">
|
{showDBML ? (
|
||||||
{tables.length === 0 ? (
|
<TableDBML filteredTables={filteredTables} />
|
||||||
<EmptyState
|
) : (
|
||||||
title={t(
|
<ScrollArea className="h-full">
|
||||||
'side_panel.tables_section.empty_state.title'
|
{tables.length === 0 ? (
|
||||||
)}
|
<EmptyState
|
||||||
description={t(
|
title={t(
|
||||||
'side_panel.tables_section.empty_state.description'
|
'side_panel.tables_section.empty_state.title'
|
||||||
)}
|
)}
|
||||||
className="mt-20"
|
description={t(
|
||||||
/>
|
'side_panel.tables_section.empty_state.description'
|
||||||
) : (
|
)}
|
||||||
<TableList tables={filteredTables} />
|
className="mt-20"
|
||||||
)}
|
/>
|
||||||
</ScrollArea>
|
) : filterText && filteredTables.length === 0 ? (
|
||||||
|
<div className="mt-10 flex flex-col items-center gap-2">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{t('side_panel.tables_section.no_results')}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleClearFilter}
|
||||||
|
className="gap-1"
|
||||||
|
>
|
||||||
|
<X className="size-3.5" />
|
||||||
|
{t('side_panel.tables_section.clear')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<TableList tables={filteredTables} />
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { Label } from '@/components/label/label';
|
|
||||||
import { Button } from '@/components/button/button';
|
import { Button } from '@/components/button/button';
|
||||||
import { Check } from 'lucide-react';
|
import { Check } from 'lucide-react';
|
||||||
import { Input } from '@/components/input/input';
|
import { Input } from '@/components/input/input';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { useClickAway, useKeyPressEvent } from 'react-use';
|
import { useClickAway, useKeyPressEvent } from 'react-use';
|
||||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
|
||||||
import { DiagramIcon } from '@/components/diagram-icon/diagram-icon';
|
import { DiagramIcon } from '@/components/diagram-icon/diagram-icon';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -22,7 +20,6 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
|
|||||||
const { diagramName, updateDiagramName, currentDiagram } = useChartDB();
|
const { diagramName, updateDiagramName, currentDiagram } = useChartDB();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isMd: isDesktop } = useBreakpoint('md');
|
|
||||||
const [editMode, setEditMode] = useState(false);
|
const [editMode, setEditMode] = useState(false);
|
||||||
const [editedDiagramName, setEditedDiagramName] =
|
const [editedDiagramName, setEditedDiagramName] =
|
||||||
React.useState(diagramName);
|
React.useState(diagramName);
|
||||||
@@ -54,7 +51,7 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
|
|||||||
<div className="group">
|
<div className="group">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex flex-1 flex-row items-center justify-center px-2 py-1',
|
'flex flex-1 flex-row items-center justify-center px-2 py-1 whitespace-nowrap',
|
||||||
{
|
{
|
||||||
'text-editable': !editMode,
|
'text-editable': !editMode,
|
||||||
}
|
}
|
||||||
@@ -64,9 +61,6 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
|
|||||||
databaseType={currentDiagram.databaseType}
|
databaseType={currentDiagram.databaseType}
|
||||||
databaseEdition={currentDiagram.databaseEdition}
|
databaseEdition={currentDiagram.databaseEdition}
|
||||||
/>
|
/>
|
||||||
<div className="flex">
|
|
||||||
{isDesktop ? <Label>{t('diagrams')}/</Label> : null}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row items-center gap-1">
|
<div className="flex flex-row items-center gap-1">
|
||||||
{editMode ? (
|
{editMode ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/tooltip/tooltip';
|
} from '@/components/tooltip/tooltip';
|
||||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { LocaleFunc } from 'timeago.js';
|
import type { LocaleFunc } from 'timeago.js';
|
||||||
import { register as registerLocale } from 'timeago.js';
|
import { register as registerLocale } from 'timeago.js';
|
||||||
|
import { Save } from 'lucide-react';
|
||||||
|
|
||||||
export interface LastSavedProps {}
|
export interface LastSavedProps {}
|
||||||
|
|
||||||
const timeAgolocaleFromLanguage = async (
|
const timeAgolocaleFromLanguage = async (
|
||||||
@@ -73,8 +74,7 @@ const timeAgolocaleFromLanguage = async (
|
|||||||
|
|
||||||
export const LastSaved: React.FC<LastSavedProps> = () => {
|
export const LastSaved: React.FC<LastSavedProps> = () => {
|
||||||
const { currentDiagram } = useChartDB();
|
const { currentDiagram } = useChartDB();
|
||||||
const { t, i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const { isMd: isDesktop } = useBreakpoint('md');
|
|
||||||
const [language, setLanguage] = useState<string>('en_US');
|
const [language, setLanguage] = useState<string>('en_US');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -93,8 +93,11 @@ export const LastSaved: React.FC<LastSavedProps> = () => {
|
|||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
<Badge variant="secondary" className="flex gap-1">
|
<Badge
|
||||||
{isDesktop ? t('last_saved') : t('saved')}
|
variant="secondary"
|
||||||
|
className="flex gap-1.5 whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<Save size={16} />
|
||||||
<TimeAgo
|
<TimeAgo
|
||||||
datetime={currentDiagram.updatedAt}
|
datetime={currentDiagram.updatedAt}
|
||||||
locale={language}
|
locale={language}
|
||||||
|
|||||||
517
src/pages/editor-page/top-navbar/menu/menu.tsx
Normal file
517
src/pages/editor-page/top-navbar/menu/menu.tsx
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Menubar,
|
||||||
|
MenubarCheckboxItem,
|
||||||
|
MenubarContent,
|
||||||
|
MenubarItem,
|
||||||
|
MenubarMenu,
|
||||||
|
MenubarSeparator,
|
||||||
|
MenubarShortcut,
|
||||||
|
MenubarSub,
|
||||||
|
MenubarSubContent,
|
||||||
|
MenubarSubTrigger,
|
||||||
|
MenubarTrigger,
|
||||||
|
} from '@/components/menubar/menubar';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
|
import { useExportImage } from '@/hooks/use-export-image';
|
||||||
|
import { databaseTypeToLabelMap } from '@/lib/databases';
|
||||||
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
import { useConfig } from '@/hooks/use-config';
|
||||||
|
import { IS_CHARTDB_IO } from '@/lib/env';
|
||||||
|
import {
|
||||||
|
KeyboardShortcutAction,
|
||||||
|
keyboardShortcutsForOS,
|
||||||
|
} from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
|
||||||
|
import { useHistory } from '@/hooks/use-history';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useAlert } from '@/context/alert-context/alert-context';
|
||||||
|
|
||||||
|
export interface MenuProps {}
|
||||||
|
|
||||||
|
export const Menu: React.FC<MenuProps> = () => {
|
||||||
|
const {
|
||||||
|
clearDiagramData,
|
||||||
|
deleteDiagram,
|
||||||
|
updateDiagramUpdatedAt,
|
||||||
|
databaseType,
|
||||||
|
} = useChartDB();
|
||||||
|
const {
|
||||||
|
openCreateDiagramDialog,
|
||||||
|
openOpenDiagramDialog,
|
||||||
|
openExportSQLDialog,
|
||||||
|
openImportDatabaseDialog,
|
||||||
|
openExportImageDialog,
|
||||||
|
openExportDiagramDialog,
|
||||||
|
openImportDiagramDialog,
|
||||||
|
openImportDBMLDialog,
|
||||||
|
} = useDialog();
|
||||||
|
const { showAlert } = useAlert();
|
||||||
|
const { setTheme, theme } = useTheme();
|
||||||
|
const { hideSidePanel, isSidePanelShowed, showSidePanel } = useLayout();
|
||||||
|
const {
|
||||||
|
scrollAction,
|
||||||
|
setScrollAction,
|
||||||
|
setShowCardinality,
|
||||||
|
showCardinality,
|
||||||
|
setShowDependenciesOnCanvas,
|
||||||
|
showDependenciesOnCanvas,
|
||||||
|
setShowMiniMapOnCanvas,
|
||||||
|
showMiniMapOnCanvas,
|
||||||
|
} = useLocalConfig();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { redo, undo, hasRedo, hasUndo } = useHistory();
|
||||||
|
const { config, updateConfig } = useConfig();
|
||||||
|
const { exportImage } = useExportImage();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleDeleteDiagramAction = useCallback(() => {
|
||||||
|
deleteDiagram();
|
||||||
|
navigate('/');
|
||||||
|
}, [deleteDiagram, navigate]);
|
||||||
|
|
||||||
|
const createNewDiagram = () => {
|
||||||
|
openCreateDiagramDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDiagram = () => {
|
||||||
|
openOpenDiagramDialog();
|
||||||
|
};
|
||||||
|
|
||||||
|
const exportSVG = useCallback(() => {
|
||||||
|
exportImage('svg', 1);
|
||||||
|
}, [exportImage]);
|
||||||
|
|
||||||
|
const exportPNG = useCallback(() => {
|
||||||
|
openExportImageDialog({
|
||||||
|
format: 'png',
|
||||||
|
});
|
||||||
|
}, [openExportImageDialog]);
|
||||||
|
|
||||||
|
const exportJPG = useCallback(() => {
|
||||||
|
openExportImageDialog({
|
||||||
|
format: 'jpeg',
|
||||||
|
});
|
||||||
|
}, [openExportImageDialog]);
|
||||||
|
|
||||||
|
const openChartDBIO = useCallback(() => {
|
||||||
|
window.location.href = 'https://chartdb.io';
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openJoinDiscord = useCallback(() => {
|
||||||
|
window.open('https://discord.gg/QeFwyWSKwC', '_blank');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openCalendly = useCallback(() => {
|
||||||
|
window.open('https://calendly.com/fishner/15min', '_blank');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const exportSQL = useCallback(
|
||||||
|
(databaseType: DatabaseType) => {
|
||||||
|
if (databaseType === DatabaseType.GENERIC) {
|
||||||
|
openExportSQLDialog({
|
||||||
|
targetDatabaseType: DatabaseType.GENERIC,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IS_CHARTDB_IO) {
|
||||||
|
const now = new Date();
|
||||||
|
const lastExportsInLastHalfHour =
|
||||||
|
config?.exportActions?.filter(
|
||||||
|
(date) =>
|
||||||
|
now.getTime() - date.getTime() < 30 * 60 * 1000
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
if (lastExportsInLastHalfHour.length >= 5) {
|
||||||
|
showAlert({
|
||||||
|
title: 'Export SQL Limit Reached',
|
||||||
|
content: (
|
||||||
|
<div className="flex flex-col gap-1 text-sm">
|
||||||
|
<div>
|
||||||
|
We set a budget to allow the community to
|
||||||
|
check the feature. You have reached the
|
||||||
|
limit of 5 AI exports every 30min.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Feel free to use your OPENAI_TOKEN, see the
|
||||||
|
manual{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/chartdb/chartdb"
|
||||||
|
target="_blank"
|
||||||
|
className="text-pink-600 hover:underline"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
here.
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
closeLabel: 'Close',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateConfig({
|
||||||
|
exportActions: [...lastExportsInLastHalfHour, now],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openExportSQLDialog({
|
||||||
|
targetDatabaseType: databaseType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[config?.exportActions, updateConfig, showAlert, openExportSQLDialog]
|
||||||
|
);
|
||||||
|
|
||||||
|
const showOrHideSidePanel = useCallback(() => {
|
||||||
|
if (isSidePanelShowed) {
|
||||||
|
hideSidePanel();
|
||||||
|
} else {
|
||||||
|
showSidePanel();
|
||||||
|
}
|
||||||
|
}, [isSidePanelShowed, showSidePanel, hideSidePanel]);
|
||||||
|
|
||||||
|
const showOrHideCardinality = useCallback(() => {
|
||||||
|
setShowCardinality(!showCardinality);
|
||||||
|
}, [showCardinality, setShowCardinality]);
|
||||||
|
|
||||||
|
const showOrHideDependencies = useCallback(() => {
|
||||||
|
setShowDependenciesOnCanvas(!showDependenciesOnCanvas);
|
||||||
|
}, [showDependenciesOnCanvas, setShowDependenciesOnCanvas]);
|
||||||
|
|
||||||
|
const showOrHideMiniMap = useCallback(() => {
|
||||||
|
setShowMiniMapOnCanvas(!showMiniMapOnCanvas);
|
||||||
|
}, [showMiniMapOnCanvas, setShowMiniMapOnCanvas]);
|
||||||
|
|
||||||
|
const emojiAI = '✨';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menubar className="h-8 border-none py-2 shadow-none md:h-10 md:py-0">
|
||||||
|
<MenubarMenu>
|
||||||
|
<MenubarTrigger>{t('menu.file.file')}</MenubarTrigger>
|
||||||
|
<MenubarContent>
|
||||||
|
<MenubarItem onClick={createNewDiagram}>
|
||||||
|
{t('menu.file.new')}
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem onClick={openDiagram}>
|
||||||
|
{t('menu.file.open')}
|
||||||
|
<MenubarShortcut>
|
||||||
|
{
|
||||||
|
keyboardShortcutsForOS[
|
||||||
|
KeyboardShortcutAction.OPEN_DIAGRAM
|
||||||
|
].keyCombinationLabel
|
||||||
|
}
|
||||||
|
</MenubarShortcut>
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem onClick={updateDiagramUpdatedAt}>
|
||||||
|
{t('menu.file.save')}
|
||||||
|
<MenubarShortcut>
|
||||||
|
{
|
||||||
|
keyboardShortcutsForOS[
|
||||||
|
KeyboardShortcutAction.SAVE_DIAGRAM
|
||||||
|
].keyCombinationLabel
|
||||||
|
}
|
||||||
|
</MenubarShortcut>
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarSeparator />
|
||||||
|
<MenubarSub>
|
||||||
|
<MenubarSubTrigger>
|
||||||
|
{t('menu.file.import')}
|
||||||
|
</MenubarSubTrigger>
|
||||||
|
<MenubarSubContent>
|
||||||
|
<MenubarItem onClick={() => openImportDBMLDialog()}>
|
||||||
|
.dbml
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarSeparator />
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() =>
|
||||||
|
openImportDatabaseDialog({
|
||||||
|
databaseType: DatabaseType.POSTGRESQL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{databaseTypeToLabelMap['postgresql']}
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() =>
|
||||||
|
openImportDatabaseDialog({
|
||||||
|
databaseType: DatabaseType.MYSQL,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{databaseTypeToLabelMap['mysql']}
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() =>
|
||||||
|
openImportDatabaseDialog({
|
||||||
|
databaseType: DatabaseType.SQL_SERVER,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{databaseTypeToLabelMap['sql_server']}
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() =>
|
||||||
|
openImportDatabaseDialog({
|
||||||
|
databaseType: DatabaseType.MARIADB,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{databaseTypeToLabelMap['mariadb']}
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() =>
|
||||||
|
openImportDatabaseDialog({
|
||||||
|
databaseType: DatabaseType.SQLITE,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{databaseTypeToLabelMap['sqlite']}
|
||||||
|
</MenubarItem>
|
||||||
|
</MenubarSubContent>
|
||||||
|
</MenubarSub>
|
||||||
|
<MenubarSeparator />
|
||||||
|
<MenubarSub>
|
||||||
|
<MenubarSubTrigger>
|
||||||
|
{t('menu.file.export_sql')}
|
||||||
|
</MenubarSubTrigger>
|
||||||
|
<MenubarSubContent>
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() => exportSQL(DatabaseType.GENERIC)}
|
||||||
|
>
|
||||||
|
{databaseTypeToLabelMap['generic']}
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() =>
|
||||||
|
exportSQL(DatabaseType.POSTGRESQL)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{databaseTypeToLabelMap['postgresql']}
|
||||||
|
<MenubarShortcut className="text-base">
|
||||||
|
{emojiAI}
|
||||||
|
</MenubarShortcut>
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() => exportSQL(DatabaseType.MYSQL)}
|
||||||
|
>
|
||||||
|
{databaseTypeToLabelMap['mysql']}
|
||||||
|
<MenubarShortcut className="text-base">
|
||||||
|
{emojiAI}
|
||||||
|
</MenubarShortcut>
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() =>
|
||||||
|
exportSQL(DatabaseType.SQL_SERVER)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{databaseTypeToLabelMap['sql_server']}
|
||||||
|
<MenubarShortcut className="text-base">
|
||||||
|
{emojiAI}
|
||||||
|
</MenubarShortcut>
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() => exportSQL(DatabaseType.MARIADB)}
|
||||||
|
>
|
||||||
|
{databaseTypeToLabelMap['mariadb']}
|
||||||
|
<MenubarShortcut className="text-base">
|
||||||
|
{emojiAI}
|
||||||
|
</MenubarShortcut>
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() => exportSQL(DatabaseType.SQLITE)}
|
||||||
|
>
|
||||||
|
{databaseTypeToLabelMap['sqlite']}
|
||||||
|
<MenubarShortcut className="text-base">
|
||||||
|
{emojiAI}
|
||||||
|
</MenubarShortcut>
|
||||||
|
</MenubarItem>
|
||||||
|
</MenubarSubContent>
|
||||||
|
</MenubarSub>
|
||||||
|
<MenubarSub>
|
||||||
|
<MenubarSubTrigger>
|
||||||
|
{t('menu.file.export_as')}
|
||||||
|
</MenubarSubTrigger>
|
||||||
|
<MenubarSubContent>
|
||||||
|
<MenubarItem onClick={exportPNG}>PNG</MenubarItem>
|
||||||
|
<MenubarItem onClick={exportJPG}>JPG</MenubarItem>
|
||||||
|
<MenubarItem onClick={exportSVG}>SVG</MenubarItem>
|
||||||
|
</MenubarSubContent>
|
||||||
|
</MenubarSub>
|
||||||
|
<MenubarSeparator />
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() =>
|
||||||
|
showAlert({
|
||||||
|
title: t('delete_diagram_alert.title'),
|
||||||
|
description: t(
|
||||||
|
'delete_diagram_alert.description'
|
||||||
|
),
|
||||||
|
actionLabel: t('delete_diagram_alert.delete'),
|
||||||
|
closeLabel: t('delete_diagram_alert.cancel'),
|
||||||
|
onAction: handleDeleteDiagramAction,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('menu.file.delete_diagram')}
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarSeparator />
|
||||||
|
<MenubarItem>{t('menu.file.exit')}</MenubarItem>
|
||||||
|
</MenubarContent>
|
||||||
|
</MenubarMenu>
|
||||||
|
<MenubarMenu>
|
||||||
|
<MenubarTrigger>{t('menu.edit.edit')}</MenubarTrigger>
|
||||||
|
<MenubarContent>
|
||||||
|
<MenubarItem onClick={undo} disabled={!hasUndo}>
|
||||||
|
{t('menu.edit.undo')}
|
||||||
|
<MenubarShortcut>
|
||||||
|
{
|
||||||
|
keyboardShortcutsForOS[
|
||||||
|
KeyboardShortcutAction.UNDO
|
||||||
|
].keyCombinationLabel
|
||||||
|
}
|
||||||
|
</MenubarShortcut>
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem onClick={redo} disabled={!hasRedo}>
|
||||||
|
{t('menu.edit.redo')}
|
||||||
|
<MenubarShortcut>
|
||||||
|
{
|
||||||
|
keyboardShortcutsForOS[
|
||||||
|
KeyboardShortcutAction.REDO
|
||||||
|
].keyCombinationLabel
|
||||||
|
}
|
||||||
|
</MenubarShortcut>
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarSeparator />
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() =>
|
||||||
|
showAlert({
|
||||||
|
title: t('clear_diagram_alert.title'),
|
||||||
|
description: t(
|
||||||
|
'clear_diagram_alert.description'
|
||||||
|
),
|
||||||
|
actionLabel: t('clear_diagram_alert.clear'),
|
||||||
|
closeLabel: t('clear_diagram_alert.cancel'),
|
||||||
|
onAction: clearDiagramData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('menu.edit.clear')}
|
||||||
|
</MenubarItem>
|
||||||
|
</MenubarContent>
|
||||||
|
</MenubarMenu>
|
||||||
|
<MenubarMenu>
|
||||||
|
<MenubarTrigger>{t('menu.view.view')}</MenubarTrigger>
|
||||||
|
<MenubarContent>
|
||||||
|
<MenubarItem onClick={showOrHideSidePanel}>
|
||||||
|
{isSidePanelShowed
|
||||||
|
? t('menu.view.hide_sidebar')
|
||||||
|
: t('menu.view.show_sidebar')}
|
||||||
|
<MenubarShortcut>
|
||||||
|
{
|
||||||
|
keyboardShortcutsForOS[
|
||||||
|
KeyboardShortcutAction.TOGGLE_SIDE_PANEL
|
||||||
|
].keyCombinationLabel
|
||||||
|
}
|
||||||
|
</MenubarShortcut>
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarSeparator />
|
||||||
|
<MenubarItem onClick={showOrHideCardinality}>
|
||||||
|
{showCardinality
|
||||||
|
? t('menu.view.hide_cardinality')
|
||||||
|
: t('menu.view.show_cardinality')}
|
||||||
|
</MenubarItem>
|
||||||
|
{databaseType !== DatabaseType.CLICKHOUSE ? (
|
||||||
|
<MenubarItem onClick={showOrHideDependencies}>
|
||||||
|
{showDependenciesOnCanvas
|
||||||
|
? t('menu.view.hide_dependencies')
|
||||||
|
: t('menu.view.show_dependencies')}
|
||||||
|
</MenubarItem>
|
||||||
|
) : null}
|
||||||
|
<MenubarItem onClick={showOrHideMiniMap}>
|
||||||
|
{showMiniMapOnCanvas
|
||||||
|
? t('menu.view.hide_minimap')
|
||||||
|
: t('menu.view.show_minimap')}
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarSeparator />
|
||||||
|
<MenubarSub>
|
||||||
|
<MenubarSubTrigger>
|
||||||
|
{t('menu.view.zoom_on_scroll')}
|
||||||
|
</MenubarSubTrigger>
|
||||||
|
<MenubarSubContent>
|
||||||
|
<MenubarCheckboxItem
|
||||||
|
checked={scrollAction === 'zoom'}
|
||||||
|
onClick={() => setScrollAction('zoom')}
|
||||||
|
>
|
||||||
|
{t('zoom.on')}
|
||||||
|
</MenubarCheckboxItem>
|
||||||
|
<MenubarCheckboxItem
|
||||||
|
checked={scrollAction === 'pan'}
|
||||||
|
onClick={() => setScrollAction('pan')}
|
||||||
|
>
|
||||||
|
{t('zoom.off')}
|
||||||
|
</MenubarCheckboxItem>
|
||||||
|
</MenubarSubContent>
|
||||||
|
</MenubarSub>
|
||||||
|
<MenubarSeparator />
|
||||||
|
<MenubarSub>
|
||||||
|
<MenubarSubTrigger>
|
||||||
|
{t('menu.view.theme')}
|
||||||
|
</MenubarSubTrigger>
|
||||||
|
<MenubarSubContent>
|
||||||
|
<MenubarCheckboxItem
|
||||||
|
checked={theme === 'system'}
|
||||||
|
onClick={() => setTheme('system')}
|
||||||
|
>
|
||||||
|
{t('theme.system')}
|
||||||
|
</MenubarCheckboxItem>
|
||||||
|
<MenubarCheckboxItem
|
||||||
|
checked={theme === 'light'}
|
||||||
|
onClick={() => setTheme('light')}
|
||||||
|
>
|
||||||
|
{t('theme.light')}
|
||||||
|
</MenubarCheckboxItem>
|
||||||
|
<MenubarCheckboxItem
|
||||||
|
checked={theme === 'dark'}
|
||||||
|
onClick={() => setTheme('dark')}
|
||||||
|
>
|
||||||
|
{t('theme.dark')}
|
||||||
|
</MenubarCheckboxItem>
|
||||||
|
</MenubarSubContent>
|
||||||
|
</MenubarSub>
|
||||||
|
</MenubarContent>
|
||||||
|
</MenubarMenu>
|
||||||
|
|
||||||
|
<MenubarMenu>
|
||||||
|
<MenubarTrigger>{t('menu.share.share')}</MenubarTrigger>
|
||||||
|
<MenubarContent>
|
||||||
|
<MenubarItem onClick={openExportDiagramDialog}>
|
||||||
|
{t('menu.share.export_diagram')}
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem onClick={openImportDiagramDialog}>
|
||||||
|
{t('menu.share.import_diagram')}
|
||||||
|
</MenubarItem>
|
||||||
|
</MenubarContent>
|
||||||
|
</MenubarMenu>
|
||||||
|
|
||||||
|
<MenubarMenu>
|
||||||
|
<MenubarTrigger>{t('menu.help.help')}</MenubarTrigger>
|
||||||
|
<MenubarContent>
|
||||||
|
<MenubarItem onClick={openChartDBIO}>
|
||||||
|
{t('menu.help.visit_website')}
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem onClick={openJoinDiscord}>
|
||||||
|
{t('menu.help.join_discord')}
|
||||||
|
</MenubarItem>
|
||||||
|
<MenubarItem onClick={openCalendly}>
|
||||||
|
{t('menu.help.schedule_a_call')}
|
||||||
|
</MenubarItem>
|
||||||
|
</MenubarContent>
|
||||||
|
</MenubarMenu>
|
||||||
|
</Menubar>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,177 +1,18 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import {
|
|
||||||
Menubar,
|
|
||||||
MenubarCheckboxItem,
|
|
||||||
MenubarContent,
|
|
||||||
MenubarItem,
|
|
||||||
MenubarMenu,
|
|
||||||
MenubarSeparator,
|
|
||||||
MenubarShortcut,
|
|
||||||
MenubarSub,
|
|
||||||
MenubarSubContent,
|
|
||||||
MenubarSubTrigger,
|
|
||||||
MenubarTrigger,
|
|
||||||
} from '@/components/menubar/menubar';
|
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
|
||||||
import ChartDBLogo from '@/assets/logo-light.png';
|
import ChartDBLogo from '@/assets/logo-light.png';
|
||||||
import ChartDBDarkLogo from '@/assets/logo-dark.png';
|
import ChartDBDarkLogo from '@/assets/logo-dark.png';
|
||||||
import { useDialog } from '@/hooks/use-dialog';
|
|
||||||
import { useExportImage } from '@/hooks/use-export-image';
|
|
||||||
import { databaseTypeToLabelMap } from '@/lib/databases';
|
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
|
||||||
import { useConfig } from '@/hooks/use-config';
|
|
||||||
import { IS_CHARTDB_IO } from '@/lib/env';
|
|
||||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
||||||
import {
|
|
||||||
KeyboardShortcutAction,
|
|
||||||
keyboardShortcutsForOS,
|
|
||||||
} from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
|
|
||||||
import { useHistory } from '@/hooks/use-history';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useLayout } from '@/hooks/use-layout';
|
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
|
||||||
import { DiagramName } from './diagram-name';
|
import { DiagramName } from './diagram-name';
|
||||||
import { LastSaved } from './last-saved';
|
import { LastSaved } from './last-saved';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { LanguageNav } from './language-nav/language-nav';
|
import { LanguageNav } from './language-nav/language-nav';
|
||||||
|
import { Menu } from './menu/menu';
|
||||||
|
|
||||||
export interface TopNavbarProps {}
|
export interface TopNavbarProps {}
|
||||||
|
|
||||||
export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
||||||
const {
|
|
||||||
clearDiagramData,
|
|
||||||
deleteDiagram,
|
|
||||||
updateDiagramUpdatedAt,
|
|
||||||
databaseType,
|
|
||||||
} = useChartDB();
|
|
||||||
const {
|
|
||||||
openCreateDiagramDialog,
|
|
||||||
openOpenDiagramDialog,
|
|
||||||
openExportSQLDialog,
|
|
||||||
openImportDatabaseDialog,
|
|
||||||
showAlert,
|
|
||||||
openExportImageDialog,
|
|
||||||
openExportDiagramDialog,
|
|
||||||
openImportDiagramDialog,
|
|
||||||
} = useDialog();
|
|
||||||
const { setTheme, theme } = useTheme();
|
|
||||||
const { hideSidePanel, isSidePanelShowed, showSidePanel } = useLayout();
|
|
||||||
const {
|
|
||||||
scrollAction,
|
|
||||||
setScrollAction,
|
|
||||||
setShowCardinality,
|
|
||||||
showCardinality,
|
|
||||||
setShowDependenciesOnCanvas,
|
|
||||||
showDependenciesOnCanvas,
|
|
||||||
} = useLocalConfig();
|
|
||||||
const { effectiveTheme } = useTheme();
|
const { effectiveTheme } = useTheme();
|
||||||
const { t } = useTranslation();
|
|
||||||
const { redo, undo, hasRedo, hasUndo } = useHistory();
|
|
||||||
const { isMd: isDesktop } = useBreakpoint('md');
|
const { isMd: isDesktop } = useBreakpoint('md');
|
||||||
const { config, updateConfig } = useConfig();
|
|
||||||
const { exportImage } = useExportImage();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleDeleteDiagramAction = useCallback(() => {
|
|
||||||
deleteDiagram();
|
|
||||||
navigate('/');
|
|
||||||
}, [deleteDiagram, navigate]);
|
|
||||||
|
|
||||||
const createNewDiagram = () => {
|
|
||||||
openCreateDiagramDialog();
|
|
||||||
};
|
|
||||||
|
|
||||||
const openDiagram = () => {
|
|
||||||
openOpenDiagramDialog();
|
|
||||||
};
|
|
||||||
|
|
||||||
const exportSVG = useCallback(() => {
|
|
||||||
exportImage('svg', 1);
|
|
||||||
}, [exportImage]);
|
|
||||||
|
|
||||||
const exportPNG = useCallback(() => {
|
|
||||||
openExportImageDialog({
|
|
||||||
format: 'png',
|
|
||||||
});
|
|
||||||
}, [openExportImageDialog]);
|
|
||||||
|
|
||||||
const exportJPG = useCallback(() => {
|
|
||||||
openExportImageDialog({
|
|
||||||
format: 'jpeg',
|
|
||||||
});
|
|
||||||
}, [openExportImageDialog]);
|
|
||||||
|
|
||||||
const openChartDBIO = useCallback(() => {
|
|
||||||
window.location.href = 'https://chartdb.io';
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const openJoinDiscord = useCallback(() => {
|
|
||||||
window.open('https://discord.gg/QeFwyWSKwC', '_blank');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const openCalendly = useCallback(() => {
|
|
||||||
window.open('https://calendly.com/fishner/15min', '_blank');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const exportSQL = useCallback(
|
|
||||||
(databaseType: DatabaseType) => {
|
|
||||||
if (databaseType === DatabaseType.GENERIC) {
|
|
||||||
openExportSQLDialog({
|
|
||||||
targetDatabaseType: DatabaseType.GENERIC,
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IS_CHARTDB_IO) {
|
|
||||||
const now = new Date();
|
|
||||||
const lastExportsInLastHalfHour =
|
|
||||||
config?.exportActions?.filter(
|
|
||||||
(date) =>
|
|
||||||
now.getTime() - date.getTime() < 30 * 60 * 1000
|
|
||||||
) ?? [];
|
|
||||||
|
|
||||||
if (lastExportsInLastHalfHour.length >= 5) {
|
|
||||||
showAlert({
|
|
||||||
title: 'Export SQL Limit Reached',
|
|
||||||
content: (
|
|
||||||
<div className="flex flex-col gap-1 text-sm">
|
|
||||||
<div>
|
|
||||||
We set a budget to allow the community to
|
|
||||||
check the feature. You have reached the
|
|
||||||
limit of 5 AI exports every 30min.
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
Feel free to use your OPENAI_TOKEN, see the
|
|
||||||
manual{' '}
|
|
||||||
<a
|
|
||||||
href="https://github.com/chartdb/chartdb"
|
|
||||||
target="_blank"
|
|
||||||
className="text-pink-600 hover:underline"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
here.
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
closeLabel: 'Close',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateConfig({
|
|
||||||
exportActions: [...lastExportsInLastHalfHour, now],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
openExportSQLDialog({
|
|
||||||
targetDatabaseType: databaseType,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[config?.exportActions, updateConfig, showAlert, openExportSQLDialog]
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderStars = useCallback(() => {
|
const renderStars = useCallback(() => {
|
||||||
return (
|
return (
|
||||||
@@ -184,27 +25,26 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
|||||||
);
|
);
|
||||||
}, [isDesktop]);
|
}, [isDesktop]);
|
||||||
|
|
||||||
const showOrHideSidePanel = useCallback(() => {
|
const openBuckleWaitlist = useCallback(() => {
|
||||||
if (isSidePanelShowed) {
|
window.open('https://waitlist.buckle.dev', '_blank');
|
||||||
hideSidePanel();
|
}, []);
|
||||||
} else {
|
|
||||||
showSidePanel();
|
|
||||||
}
|
|
||||||
}, [isSidePanelShowed, showSidePanel, hideSidePanel]);
|
|
||||||
|
|
||||||
const showOrHideCardinality = useCallback(() => {
|
const renderGetBuckleButton = useCallback(() => {
|
||||||
setShowCardinality(!showCardinality);
|
return (
|
||||||
}, [showCardinality, setShowCardinality]);
|
<button
|
||||||
|
className="gradient-background relative inline-flex items-center justify-center overflow-hidden rounded-lg p-0.5 text-base text-gray-700 focus:outline-none focus:ring-0"
|
||||||
const showOrHideDependencies = useCallback(() => {
|
onClick={openBuckleWaitlist}
|
||||||
setShowDependenciesOnCanvas(!showDependenciesOnCanvas);
|
>
|
||||||
}, [showDependenciesOnCanvas, setShowDependenciesOnCanvas]);
|
<span className="relative inline-flex items-center justify-center whitespace-nowrap rounded-md bg-background px-2 py-0.5 font-primary text-xs font-semibold text-foreground md:text-sm">
|
||||||
|
ChartDB v2.0 🔥
|
||||||
const emojiAI = '✨';
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}, [openBuckleWaitlist]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="flex flex-col justify-between border-b px-3 md:h-12 md:flex-row md:items-center md:px-4">
|
<nav className="flex flex-col justify-between border-b px-3 md:h-12 md:flex-row md:items-center md:px-4">
|
||||||
<div className="flex flex-1 flex-col justify-between gap-x-3 md:flex-row md:justify-normal">
|
<div className="flex flex-1 flex-col justify-between gap-x-1 md:flex-row md:justify-normal">
|
||||||
<div className="flex items-center justify-between pt-[8px] font-primary md:py-[10px]">
|
<div className="flex items-center justify-between pt-[8px] font-primary md:py-[10px]">
|
||||||
<a
|
<a
|
||||||
href="https://chartdb.io"
|
href="https://chartdb.io"
|
||||||
@@ -223,357 +63,19 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
|||||||
</a>
|
</a>
|
||||||
{!isDesktop ? (
|
{!isDesktop ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{renderGetBuckleButton()}
|
||||||
{renderStars()}
|
{renderStars()}
|
||||||
<LanguageNav />
|
<LanguageNav />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
<Menu />
|
||||||
<Menubar className="h-8 border-none py-2 shadow-none md:h-10 md:py-0">
|
|
||||||
<MenubarMenu>
|
|
||||||
<MenubarTrigger>{t('menu.file.file')}</MenubarTrigger>
|
|
||||||
<MenubarContent>
|
|
||||||
<MenubarItem onClick={createNewDiagram}>
|
|
||||||
{t('menu.file.new')}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem onClick={openDiagram}>
|
|
||||||
{t('menu.file.open')}
|
|
||||||
<MenubarShortcut>
|
|
||||||
{
|
|
||||||
keyboardShortcutsForOS[
|
|
||||||
KeyboardShortcutAction.OPEN_DIAGRAM
|
|
||||||
].keyCombinationLabel
|
|
||||||
}
|
|
||||||
</MenubarShortcut>
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem onClick={updateDiagramUpdatedAt}>
|
|
||||||
{t('menu.file.save')}
|
|
||||||
<MenubarShortcut>
|
|
||||||
{
|
|
||||||
keyboardShortcutsForOS[
|
|
||||||
KeyboardShortcutAction.SAVE_DIAGRAM
|
|
||||||
].keyCombinationLabel
|
|
||||||
}
|
|
||||||
</MenubarShortcut>
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarSeparator />
|
|
||||||
<MenubarSub>
|
|
||||||
<MenubarSubTrigger>
|
|
||||||
{t('menu.file.import_database')}
|
|
||||||
</MenubarSubTrigger>
|
|
||||||
<MenubarSubContent>
|
|
||||||
<MenubarItem
|
|
||||||
onClick={() =>
|
|
||||||
openImportDatabaseDialog({
|
|
||||||
databaseType:
|
|
||||||
DatabaseType.POSTGRESQL,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{databaseTypeToLabelMap['postgresql']}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem
|
|
||||||
onClick={() =>
|
|
||||||
openImportDatabaseDialog({
|
|
||||||
databaseType:
|
|
||||||
DatabaseType.MYSQL,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{databaseTypeToLabelMap['mysql']}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem
|
|
||||||
onClick={() =>
|
|
||||||
openImportDatabaseDialog({
|
|
||||||
databaseType:
|
|
||||||
DatabaseType.SQL_SERVER,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{databaseTypeToLabelMap['sql_server']}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem
|
|
||||||
onClick={() =>
|
|
||||||
openImportDatabaseDialog({
|
|
||||||
databaseType:
|
|
||||||
DatabaseType.MARIADB,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{databaseTypeToLabelMap['mariadb']}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem
|
|
||||||
onClick={() =>
|
|
||||||
openImportDatabaseDialog({
|
|
||||||
databaseType:
|
|
||||||
DatabaseType.SQLITE,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{databaseTypeToLabelMap['sqlite']}
|
|
||||||
</MenubarItem>
|
|
||||||
</MenubarSubContent>
|
|
||||||
</MenubarSub>
|
|
||||||
<MenubarSeparator />
|
|
||||||
<MenubarSub>
|
|
||||||
<MenubarSubTrigger>
|
|
||||||
{t('menu.file.export_sql')}
|
|
||||||
</MenubarSubTrigger>
|
|
||||||
<MenubarSubContent>
|
|
||||||
<MenubarItem
|
|
||||||
onClick={() =>
|
|
||||||
exportSQL(DatabaseType.GENERIC)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{databaseTypeToLabelMap['generic']}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem
|
|
||||||
onClick={() =>
|
|
||||||
exportSQL(DatabaseType.POSTGRESQL)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{databaseTypeToLabelMap['postgresql']}
|
|
||||||
<MenubarShortcut className="text-base">
|
|
||||||
{emojiAI}
|
|
||||||
</MenubarShortcut>
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem
|
|
||||||
onClick={() =>
|
|
||||||
exportSQL(DatabaseType.MYSQL)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{databaseTypeToLabelMap['mysql']}
|
|
||||||
<MenubarShortcut className="text-base">
|
|
||||||
{emojiAI}
|
|
||||||
</MenubarShortcut>
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem
|
|
||||||
onClick={() =>
|
|
||||||
exportSQL(DatabaseType.SQL_SERVER)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{databaseTypeToLabelMap['sql_server']}
|
|
||||||
<MenubarShortcut className="text-base">
|
|
||||||
{emojiAI}
|
|
||||||
</MenubarShortcut>
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem
|
|
||||||
onClick={() =>
|
|
||||||
exportSQL(DatabaseType.MARIADB)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{databaseTypeToLabelMap['mariadb']}
|
|
||||||
<MenubarShortcut className="text-base">
|
|
||||||
{emojiAI}
|
|
||||||
</MenubarShortcut>
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem
|
|
||||||
onClick={() =>
|
|
||||||
exportSQL(DatabaseType.SQLITE)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{databaseTypeToLabelMap['sqlite']}
|
|
||||||
<MenubarShortcut className="text-base">
|
|
||||||
{emojiAI}
|
|
||||||
</MenubarShortcut>
|
|
||||||
</MenubarItem>
|
|
||||||
</MenubarSubContent>
|
|
||||||
</MenubarSub>
|
|
||||||
<MenubarSub>
|
|
||||||
<MenubarSubTrigger>
|
|
||||||
{t('menu.file.export_as')}
|
|
||||||
</MenubarSubTrigger>
|
|
||||||
<MenubarSubContent>
|
|
||||||
<MenubarItem onClick={exportPNG}>
|
|
||||||
PNG
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem onClick={exportJPG}>
|
|
||||||
JPG
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem onClick={exportSVG}>
|
|
||||||
SVG
|
|
||||||
</MenubarItem>
|
|
||||||
</MenubarSubContent>
|
|
||||||
</MenubarSub>
|
|
||||||
<MenubarSeparator />
|
|
||||||
<MenubarItem
|
|
||||||
onClick={() =>
|
|
||||||
showAlert({
|
|
||||||
title: t('delete_diagram_alert.title'),
|
|
||||||
description: t(
|
|
||||||
'delete_diagram_alert.description'
|
|
||||||
),
|
|
||||||
actionLabel: t(
|
|
||||||
'delete_diagram_alert.delete'
|
|
||||||
),
|
|
||||||
closeLabel: t(
|
|
||||||
'delete_diagram_alert.cancel'
|
|
||||||
),
|
|
||||||
onAction: handleDeleteDiagramAction,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('menu.file.delete_diagram')}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarSeparator />
|
|
||||||
<MenubarItem>{t('menu.file.exit')}</MenubarItem>
|
|
||||||
</MenubarContent>
|
|
||||||
</MenubarMenu>
|
|
||||||
<MenubarMenu>
|
|
||||||
<MenubarTrigger>{t('menu.edit.edit')}</MenubarTrigger>
|
|
||||||
<MenubarContent>
|
|
||||||
<MenubarItem onClick={undo} disabled={!hasUndo}>
|
|
||||||
{t('menu.edit.undo')}
|
|
||||||
<MenubarShortcut>
|
|
||||||
{
|
|
||||||
keyboardShortcutsForOS[
|
|
||||||
KeyboardShortcutAction.UNDO
|
|
||||||
].keyCombinationLabel
|
|
||||||
}
|
|
||||||
</MenubarShortcut>
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem onClick={redo} disabled={!hasRedo}>
|
|
||||||
{t('menu.edit.redo')}
|
|
||||||
<MenubarShortcut>
|
|
||||||
{
|
|
||||||
keyboardShortcutsForOS[
|
|
||||||
KeyboardShortcutAction.REDO
|
|
||||||
].keyCombinationLabel
|
|
||||||
}
|
|
||||||
</MenubarShortcut>
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarSeparator />
|
|
||||||
<MenubarItem
|
|
||||||
onClick={() =>
|
|
||||||
showAlert({
|
|
||||||
title: t('clear_diagram_alert.title'),
|
|
||||||
description: t(
|
|
||||||
'clear_diagram_alert.description'
|
|
||||||
),
|
|
||||||
actionLabel: t(
|
|
||||||
'clear_diagram_alert.clear'
|
|
||||||
),
|
|
||||||
closeLabel: t(
|
|
||||||
'clear_diagram_alert.cancel'
|
|
||||||
),
|
|
||||||
onAction: clearDiagramData,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t('menu.edit.clear')}
|
|
||||||
</MenubarItem>
|
|
||||||
</MenubarContent>
|
|
||||||
</MenubarMenu>
|
|
||||||
<MenubarMenu>
|
|
||||||
<MenubarTrigger>{t('menu.view.view')}</MenubarTrigger>
|
|
||||||
<MenubarContent>
|
|
||||||
<MenubarItem onClick={showOrHideSidePanel}>
|
|
||||||
{isSidePanelShowed
|
|
||||||
? t('menu.view.hide_sidebar')
|
|
||||||
: t('menu.view.show_sidebar')}
|
|
||||||
<MenubarShortcut>
|
|
||||||
{
|
|
||||||
keyboardShortcutsForOS[
|
|
||||||
KeyboardShortcutAction
|
|
||||||
.TOGGLE_SIDE_PANEL
|
|
||||||
].keyCombinationLabel
|
|
||||||
}
|
|
||||||
</MenubarShortcut>
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarSeparator />
|
|
||||||
<MenubarItem onClick={showOrHideCardinality}>
|
|
||||||
{showCardinality
|
|
||||||
? t('menu.view.hide_cardinality')
|
|
||||||
: t('menu.view.show_cardinality')}
|
|
||||||
</MenubarItem>
|
|
||||||
{databaseType !== DatabaseType.CLICKHOUSE ? (
|
|
||||||
<MenubarItem onClick={showOrHideDependencies}>
|
|
||||||
{showDependenciesOnCanvas
|
|
||||||
? t('menu.view.hide_dependencies')
|
|
||||||
: t('menu.view.show_dependencies')}
|
|
||||||
</MenubarItem>
|
|
||||||
) : null}
|
|
||||||
<MenubarSeparator />
|
|
||||||
<MenubarSub>
|
|
||||||
<MenubarSubTrigger>
|
|
||||||
{t('menu.view.zoom_on_scroll')}
|
|
||||||
</MenubarSubTrigger>
|
|
||||||
<MenubarSubContent>
|
|
||||||
<MenubarCheckboxItem
|
|
||||||
checked={scrollAction === 'zoom'}
|
|
||||||
onClick={() => setScrollAction('zoom')}
|
|
||||||
>
|
|
||||||
{t('zoom.on')}
|
|
||||||
</MenubarCheckboxItem>
|
|
||||||
<MenubarCheckboxItem
|
|
||||||
checked={scrollAction === 'pan'}
|
|
||||||
onClick={() => setScrollAction('pan')}
|
|
||||||
>
|
|
||||||
{t('zoom.off')}
|
|
||||||
</MenubarCheckboxItem>
|
|
||||||
</MenubarSubContent>
|
|
||||||
</MenubarSub>
|
|
||||||
<MenubarSeparator />
|
|
||||||
<MenubarSub>
|
|
||||||
<MenubarSubTrigger>
|
|
||||||
{t('menu.view.theme')}
|
|
||||||
</MenubarSubTrigger>
|
|
||||||
<MenubarSubContent>
|
|
||||||
<MenubarCheckboxItem
|
|
||||||
checked={theme === 'system'}
|
|
||||||
onClick={() => setTheme('system')}
|
|
||||||
>
|
|
||||||
{t('theme.system')}
|
|
||||||
</MenubarCheckboxItem>
|
|
||||||
<MenubarCheckboxItem
|
|
||||||
checked={theme === 'light'}
|
|
||||||
onClick={() => setTheme('light')}
|
|
||||||
>
|
|
||||||
{t('theme.light')}
|
|
||||||
</MenubarCheckboxItem>
|
|
||||||
<MenubarCheckboxItem
|
|
||||||
checked={theme === 'dark'}
|
|
||||||
onClick={() => setTheme('dark')}
|
|
||||||
>
|
|
||||||
{t('theme.dark')}
|
|
||||||
</MenubarCheckboxItem>
|
|
||||||
</MenubarSubContent>
|
|
||||||
</MenubarSub>
|
|
||||||
</MenubarContent>
|
|
||||||
</MenubarMenu>
|
|
||||||
|
|
||||||
<MenubarMenu>
|
|
||||||
<MenubarTrigger>{t('menu.share.share')}</MenubarTrigger>
|
|
||||||
<MenubarContent>
|
|
||||||
<MenubarItem onClick={openExportDiagramDialog}>
|
|
||||||
{t('menu.share.export_diagram')}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem onClick={openImportDiagramDialog}>
|
|
||||||
{t('menu.share.import_diagram')}
|
|
||||||
</MenubarItem>
|
|
||||||
</MenubarContent>
|
|
||||||
</MenubarMenu>
|
|
||||||
|
|
||||||
<MenubarMenu>
|
|
||||||
<MenubarTrigger>{t('menu.help.help')}</MenubarTrigger>
|
|
||||||
<MenubarContent>
|
|
||||||
<MenubarItem onClick={openChartDBIO}>
|
|
||||||
{t('menu.help.visit_website')}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem onClick={openJoinDiscord}>
|
|
||||||
{t('menu.help.join_discord')}
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarItem onClick={openCalendly}>
|
|
||||||
{t('menu.help.schedule_a_call')}
|
|
||||||
</MenubarItem>
|
|
||||||
</MenubarContent>
|
|
||||||
</MenubarMenu>
|
|
||||||
</Menubar>
|
|
||||||
</div>
|
</div>
|
||||||
{isDesktop ? (
|
{isDesktop ? (
|
||||||
<>
|
<>
|
||||||
<DiagramName />
|
<DiagramName />
|
||||||
<div className="hidden flex-1 items-center justify-end gap-2 sm:flex">
|
<div className="hidden flex-1 items-center justify-end gap-2 sm:flex">
|
||||||
|
{renderGetBuckleButton()}
|
||||||
<LastSaved />
|
<LastSaved />
|
||||||
{renderStars()}
|
{renderStars()}
|
||||||
<LanguageNav />
|
<LanguageNav />
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const examples: Example[] = [
|
|||||||
{
|
{
|
||||||
id: 'gaj3scrtaz46ezfmc162ingxf',
|
id: 'gaj3scrtaz46ezfmc162ingxf',
|
||||||
name: 'dept_no',
|
name: 'dept_no',
|
||||||
type: { id: 'char', name: 'chat' },
|
type: { id: 'char', name: 'char' },
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
@@ -63,7 +63,7 @@ export const examples: Example[] = [
|
|||||||
indexes: [
|
indexes: [
|
||||||
{
|
{
|
||||||
id: '87iu197demih0wymjooqm9dmh',
|
id: '87iu197demih0wymjooqm9dmh',
|
||||||
name: 'PRIMARY',
|
name: 'dept_no',
|
||||||
unique: true,
|
unique: true,
|
||||||
fieldIds: ['gaj3scrtaz46ezfmc162ingxf'],
|
fieldIds: ['gaj3scrtaz46ezfmc162ingxf'],
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
@@ -99,7 +99,7 @@ export const examples: Example[] = [
|
|||||||
{
|
{
|
||||||
id: 'jdw1yrh9xf1i7927gzs9pob2p',
|
id: 'jdw1yrh9xf1i7927gzs9pob2p',
|
||||||
name: 'dept_no',
|
name: 'dept_no',
|
||||||
type: { id: 'char', name: 'chat' },
|
type: { id: 'char', name: 'char' },
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
@@ -129,14 +129,14 @@ export const examples: Example[] = [
|
|||||||
indexes: [
|
indexes: [
|
||||||
{
|
{
|
||||||
id: 'rqb91465yc51xpvd54o5a8d0l',
|
id: 'rqb91465yc51xpvd54o5a8d0l',
|
||||||
name: 'PRIMARY',
|
name: 'emp_no',
|
||||||
unique: true,
|
unique: true,
|
||||||
fieldIds: ['wcgycjif09xrq0ly3txkq6ocu'],
|
fieldIds: ['wcgycjif09xrq0ly3txkq6ocu'],
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '8wh6op49abv143qdfjzm211xj',
|
id: '8wh6op49abv143qdfjzm211xj',
|
||||||
name: 'PRIMARY',
|
name: 'dept_no',
|
||||||
unique: true,
|
unique: true,
|
||||||
fieldIds: ['jdw1yrh9xf1i7927gzs9pob2p'],
|
fieldIds: ['jdw1yrh9xf1i7927gzs9pob2p'],
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
@@ -172,7 +172,7 @@ export const examples: Example[] = [
|
|||||||
{
|
{
|
||||||
id: 'v8plj7wq1cly03y178bysft2f',
|
id: 'v8plj7wq1cly03y178bysft2f',
|
||||||
name: 'dept_no',
|
name: 'dept_no',
|
||||||
type: { id: 'char', name: 'chat' },
|
type: { id: 'char', name: 'char' },
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
@@ -202,14 +202,14 @@ export const examples: Example[] = [
|
|||||||
indexes: [
|
indexes: [
|
||||||
{
|
{
|
||||||
id: 'cbahnbrxaaj7cg29act50izy4',
|
id: 'cbahnbrxaaj7cg29act50izy4',
|
||||||
name: 'PRIMARY',
|
name: 'emp_no',
|
||||||
unique: true,
|
unique: true,
|
||||||
fieldIds: ['ecx2zbzdc5o54e04aeg7tlg54'],
|
fieldIds: ['ecx2zbzdc5o54e04aeg7tlg54'],
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'vgxv8rkf4890yf659o2oklffv',
|
id: 'vgxv8rkf4890yf659o2oklffv',
|
||||||
name: 'PRIMARY',
|
name: 'dept_no',
|
||||||
unique: true,
|
unique: true,
|
||||||
fieldIds: ['v8plj7wq1cly03y178bysft2f'],
|
fieldIds: ['v8plj7wq1cly03y178bysft2f'],
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
@@ -297,7 +297,7 @@ export const examples: Example[] = [
|
|||||||
indexes: [
|
indexes: [
|
||||||
{
|
{
|
||||||
id: '8zg1ccoj4jb4kv6eleih38ni5',
|
id: '8zg1ccoj4jb4kv6eleih38ni5',
|
||||||
name: 'PRIMARY',
|
name: 'emp_no',
|
||||||
unique: true,
|
unique: true,
|
||||||
fieldIds: ['04csyx8ds9t3rh93txiqs4dm4'],
|
fieldIds: ['04csyx8ds9t3rh93txiqs4dm4'],
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
@@ -366,14 +366,14 @@ export const examples: Example[] = [
|
|||||||
indexes: [
|
indexes: [
|
||||||
{
|
{
|
||||||
id: 'nky2wepp8yr5g6rzvnbta1hxb',
|
id: 'nky2wepp8yr5g6rzvnbta1hxb',
|
||||||
name: 'PRIMARY',
|
name: 'emp_no',
|
||||||
unique: true,
|
unique: true,
|
||||||
fieldIds: ['b8c9v5vtpbnt5tjzcd3iat85f'],
|
fieldIds: ['b8c9v5vtpbnt5tjzcd3iat85f'],
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'w40nnsrsnlz7z7vycs4yf0s8d',
|
id: 'w40nnsrsnlz7z7vycs4yf0s8d',
|
||||||
name: 'PRIMARY',
|
name: 'from_date',
|
||||||
unique: true,
|
unique: true,
|
||||||
fieldIds: ['0s10erufqpl6y3hpqmvbcneol'],
|
fieldIds: ['0s10erufqpl6y3hpqmvbcneol'],
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
@@ -433,21 +433,21 @@ export const examples: Example[] = [
|
|||||||
indexes: [
|
indexes: [
|
||||||
{
|
{
|
||||||
id: 'ijhmb7tq6i4fd72ndvotnwo45',
|
id: 'ijhmb7tq6i4fd72ndvotnwo45',
|
||||||
name: 'PRIMARY',
|
name: 'emp_no',
|
||||||
unique: true,
|
unique: true,
|
||||||
fieldIds: ['hr2gdoc0wtwvs4pfqo6m0fwc3'],
|
fieldIds: ['hr2gdoc0wtwvs4pfqo6m0fwc3'],
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'wgneqfte0nq7d5vzed2hcqie6',
|
id: 'wgneqfte0nq7d5vzed2hcqie6',
|
||||||
name: 'PRIMARY',
|
name: 'title',
|
||||||
unique: true,
|
unique: true,
|
||||||
fieldIds: ['5evr59tury66sayiu59esoc61'],
|
fieldIds: ['5evr59tury66sayiu59esoc61'],
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'jbe9t9adhluqy8d3i7w1vgygd',
|
id: 'jbe9t9adhluqy8d3i7w1vgygd',
|
||||||
name: 'PRIMARY',
|
name: 'from_date',
|
||||||
unique: true,
|
unique: true,
|
||||||
fieldIds: ['0vs1nqvrb6t53niz5ns2eskre'],
|
fieldIds: ['0vs1nqvrb6t53niz5ns2eskre'],
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
@@ -476,7 +476,7 @@ export const examples: Example[] = [
|
|||||||
{
|
{
|
||||||
id: 'fv7o6txqvmy2349aq3pg0hnkm',
|
id: 'fv7o6txqvmy2349aq3pg0hnkm',
|
||||||
name: 'dept_no',
|
name: 'dept_no',
|
||||||
type: { id: 'char', name: 'chat' },
|
type: { id: 'char', name: 'char' },
|
||||||
primaryKey: false,
|
primaryKey: false,
|
||||||
unique: false,
|
unique: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
@@ -2465,7 +2465,7 @@ export const examples: Example[] = [
|
|||||||
{
|
{
|
||||||
id: 'lng1oxspuc1hsny8x0sti0o72',
|
id: 'lng1oxspuc1hsny8x0sti0o72',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
type: { id: 'char', name: 'chat' },
|
type: { id: 'char', name: 'char' },
|
||||||
primaryKey: false,
|
primaryKey: false,
|
||||||
unique: false,
|
unique: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const employeeDb: Template = {
|
|||||||
{
|
{
|
||||||
id: 'gaj3scrtaz46ezfmc162ingxf',
|
id: 'gaj3scrtaz46ezfmc162ingxf',
|
||||||
name: 'dept_no',
|
name: 'dept_no',
|
||||||
type: { id: 'char', name: 'chat' },
|
type: { id: 'char', name: 'char' },
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
@@ -88,7 +88,7 @@ export const employeeDb: Template = {
|
|||||||
{
|
{
|
||||||
id: 'jdw1yrh9xf1i7927gzs9pob2p',
|
id: 'jdw1yrh9xf1i7927gzs9pob2p',
|
||||||
name: 'dept_no',
|
name: 'dept_no',
|
||||||
type: { id: 'char', name: 'chat' },
|
type: { id: 'char', name: 'char' },
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
@@ -161,7 +161,7 @@ export const employeeDb: Template = {
|
|||||||
{
|
{
|
||||||
id: 'v8plj7wq1cly03y178bysft2f',
|
id: 'v8plj7wq1cly03y178bysft2f',
|
||||||
name: 'dept_no',
|
name: 'dept_no',
|
||||||
type: { id: 'char', name: 'chat' },
|
type: { id: 'char', name: 'char' },
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
@@ -465,7 +465,7 @@ export const employeeDb: Template = {
|
|||||||
{
|
{
|
||||||
id: 'fv7o6txqvmy2349aq3pg0hnkm',
|
id: 'fv7o6txqvmy2349aq3pg0hnkm',
|
||||||
name: 'dept_no',
|
name: 'dept_no',
|
||||||
type: { id: 'char', name: 'chat' },
|
type: { id: 'char', name: 'char' },
|
||||||
primaryKey: false,
|
primaryKey: false,
|
||||||
unique: false,
|
unique: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const visualNovelDb: Template = {
|
|||||||
{
|
{
|
||||||
id: 'gaj3scrtaz46ezfmc162ingxf',
|
id: 'gaj3scrtaz46ezfmc162ingxf',
|
||||||
name: 'dept_no',
|
name: 'dept_no',
|
||||||
type: { id: 'char', name: 'chat' },
|
type: { id: 'char', name: 'char' },
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
@@ -89,7 +89,7 @@ export const visualNovelDb: Template = {
|
|||||||
{
|
{
|
||||||
id: 'jdw1yrh9xf1i7927gzs9pob2p',
|
id: 'jdw1yrh9xf1i7927gzs9pob2p',
|
||||||
name: 'dept_no',
|
name: 'dept_no',
|
||||||
type: { id: 'char', name: 'chat' },
|
type: { id: 'char', name: 'char' },
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
@@ -162,7 +162,7 @@ export const visualNovelDb: Template = {
|
|||||||
{
|
{
|
||||||
id: 'v8plj7wq1cly03y178bysft2f',
|
id: 'v8plj7wq1cly03y178bysft2f',
|
||||||
name: 'dept_no',
|
name: 'dept_no',
|
||||||
type: { id: 'char', name: 'chat' },
|
type: { id: 'char', name: 'char' },
|
||||||
primaryKey: true,
|
primaryKey: true,
|
||||||
unique: true,
|
unique: true,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
@@ -466,7 +466,7 @@ export const visualNovelDb: Template = {
|
|||||||
{
|
{
|
||||||
id: 'fv7o6txqvmy2349aq3pg0hnkm',
|
id: 'fv7o6txqvmy2349aq3pg0hnkm',
|
||||||
name: 'dept_no',
|
name: 'dept_no',
|
||||||
type: { id: 'char', name: 'chat' },
|
type: { id: 'char', name: 'char' },
|
||||||
primaryKey: false,
|
primaryKey: false,
|
||||||
unique: false,
|
unique: false,
|
||||||
nullable: false,
|
nullable: false,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const defaultTheme = require('tailwindcss/defaultTheme');
|
import defaultTheme from 'tailwindcss/defaultTheme';
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
Reference in New Issue
Block a user