Compare commits

...

21 Commits

Author SHA1 Message Date
Guy Ben-Aharon
42c159605d chore(main): release 1.1.0 (#364) 2024-11-13 15:26:17 +02:00
Guy Ben-Aharon
78c427f38e fix(templates): fix issue with double-clone on localhost (#394) 2024-11-13 15:22:00 +02:00
Jonathan Fishner
bae74d1693 feat(add templates) add five more templates (gravity, koel.dev, laravel-permission, laravel-spark, voyager) (#392) 2024-11-13 13:24:01 +02:00
Ian Cheng
123f40f39e fix(i18n): added traditional Chinese language translation (#356)
* feat: added traditional chinese language translation

* feat: added traditional chinese language translation

---------

Co-authored-by: Jonathan Fishner <jonathanfishner11@gmail.com>
2024-11-12 17:26:42 +02:00
ntoniazzi
e3129cec74 fix(i18n): french translation update - share menu (#391) 2024-11-12 17:22:40 +02:00
Eva
5508c1e084 fix(i18n): Fixed part of RU lang introduced in #365 feat(share) (#380)
Co-authored-by: Jonathan Fishner <jonathanfishner11@gmail.com>
2024-11-12 13:43:36 +02:00
lkjhxx
9f2893319a fix(i18n): Add simplified chinese (#385)
* feat: add Simplified Chinese

* feat: add Simplified Chinese

* fix linter Update zh_CN.ts

---------

Co-authored-by: Jonathan Fishner <jonathanfishner11@gmail.com>
2024-11-12 13:23:57 +02:00
Guy Ben-Aharon
125a39fb5b fix(sql export): make loading for export interactive (#388) 2024-11-12 12:37:56 +02:00
Guy Ben-Aharon
4ca1832732 fix(bundle): fix bundle size (#382) 2024-11-11 01:13:44 +02:00
Guy Ben-Aharon
3609bfea4d fix(share): add loader to the export (#381)
* fix(share): add loader to the export

* fix(share): add loader to the export
2024-11-10 23:33:16 +02:00
Guy Ben-Aharon
94a5d84fae feat(share): add sharing capabilities to import and export diagrams (#365)
* feat(share): add sharing capabilities to import and export diagrams

* remove use client

* fix build

* add error parse indication

* add import from initial dialog

* fix build
2024-11-10 16:30:15 +02:00
Jonathan Fishner
85e691fcbe fix for tempalte name novel database (#379) 2024-11-10 09:02:51 +02:00
Daniel Cruz
709ccff8fa Adds missing spanish translations (#372)
fix(translations): Add missing Spanish translations
2024-11-09 20:06:10 +02:00
Elton Costa
6c7eb4609d feat(canvas): Added Snap to grid functionality. Toggle/hold shift to enable snap to grid. (#373)
* asd

* add translations & useKeyPress

* fix build

* fix build

---------

Co-authored-by: Guy Ben-Aharon <guybenah@gmail.com>
Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2024-11-09 19:55:40 +02:00
Eva
2c69b08eae fix(i18n): Added Russian language (#376)
* (i18n): Added Russian language.

* Update src/i18n/locales/ru.ts

Co-authored-by: Eva <29357907+nikelborm@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Eva <29357907+nikelborm@users.noreply.github.com>

* Refined and added missing fields to RU translation

* Refined and added missing fields to RU translation

---------

Co-authored-by: Aditya Kale <kaleaditya779@gmail.com>
Co-authored-by: Jonathan Fishner <jonathanfishner11@gmail.com>
2024-11-09 19:01:36 +02:00
Favor
84e7591d05 fix: improve title name edit interaction (#367)
* chore(main): improve table name edit interaction

* chore: fix lint issues

* feat(i18n): add double-click functionality and tooltip translations

- Change editable titles action to `onDoubleClick`
- Add tooltip translations for table name editing feature
- Support for 9 languages: Russian, Japanese, Hindi, French, Spanish,
  German, Ukrainian, Portuguese, and Korean
- Improve UX by indicating double-click edit functionality across languages

* naming + some padding

---------

Co-authored-by: Guy Ben-Aharon <guybenah@gmail.com>
2024-11-09 15:41:22 +02:00
orig
545e8578c9 fix(dockerfile): support openai key in docker build (#366) 2024-11-09 14:19:02 +02:00
Jonathan Fishner
f1d073d053 fix(templates): change the template url to be database instead of db (#374)
* fix(templates): change the template url to be database instead of db

* add tag

* layout fixes

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2024-11-09 14:02:29 +02:00
Jonathan Fishner
20b3396ec2 feat(add templates): add five more templates (laravel, django, twitter… (#371)
* feat(add templates): add five more templates (laravel, django, twitter, adonis-acl, akaunting)

* fix build

* fix tags

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2024-11-09 03:38:03 +02:00
☁️dungsil
b305be82ae fix(i18n): add korean (#362)
Co-authored-by: Jonathan Fishner <jonathanfishner11@gmail.com>
2024-11-07 16:27:24 +02:00
Jonathan Fishner
1430d2c236 fix(import json): for Check Script Result, default with quotes (#358) 2024-11-07 16:18:56 +02:00
84 changed files with 14124 additions and 727 deletions

View File

@@ -1,5 +1,32 @@
# Changelog
## [1.1.0](https://github.com/chartdb/chartdb/compare/v1.0.1...v1.1.0) (2024-11-13)
### Features
* **add templates:** add five more templates (laravel, django, twitter… ([#371](https://github.com/chartdb/chartdb/issues/371)) ([20b3396](https://github.com/chartdb/chartdb/commit/20b3396ec2afff09ca8bcdd91f5c6284c93cd959))
* **canvas:** Added Snap to grid functionality. Toggle/hold shift to enable snap to grid. ([#373](https://github.com/chartdb/chartdb/issues/373)) ([6c7eb46](https://github.com/chartdb/chartdb/commit/6c7eb4609d8466278de30317665929ec529c1f94))
* **share:** add sharing capabilities to import and export diagrams ([#365](https://github.com/chartdb/chartdb/issues/365)) ([94a5d84](https://github.com/chartdb/chartdb/commit/94a5d84fae819b0de6c1e471d1aad16dc8f39dd6))
### Bug Fixes
* **bundle:** fix bundle size ([#382](https://github.com/chartdb/chartdb/issues/382)) ([4ca1832](https://github.com/chartdb/chartdb/commit/4ca18327324106950f0d1af851b9b74379b67b7b))
* **dockerfile:** support openai key in docker build ([#366](https://github.com/chartdb/chartdb/issues/366)) ([545e857](https://github.com/chartdb/chartdb/commit/545e8578c9e8aa71696f6aa8bec81cacaa602c2d))
* **i18n:** add korean ([#362](https://github.com/chartdb/chartdb/issues/362)) ([b305be8](https://github.com/chartdb/chartdb/commit/b305be82aee00994ef576ca6fd62d72dd491f771))
* **i18n:** Add simplified chinese ([#385](https://github.com/chartdb/chartdb/issues/385)) ([9f28933](https://github.com/chartdb/chartdb/commit/9f2893319a1a2aed9a7c03d15e25a17ab37c2465))
* **i18n:** Added Russian language ([#376](https://github.com/chartdb/chartdb/issues/376)) ([2c69b08](https://github.com/chartdb/chartdb/commit/2c69b08eaea6b86ce0c1ddb18a23e22629198bf5))
* **i18n:** added traditional Chinese language translation ([#356](https://github.com/chartdb/chartdb/issues/356)) ([123f40f](https://github.com/chartdb/chartdb/commit/123f40f39e703ad612635964af530ac72c387d3c))
* **i18n:** Fixed part of RU lang introduced in [#365](https://github.com/chartdb/chartdb/issues/365) feat(share) ([#380](https://github.com/chartdb/chartdb/issues/380)) ([5508c1e](https://github.com/chartdb/chartdb/commit/5508c1e084e0ee24d1a54f721f760b9fc14df107))
* **i18n:** french translation update - share menu ([#391](https://github.com/chartdb/chartdb/issues/391)) ([e3129ce](https://github.com/chartdb/chartdb/commit/e3129cec744d18f09953544d9e74cd5adc4e8afb))
* **import json:** for Check Script Result, default with quotes ([#358](https://github.com/chartdb/chartdb/issues/358)) ([1430d2c](https://github.com/chartdb/chartdb/commit/1430d2c2365b7b74e36b8ff9d32a163d7437448a))
* improve title name edit interaction ([#367](https://github.com/chartdb/chartdb/issues/367)) ([84e7591](https://github.com/chartdb/chartdb/commit/84e7591d0586b9a457f31737c6e363ef41574142))
* **share:** add loader to the export ([#381](https://github.com/chartdb/chartdb/issues/381)) ([3609bfe](https://github.com/chartdb/chartdb/commit/3609bfea4d4c78b03711ff8d721b4e67bf82185a))
* **sql export:** make loading for export interactive ([#388](https://github.com/chartdb/chartdb/issues/388)) ([125a39f](https://github.com/chartdb/chartdb/commit/125a39fb5be803f0e6db0b68fb5bc8e290fa8dae))
* **templates:** change the template url to be database instead of db ([#374](https://github.com/chartdb/chartdb/issues/374)) ([f1d073d](https://github.com/chartdb/chartdb/commit/f1d073d05383955da6f60a9a66ed2be879b103e4))
* **templates:** fix issue with double-clone on localhost ([#394](https://github.com/chartdb/chartdb/issues/394)) ([78c427f](https://github.com/chartdb/chartdb/commit/78c427f38e5c64fc340d13ceb2153c2b85db437e))
## [1.0.1](https://github.com/chartdb/chartdb/compare/v1.0.0...v1.0.1) (2024-11-06)

View File

@@ -1,5 +1,7 @@
FROM node:22-alpine AS builder
ARG VITE_OPENAI_API_KEY
WORKDIR /usr/src/app
COPY package.json package-lock.json ./

View File

@@ -97,7 +97,7 @@ VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> npm run build
### Running the Docker Container
```bash
docker build -t chartdb .
docker build -t chartdb . (If you want AI capabilities, use `docker build --build-arg VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -t chartdb .`)
docker run -p 8080:80 chartdb
```

8
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "chartdb",
"version": "1.0.1",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chartdb",
"version": "1.0.1",
"version": "1.1.0",
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dnd-kit/sortable": "^8.0.0",
@@ -60,7 +60,8 @@
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"timeago-react": "^3.0.6",
"vaul": "^0.9.1"
"vaul": "^0.9.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22.1.0",
@@ -10539,7 +10540,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.0.1",
"version": "1.1.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -64,7 +64,8 @@
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"timeago-react": "^3.0.6",
"vaul": "^0.9.1"
"vaul": "^0.9.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^22.1.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

View File

@@ -0,0 +1,62 @@
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
const alertVariants = cva(
'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7',
{
variants: {
variant: {
default: 'bg-background text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: 'default',
},
}
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = 'Alert';
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn(
'mb-1 font-medium leading-none tracking-tight',
className
)}
{...props}
/>
));
AlertTitle.displayName = 'AlertTitle';
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('text-sm [&_p]:leading-relaxed', className)}
{...props}
/>
));
AlertDescription.displayName = 'AlertDescription';
export { Alert, AlertTitle, AlertDescription };

View File

@@ -16,6 +16,8 @@ export interface CodeSnippetProps {
code: string;
language?: 'sql' | 'shell';
loading?: boolean;
autoScroll?: boolean;
isComplete?: boolean;
}
export const Editor = lazy(() =>
@@ -25,7 +27,14 @@ export const Editor = lazy(() =>
);
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
({ className, code, loading, language = 'sql' }) => {
({
className,
code,
loading,
language = 'sql',
autoScroll = false,
isComplete = true,
}) => {
const { t } = useTranslation();
const monaco = useMonaco();
const { effectiveTheme } = useTheme();
@@ -47,6 +56,16 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
}, 1500);
}, [isCopied]);
useEffect(() => {
if (monaco) {
const editor = monaco.editor.getModels()[0];
if (editor && autoScroll) {
const lineCount = editor.getLineCount();
monaco.editor.getEditors()[0]?.revealLine(lineCount);
}
}
}, [code, monaco, autoScroll]);
const copyToClipboard = useCallback(() => {
navigator.clipboard.writeText(code);
setIsCopied(true);
@@ -63,32 +82,38 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
<Spinner />
) : (
<Suspense fallback={<Spinner />}>
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<TooltipTrigger
asChild
className="absolute right-1 top-1 z-10"
{isComplete ? (
<Tooltip
onOpenChange={setTooltipOpen}
open={isCopied || tooltipOpen}
>
<span>
<Button
className=" h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t(isCopied ? 'copied' : 'copy_to_clipboard')}
</TooltipContent>
</Tooltip>
<TooltipTrigger
asChild
className="absolute right-1 top-1 z-10"
>
<span>
<Button
className=" h-fit p-1.5"
variant="outline"
onClick={copyToClipboard}
>
{isCopied ? (
<CopyCheck size={16} />
) : (
<Copy size={16} />
)}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t(
isCopied
? 'copied'
: 'copy_to_clipboard'
)}
</TooltipContent>
</Tooltip>
) : null}
<Editor
value={code}
@@ -118,6 +143,9 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
contextmenu: false,
}}
/>
{!isComplete ? (
<div className="absolute bottom-2 right-2 size-2 animate-blink rounded-full bg-pink-600" />
) : null}
</Suspense>
)}
</div>

View File

@@ -0,0 +1,168 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Upload, FileIcon, AlertCircle, Trash2 } from 'lucide-react';
import { Button } from '../button/button';
interface FileWithPreview extends File {
preview?: string;
}
export interface FileUploaderProps {
onFilesChange?: (files: File[]) => void;
multiple?: boolean;
supportedExtensions?: string[];
}
export const FileUploader: React.FC<FileUploaderProps> = ({
onFilesChange,
multiple,
supportedExtensions,
}) => {
const [files, setFiles] = useState<FileWithPreview[]>([]);
const [isDragging, setIsDragging] = useState(false);
const [error, setError] = useState<string | null>(null);
const isFileSupported = useCallback(
(file: File) => {
if (!supportedExtensions) return true;
const fileExtension = file.name.split('.').pop()?.toLowerCase();
return fileExtension
? supportedExtensions.includes(`.${fileExtension}`)
: false;
},
[supportedExtensions]
);
const handleFiles = useCallback(
(selectedFiles: FileList) => {
const newFiles = Array.from(selectedFiles)
.filter((file) => {
if (!isFileSupported(file)) {
setError(
`File type not supported. Supported types: ${supportedExtensions?.join(', ')}`
);
return false;
}
return true;
})
.map((file) =>
Object.assign(file, { preview: URL.createObjectURL(file) })
);
if (newFiles.length === 0) return;
setError(null);
setFiles((prevFiles) => {
if (multiple) {
return [...prevFiles, ...newFiles];
} else {
return newFiles.slice(0, 1);
}
});
},
[multiple, supportedExtensions, isFileSupported]
);
const onDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(true);
}, []);
const onDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(false);
}, []);
const onDrop = useCallback(
(e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDragging(false);
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
handleFiles(e.dataTransfer.files);
}
},
[handleFiles]
);
useEffect(() => {
if (onFilesChange) {
onFilesChange(files.length > 0 ? files : []);
}
}, [files, onFilesChange]);
const removeFile = useCallback((fileToRemove: File) => {
setFiles((prevFiles) =>
prevFiles.filter((file) => file !== fileToRemove)
);
}, []);
return (
<div className="mx-auto w-full max-w-md">
<div
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
className={`cursor-pointer rounded-lg border-2 border-dashed p-8 text-center transition-colors ${
isDragging
? 'border-primary bg-primary/10 dark:bg-primary/20'
: 'border-gray-300 hover:border-primary dark:border-gray-600 dark:hover:border-primary'
}`}
>
<input
type="file"
multiple={multiple}
onChange={(e) =>
e.target.files && handleFiles(e.target.files)
}
className="hidden"
id="fileInput"
accept={supportedExtensions?.join(',')}
/>
<label htmlFor="fileInput" className="cursor-pointer">
<Upload className="mx-auto size-12 text-gray-400 dark:text-gray-500" />
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
{multiple
? 'Drag and drop files here or click to select'
: 'Drag and drop a file here or click to select'}
</p>
{supportedExtensions ? (
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
Supported types: {supportedExtensions.join(', ')}
</p>
) : null}
</label>
</div>
{error ? (
<div className="mt-4 flex items-center rounded-lg bg-red-100 p-3 text-red-700 dark:bg-red-900 dark:text-red-200">
<AlertCircle className="mr-2 size-5" />
<span className="text-sm">{error}</span>
</div>
) : null}
{files.length > 0 ? (
<ul className="mt-4 space-y-4">
{files.map((file) => (
<li
key={file.name}
className="flex items-center justify-between rounded-lg bg-gray-100 p-3 dark:bg-gray-800"
>
<div className="flex min-w-0 flex-1 items-center space-x-2">
<FileIcon className="size-5 text-primary" />
<span className="truncate text-sm font-medium text-gray-700 dark:text-gray-300">
{file.name}
</span>
</div>
<Button
variant="ghost"
className="size-5 p-0 hover:bg-primary-foreground"
onClick={() => removeFile(file)}
>
<Trash2 className="size-3.5 text-red-700" />
</Button>
</li>
))}
</ul>
) : null}
</div>
);
};

View File

@@ -34,7 +34,7 @@ export const ListMenu = React.forwardRef<HTMLDivElement, ListMenuProps>(
strokeWidth={item.selected ? 2.4 : 2}
/>
) : null}
{item.title}
<span className="min-w-0 truncate">{item.title}</span>
</Link>
))}
</div>

View File

@@ -5,6 +5,8 @@ import type { TableSchemaDialogProps } from '@/dialogs/table-schema-dialog/table
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog';
import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
export interface DialogContext {
// Create diagram dialog
@@ -48,6 +50,18 @@ export interface DialogContext {
params: Omit<ExportImageDialogProps, 'dialog'>
) => void;
closeExportImageDialog: () => void;
// Export diagram dialog
openExportDiagramDialog: (
params: Omit<ExportDiagramDialogProps, 'dialog'>
) => void;
closeExportDiagramDialog: () => void;
// Import diagram dialog
openImportDiagramDialog: (
params: Omit<ImportDiagramDialogProps, 'dialog'>
) => void;
closeImportDiagramDialog: () => void;
}
export const dialogContext = createContext<DialogContext>({
@@ -69,4 +83,8 @@ export const dialogContext = createContext<DialogContext>({
closeStarUsDialog: emptyFn,
openExportImageDialog: emptyFn,
closeExportImageDialog: emptyFn,
openExportDiagramDialog: emptyFn,
closeExportDiagramDialog: emptyFn,
openImportDiagramDialog: emptyFn,
closeImportDiagramDialog: emptyFn,
});

View File

@@ -17,6 +17,8 @@ import { emptyFn } from '@/lib/utils';
import { StarUsDialog } from '@/dialogs/star-us-dialog/star-us-dialog';
import type { ExportImageDialogProps } 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 { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
children,
@@ -86,6 +88,14 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
[setOpenTableSchemaDialog]
);
// Export image dialog
const [openExportDiagramDialog, setOpenExportDiagramDialog] =
useState(false);
// Import diagram dialog
const [openImportDiagramDialog, setOpenImportDiagramDialog] =
useState(false);
// Alert dialog
const [showAlert, setShowAlert] = useState(false);
const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({
@@ -126,6 +136,12 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
closeStarUsDialog: () => setOpenStarUsDialog(false),
closeExportImageDialog: () => setOpenExportImageDialog(false),
openExportImageDialog: openExportImageDialogHandler,
openExportDiagramDialog: () => setOpenExportDiagramDialog(true),
closeExportDiagramDialog: () =>
setOpenExportDiagramDialog(false),
openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
closeImportDiagramDialog: () =>
setOpenImportDiagramDialog(false),
}}
>
{children}
@@ -152,6 +168,8 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
dialog={{ open: openExportImageDialog }}
{...exportImageDialogParams}
/>
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
</dialogContext.Provider>
);
};

View File

@@ -10,6 +10,7 @@ import {
import { DatabaseType } from '@/lib/domain/database-type';
import { useTranslation } from 'react-i18next';
import { SelectDatabaseContent } from './select-database-content';
import { useDialog } from '@/hooks/use-dialog';
export interface SelectDatabaseProps {
onContinue: () => void;
@@ -27,6 +28,7 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({
createNewDiagram,
}) => {
const { t } = useTranslation();
const { openImportDiagramDialog } = useDialog();
return (
<>
@@ -51,7 +53,13 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({
</Button>
</DialogClose>
) : (
<div></div>
<Button
type="button"
variant="ghost"
onClick={openImportDiagramDialog}
>
{t('new_diagram_dialog.import_from_file')}
</Button>
)}
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end sm:space-x-2">
<Button

View File

@@ -0,0 +1,110 @@
import React, { useCallback, useEffect, useMemo, useState } 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 { SelectBoxOption } from '@/components/select-box/select-box';
import { SelectBox } from '@/components/select-box/select-box';
import type { BaseDialogProps } from '../common/base-dialog-props';
import { useTranslation } from 'react-i18next';
import { useChartDB } from '@/hooks/use-chartdb';
import { diagramToJSONOutput } from '@/lib/export-import-utils';
import { Spinner } from '@/components/spinner/spinner';
import { waitFor } from '@/lib/utils';
export interface ExportDiagramDialogProps extends BaseDialogProps {}
export const ExportDiagramDialog: React.FC<ExportDiagramDialogProps> = ({
dialog,
}) => {
const { t } = useTranslation();
const { diagramName, currentDiagram } = useChartDB();
const [isLoading, setIsLoading] = useState(false);
const { closeExportDiagramDialog } = useDialog();
useEffect(() => {
if (!dialog.open) return;
setIsLoading(false);
}, [dialog.open]);
const downloadOutput = useCallback(
(dataUrl: string) => {
const a = document.createElement('a');
a.setAttribute('download', `ChartDB(${diagramName}).json`);
a.setAttribute('href', dataUrl);
a.click();
},
[diagramName]
);
const handleExport = useCallback(async () => {
setIsLoading(true);
await waitFor(1000);
const json = diagramToJSONOutput(currentDiagram);
const blob = new Blob([json], { type: 'application/json' });
const dataUrl = URL.createObjectURL(blob);
downloadOutput(dataUrl);
setIsLoading(false);
closeExportDiagramDialog();
}, [downloadOutput, currentDiagram, closeExportDiagramDialog]);
const outputTypeOptions: SelectBoxOption[] = useMemo(
() =>
['json'].map((format) => ({
value: format,
label: t(`export_diagram_dialog.format_${format}`),
})),
[t]
);
return (
<Dialog
{...dialog}
onOpenChange={(open) => {
if (!open) {
closeExportDiagramDialog();
}
}}
>
<DialogContent className="flex flex-col" showClose>
<DialogHeader>
<DialogTitle>
{t('export_diagram_dialog.title')}
</DialogTitle>
<DialogDescription>
{t('export_diagram_dialog.description')}
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-1">
<div className="grid w-full items-center gap-4">
<SelectBox
options={outputTypeOptions}
multiple={false}
value="json"
/>
</div>
</div>
<DialogFooter className="flex gap-1 md:justify-between">
<DialogClose asChild>
<Button variant="secondary">
{t('export_diagram_dialog.cancel')}
</Button>
</DialogClose>
<Button onClick={handleExport} disabled={isLoading}>
{isLoading ? (
<Spinner className="mr-1 size-5 text-primary-foreground" />
) : null}
{t('export_diagram_dialog.export')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -20,7 +20,7 @@ import {
import { databaseTypeToLabelMap } from '@/lib/databases';
import { DatabaseType } from '@/lib/domain/database-type';
import { Annoyed, Sparkles } from 'lucide-react';
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import type { BaseDialogProps } from '../common/base-dialog-props';
@@ -37,28 +37,47 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
const { t } = useTranslation();
const [script, setScript] = React.useState<string>();
const [error, setError] = React.useState<boolean>(false);
const [isScriptLoading, setIsScriptLoading] =
React.useState<boolean>(false);
const abortControllerRef = useRef<AbortController | null>(null);
const exportSQLScript = useCallback(async () => {
if (targetDatabaseType === DatabaseType.GENERIC) {
return Promise.resolve(exportBaseSQL(currentDiagram));
} else {
return exportSQL(currentDiagram, targetDatabaseType);
return exportSQL(currentDiagram, targetDatabaseType, {
stream: true,
onResultStream: (text) =>
setScript((prev) => (prev ? prev + text : text)),
signal: abortControllerRef.current?.signal,
});
}
}, [targetDatabaseType, currentDiagram]);
useEffect(() => {
if (!dialog.open) return;
if (!dialog.open) {
abortControllerRef.current?.abort();
return;
}
abortControllerRef.current = new AbortController();
setScript(undefined);
setError(false);
const fetchScript = async () => {
try {
setIsScriptLoading(true);
const script = await exportSQLScript();
setScript(script);
setIsScriptLoading(false);
} catch (e) {
setError(true);
}
};
fetchScript();
return () => {
abortControllerRef.current?.abort();
};
}, [dialog.open, setScript, exportSQLScript, setError]);
const renderError = useCallback(
@@ -156,7 +175,12 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
) : script.length === 0 ? (
renderError()
) : (
<CodeSnippet className="h-96 w-full" code={script!} />
<CodeSnippet
className="h-96 w-full"
code={script!}
autoScroll={true}
isComplete={!isScriptLoading}
/>
)}
</div>

View File

@@ -0,0 +1,129 @@
import React, { useCallback, useEffect, useState } 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 { useTranslation } from 'react-i18next';
import { FileUploader } from '@/components/file-uploader/file-uploader';
import { useStorage } from '@/hooks/use-storage';
import { useNavigate } from 'react-router-dom';
import { diagramFromJSONInput } from '@/lib/export-import-utils';
import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert';
import { AlertCircle } from 'lucide-react';
export interface ImportDiagramDialogProps extends BaseDialogProps {}
export const ImportDiagramDialog: React.FC<ImportDiagramDialogProps> = ({
dialog,
}) => {
const { t } = useTranslation();
const [file, setFile] = useState<File | null>(null);
const { addDiagram } = useStorage();
const navigate = useNavigate();
const [error, setError] = useState(false);
const onFileChange = useCallback((files: File[]) => {
if (files.length === 0) {
setFile(null);
return;
}
setFile(files[0]);
}, []);
useEffect(() => {
if (!dialog.open) return;
setError(false);
setFile(null);
}, [dialog.open]);
const { closeImportDiagramDialog, closeCreateDiagramDialog } = useDialog();
const handleImport = useCallback(() => {
if (!file) return;
const reader = new FileReader();
reader.onload = async (e) => {
const json = e.target?.result;
if (typeof json !== 'string') return;
try {
const diagram = diagramFromJSONInput(json);
await addDiagram({ diagram });
closeImportDiagramDialog();
closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`);
} catch (e) {
setError(true);
throw e;
}
};
reader.readAsText(file);
}, [
file,
addDiagram,
navigate,
closeImportDiagramDialog,
closeCreateDiagramDialog,
]);
return (
<Dialog
{...dialog}
onOpenChange={(open) => {
if (!open) {
closeImportDiagramDialog();
}
}}
>
<DialogContent className="flex flex-col" showClose>
<DialogHeader>
<DialogTitle>
{t('import_diagram_dialog.title')}
</DialogTitle>
<DialogDescription>
{t('import_diagram_dialog.description')}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col p-1">
<FileUploader
supportedExtensions={['.json']}
onFilesChange={onFileChange}
/>
{error ? (
<Alert variant="destructive" className="mt-2">
<AlertCircle className="size-4" />
<AlertTitle>
{t('import_diagram_dialog.error.title')}
</AlertTitle>
<AlertDescription>
{t('import_diagram_dialog.error.description')}
</AlertDescription>
</Alert>
) : null}
</div>
<DialogFooter className="flex gap-1 md:justify-between">
<DialogClose asChild>
<Button variant="secondary">
{t('import_diagram_dialog.cancel')}
</Button>
</DialogClose>
<Button onClick={handleImport} disabled={file === null}>
{t('import_diagram_dialog.import')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -3,69 +3,73 @@
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--subtitle: 215.3 19.3% 34.5%;
}
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--subtitle: 215.3 19.3% 34.5%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--subtitle: 212.7 26.8% 83.9%;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--subtitle: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
.text-editable {
@apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] rounded-md cursor-pointer;
}
}

View File

@@ -7,8 +7,12 @@ import { fr, frMetadata } from './locales/fr';
import { de, deMetadata } from './locales/de';
import { hi, hiMetadata } from './locales/hi';
import { ja, jaMetadata } from './locales/ja';
import { ko_KR, ko_KRMetadata } from './locales/ko_KR.ts';
import { pt_BR, pt_BRMetadata } from './locales/pt_BR';
import { uk, ukMetadata } from './locales/uk';
import { ru, ruMetadata } from './locales/ru';
import { zh_CN, zh_CNMetadata } from './locales/zh_CN';
import { zh_TW, zh_TWMetadata } from './locales/zh_TW';
export const languages: LanguageMetadata[] = [
enMetadata,
@@ -17,8 +21,12 @@ export const languages: LanguageMetadata[] = [
deMetadata,
hiMetadata,
jaMetadata,
ko_KRMetadata,
pt_BRMetadata,
ukMetadata,
ruMetadata,
zh_CNMetadata,
zh_TWMetadata,
];
const resources = {
@@ -28,8 +36,12 @@ const resources = {
de,
hi,
ja,
ko_KR,
pt_BR,
uk,
ru,
zh_CN,
zh_TW,
};
i18n.use(initReactI18next).init({

View File

@@ -32,6 +32,12 @@ export const de: LanguageTranslation = {
show_dependencies: 'Abhängigkeiten anzeigen',
hide_dependencies: 'Abhängigkeiten ausblenden',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'Hilfe',
visit_website: 'ChartDB Webseite',
@@ -226,6 +232,8 @@ export const de: LanguageTranslation = {
cancel: 'Abbrechen',
back: 'Zurück',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: 'Leeres Diagramm',
continue: 'Weiter',
import: 'Importieren',
@@ -329,7 +337,26 @@ export const de: LanguageTranslation = {
close: 'Nicht jetzt',
confirm: 'Natürlich!',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'Ein zu Eins (1:1)',
one_to_many: 'Ein zu Viele (1:n)',
@@ -346,6 +373,13 @@ export const de: LanguageTranslation = {
edit_table: 'Tabelle bearbeiten',
delete_table: 'Tabelle löschen',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'Doppelklicken zum Bearbeiten',
},
},
};

View File

@@ -32,6 +32,11 @@ export const en = {
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
},
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'Help',
visit_website: 'Visit ChartDB',
@@ -224,6 +229,7 @@ export const en = {
},
cancel: 'Cancel',
import_from_file: 'Import from File',
back: 'Back',
empty_diagram: 'Empty diagram',
continue: 'Continue',
@@ -328,7 +334,25 @@ export const en = {
close: 'Not now',
confirm: 'Of course!',
},
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'One to One',
one_to_many: 'One to Many',
@@ -345,6 +369,12 @@ export const en = {
edit_table: 'Edit Table',
delete_table: 'Delete Table',
},
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'Double-click to edit',
},
},
};

View File

@@ -29,9 +29,14 @@ export const es: LanguageTranslation = {
zoom_on_scroll: 'Zoom al Desplazarse',
theme: 'Tema',
change_language: 'Idioma',
// TODO: Translate
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
show_dependencies: 'Mostrar dependencias',
hide_dependencies: 'Ocultar dependencias',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'Ayuda',
@@ -80,20 +85,19 @@ export const es: LanguageTranslation = {
saved: 'Guardado',
diagrams: 'Diagramas',
loading_diagram: 'Cargando diagrama...',
deselect_all: 'Deselect All', // TODO: Translate
select_all: 'Select All', // TODO: Translate
clear: 'Clear', // TODO: Translate
show_more: 'Show More', // TODO: Translate
show_less: 'Show Less', // TODO: Translate
// TODO: Translate
deselect_all: 'Deseleccionar todo',
select_all: 'Seleccionar todo',
clear: 'Limpiar',
show_more: 'Mostrar más',
show_less: 'Mostrar menos',
copy_to_clipboard: 'Copy to Clipboard',
copied: 'Copied!',
side_panel: {
schema: 'Schema:', // TODO: Translate
filter_by_schema: 'Filter by schema', // TODO: Translate
search_schema: 'Search schema...', // TODO: Translate
no_schemas_found: 'No schemas found.', // TODO: Translate
schema: 'Esquema:',
filter_by_schema: 'Filtrar por esquema',
search_schema: 'Buscar esquema...',
no_schemas_found: 'No se encontraron esquemas.',
view_all_options: 'Ver todas las opciones...',
tables_section: {
tables: 'Tablas',
@@ -113,7 +117,7 @@ export const es: LanguageTranslation = {
index_select_fields: 'Seleccionar campos',
field_name: 'Nombre',
field_type: 'Tipo',
no_types_found: 'No types found', // TODO: Translate
no_types_found: 'No se encontraron tipos',
field_actions: {
title: 'Atributos del Campo',
unique: 'Único',
@@ -160,23 +164,22 @@ export const es: LanguageTranslation = {
description: 'Crea una relación para conectar tablas',
},
},
// TODO: Translate
dependencies_section: {
dependencies: 'Dependencies',
filter: 'Filter',
collapse: 'Collapse All',
dependencies: 'Dependencias',
filter: 'Filtro',
collapse: 'Colapsar todo',
dependency: {
table: 'Table',
dependent_table: 'Dependent View',
delete_dependency: 'Delete',
table: 'Tabla',
dependent_table: 'Vista dependiente',
delete_dependency: 'Eliminar',
dependency_actions: {
title: 'Actions',
delete_dependency: 'Delete',
title: 'Acciones',
delete_dependency: 'Eliminar',
},
},
empty_state: {
title: 'No dependencies',
description: 'Create a view to get started',
title: 'Sin dependencias',
description: 'Crea una vista para comenzar',
},
},
},
@@ -189,8 +192,7 @@ export const es: LanguageTranslation = {
undo: 'Deshacer',
redo: 'Rehacer',
reorder_diagram: 'Reordenar Diagrama',
// TODO: Translate
highlight_overlapping_tables: 'Highlight Overlapping Tables',
highlight_overlapping_tables: 'Resaltar tablas superpuestas',
},
new_diagram_dialog: {
@@ -214,20 +216,20 @@ export const es: LanguageTranslation = {
step_1: 'Ve a Herramientas > Opciones > Resultados de Consulta > SQL Server.',
step_2: 'Si estás usando "Resultados en Cuadrícula", cambia el Máximo de Caracteres Recuperados para Datos No XML (configúralo en 9999999).',
},
// TODO: Translate
instructions_link: 'Need help? Watch how',
check_script_result: 'Check Script Result',
instructions_link: '¿Necesitas ayuda? mira cómo',
check_script_result: 'Revisa el resultado del script',
},
cancel: 'Cancelar',
back: 'Atrás',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: 'Diagrama vacío',
continue: 'Continuar',
import: 'Importar',
},
open_diagram_dialog: {
// TODO: Translate
title: 'Abrir Diagrama',
description:
'Selecciona un diagrama para abrir de la lista a continuación.',
@@ -293,16 +295,15 @@ export const es: LanguageTranslation = {
},
},
// TODO: Translate
export_image_dialog: {
title: 'Export Image',
description: 'Choose the scale factor for export:',
scale_1x: '1x Regular',
scale_2x: '2x (Recommended)',
title: 'Exportar imagen',
description: 'Escoge el factor de escalamiento para exportar:',
scale_1x: '1x regular',
scale_2x: '2x (recomendado)',
scale_3x: '3x',
scale_4x: '4x',
cancel: 'Cancel',
export: 'Export',
cancel: 'Cancelar',
export: 'Exportar',
},
new_table_schema_dialog: {
@@ -336,7 +337,26 @@ export const es: LanguageTranslation = {
change_schema: 'Cambiar',
none: 'nada',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'Uno a Uno',
one_to_many: 'Uno a Muchos',
@@ -353,6 +373,13 @@ export const es: LanguageTranslation = {
edit_table: 'Editar Tabla',
delete_table: 'Eliminar Tabla',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'Doble clic para editar',
},
},
};

View File

@@ -15,7 +15,7 @@ export const fr: LanguageTranslation = {
exit: 'Quitter',
},
edit: {
edit: 'Éditer',
edit: 'Édition',
undo: 'Annuler',
redo: 'Rétablir',
clear: 'Effacer',
@@ -32,6 +32,11 @@ export const fr: LanguageTranslation = {
show_dependencies: 'Afficher les Dépendances',
hide_dependencies: 'Masquer les Dépendances',
},
share: {
share: 'Partage',
export_diagram: 'Exporter le diagramme',
import_diagram: 'Importer un diagramme',
},
help: {
help: 'Aide',
visit_website: 'Visitez ChartDB',
@@ -218,6 +223,8 @@ export const fr: LanguageTranslation = {
cancel: 'Annuler',
back: 'Retour',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: 'Diagramme vide',
continue: 'Continuer',
import: 'Importer',
@@ -332,7 +339,26 @@ export const fr: LanguageTranslation = {
cancel: 'Annuler',
},
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'Un à Un',
one_to_many: 'Un à Plusieurs',
@@ -349,6 +375,13 @@ export const fr: LanguageTranslation = {
edit_table: 'Éditer la Table',
delete_table: 'Supprimer la Table',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'Double-cliquez pour modifier',
},
},
};

View File

@@ -32,6 +32,12 @@ export const hi: LanguageTranslation = {
show_dependencies: 'निर्भरता दिखाएँ',
hide_dependencies: 'निर्भरता छिपाएँ',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'मदद',
visit_website: 'ChartDB वेबसाइट पर जाएँ',
@@ -228,6 +234,8 @@ export const hi: LanguageTranslation = {
cancel: 'रद्द करें',
back: 'वापस',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: 'खाली आरेख',
continue: 'जारी रखें',
import: 'आयात करें',
@@ -331,7 +339,26 @@ export const hi: LanguageTranslation = {
close: 'अभी नहीं',
confirm: 'बिलकुल!',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'एक से एक',
one_to_many: 'एक से कई',
@@ -348,6 +375,13 @@ export const hi: LanguageTranslation = {
edit_table: 'तालिका संपादित करें',
delete_table: 'तालिका हटाएँ',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'संपादित करने के लिए डबल-क्लिक करें',
},
},
};

View File

@@ -33,6 +33,12 @@ export const ja: LanguageTranslation = {
show_dependencies: 'Show Dependencies',
hide_dependencies: 'Hide Dependencies',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'ヘルプ',
visit_website: 'ChartDBにアクセス',
@@ -230,6 +236,8 @@ export const ja: LanguageTranslation = {
cancel: 'キャンセル',
back: '戻る',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: '空のダイアグラム',
continue: '続行',
import: 'インポート',
@@ -333,7 +341,26 @@ export const ja: LanguageTranslation = {
close: '今はしない',
confirm: 'もちろん!',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: '1対1',
one_to_many: '1対多',
@@ -350,6 +377,13 @@ export const ja: LanguageTranslation = {
edit_table: 'テーブルを編集',
delete_table: 'テーブルを削除',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'ダブルクリックして編集',
},
},
};

387
src/i18n/locales/ko_KR.ts Normal file
View File

@@ -0,0 +1,387 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ko_KR: LanguageTranslation = {
translation: {
menu: {
file: {
file: '파일',
new: '새 다이어그램',
open: '열기',
save: '저장',
import_database: '데이터베이스 가져오기',
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: '테마',
change_language: '언어/Language',
show_dependencies: '종속성 보이기',
hide_dependencies: '종속성 숨기기',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: '도움말',
visit_website: 'ChartDB 사이트 방문',
join_discord: 'Discord 가입',
schedule_a_call: 'Talk with us!',
},
},
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:
'현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.',
dont_show_again: '다시 보여주지 마세요',
change_schema: '변경',
none: '없음',
},
theme: {
system: '시스템 설정에 따름',
light: '밝게',
dark: '어둡게',
},
zoom: {
on: '활성화',
off: '비활성화',
},
last_saved: '최근 저장일시: ',
saved: '저장됨',
diagrams: '다이어그램',
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: '모두 접기',
table: {
fields: '필드',
nullable: 'null 여부',
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: '인덱스 추가',
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: '데이터베이스에서 아래의 SQL을 실행해주세요:',
step_2: '이곳에 결과를 붙여넣어주세요:',
script_results_placeholder: '이곳에 스크립트 결과를 입력...',
ssms_instructions: {
button_text: 'SSMS을 사용하시는 경우',
title: '지침',
step_1: '도구 > 옵션 > 쿼리 응답 > SQL Server',
step_2: '"결과를 그리드로 표시"를 사용하는 경우 비 XML 데이터에 대해 검색되는 최대 문자 수를 변경합니다. (9999999로 설정)',
},
instructions_link: '도움이 필요하신가요? 영상 가이드 보기',
check_script_result: '스크립트 결과 확인',
},
cancel: '취소',
back: '뒤로가기',
// TODO: Translate
import_from_file: 'Import from File',
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}} SQL로 내보내기',
close: '닫기',
loading: {
text: '{{databaseType}} SQL을 AI가 생성하고 있습니다...',
description: '30초 정도 걸릴 수 있습니다.',
},
error: {
message:
'SQL 생성에 실패하였습니다. 잠시후 다시 시도해주세요 계속해서 증상이 발생하는 경우 <0>우리에게 연락해주세요</0>.',
description:
'당신의 OPENAI_TOKEN가 있는 경우, <0>여기에서</0> 메뉴얼을 참고하여 사용하실 수 있습니다.',
},
},
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: 'Confirm',
},
update_table_schema_dialog: {
title: '스키마 변경',
description: '"{{tableName}}" 테이블 스키마를 수정합니다',
cancel: '취소',
confirm: '변경',
},
star_us_dialog: {
title: '개선할 수 있도록 도와주세요!',
description:
'GitHub에 별을 찍어주시겠습니까? 클릭 한번이면 됩니다!',
close: '아직은 괜찮아요',
confirm: '당연하죠!',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: '일대일 (1:1)',
one_to_many: '일대다 (1:N)',
many_to_one: '다대일 (N:1)',
many_to_many: '다대다 (N:N)',
},
canvas_context_menu: {
new_table: '새 테이블',
new_relationship: '새 연관관계',
},
table_node_context_menu: {
edit_table: '테이블 수정',
delete_table: '테이블 삭제',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: '더블클릭하여 편집',
},
},
};
export const ko_KRMetadata: LanguageMetadata = {
name: '한국어',
code: 'ko_KR',
};

View File

@@ -32,6 +32,12 @@ export const pt_BR: LanguageTranslation = {
show_dependencies: 'Mostrar Dependências',
hide_dependencies: 'Ocultar Dependências',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'Ajuda',
visit_website: 'Visitar ChartDB',
@@ -225,6 +231,8 @@ export const pt_BR: LanguageTranslation = {
cancel: 'Cancelar',
back: 'Voltar',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: 'Diagrama vazio',
continue: 'Continuar',
import: 'Importar',
@@ -328,7 +336,26 @@ export const pt_BR: LanguageTranslation = {
close: 'Agora não',
confirm: 'Claro!',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'Um para Um',
one_to_many: 'Um para Muitos',
@@ -345,6 +372,13 @@ export const pt_BR: LanguageTranslation = {
edit_table: 'Editar Tabela',
delete_table: 'Excluir Tabela',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'Duplo clique para editar',
},
},
};

383
src/i18n/locales/ru.ts Normal file
View File

@@ -0,0 +1,383 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ru: LanguageTranslation = {
translation: {
menu: {
file: {
file: 'Файл',
new: 'Создать',
open: 'Открыть',
save: 'Сохранить',
import_database: 'Импортировать базу данных',
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: 'Тема',
change_language: 'Сменить язык',
show_dependencies: 'Показать зависимости',
hide_dependencies: 'Скрыть зависимости',
},
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:
'{{schemasCount}} схем в этой диаграмме. В данный момент отображается: {{formattedSchemas}}.',
dont_show_again: 'Больше не показывать',
change_schema: 'Изменить',
none: 'никто',
},
theme: {
system: 'Системная',
light: 'Светлая',
dark: 'Темная',
},
zoom: {
on: 'Включено',
off: 'Выключено',
},
last_saved: 'Последнее сохранение',
saved: 'Сохранено',
diagrams: 'Диаграммы',
loading_diagram: 'Загрузка диаграммы...',
deselect_all: 'Отменить выбор всех',
select_all: 'Выбрать все',
clear: 'Очистить',
show_more: 'Показать больше',
show_less: 'Показать меньше',
side_panel: {
schema: 'Схема:',
filter_by_schema: 'Фильтр по схеме',
search_schema: 'Схема поиска...',
no_schemas_found: 'Схемы не найдены.',
view_all_options: 'Просмотреть все варианты...',
tables_section: {
tables: 'Таблицы',
add_table: 'Добавить таблицу',
filter: 'Фильтр',
collapse: 'Свернуть все',
table: {
fields: 'Поля',
nullable: 'Может содержать NULL?',
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: 'Добавить индекс',
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 Сервер.',
step_2: 'Если вы используете "Результат в сетке," измените Максимальное количество извлекаемых символов для данных, отличных от XML (установите на 9999999).',
},
instructions_link: 'Нужна помощь? Посмотрите, как',
check_script_result: 'Проверить результат выполнения скрипта',
},
cancel: 'Отменить',
back: 'Назад',
import_from_file: 'Импортировать из файла',
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: 'ИИ генерирует SQL для {{databaseType}}...',
description: 'Это должно занять до 30 секунд.',
},
error: {
message:
'Ошибка создания скрипта SQL. Попробуйте еще раз позже или <0>свяжитесь с нами</0>.',
description:
'Не стесняйтесь использовать ваш OPENAI_TOKEN, см. руководство <0>здесь</0>.',
},
},
create_relationship_dialog: {
title: 'Создать отношениe',
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: 'Экспортировать',
},
import_diagram_dialog: {
title: 'Импорт кода диаграммы',
description: 'Вставьте JSON код диаграммы ниже:',
cancel: 'Отменить',
import: 'Импортировать',
error: {
title: 'Ошибка при импорте диаграммы',
description:
'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: chartdb.io@gmail.com',
},
},
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: 'Изменить таблицу',
delete_table: 'Удалить таблицу',
},
copy_to_clipboard: 'Скопировать в буфер обмена',
copied: 'Скопировано!',
snap_to_grid_tooltip: 'Выравнивание по сетке (Удерживайте {{key}})',
tool_tips: {
double_click_to_edit: 'Кликните дважды, чтобы изменить',
},
},
};
export const ruMetadata: LanguageMetadata = {
name: 'Russian',
code: 'ru',
};

View File

@@ -32,6 +32,12 @@ export const uk: LanguageTranslation = {
show_dependencies: 'Показати залежності',
hide_dependencies: 'Приховати залежності',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'Допомога',
visit_website: 'Відвідайте ChartDB',
@@ -225,6 +231,8 @@ export const uk: LanguageTranslation = {
cancel: 'Скасувати',
back: 'Назад',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: 'Порожня діаграма',
continue: 'Продовжити',
import: 'Імпорт',
@@ -328,7 +336,26 @@ export const uk: LanguageTranslation = {
close: 'Не зараз',
confirm: 'звичайно!',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'Один до одного',
one_to_many: 'Один до багатьох',
@@ -345,6 +372,13 @@ export const uk: LanguageTranslation = {
edit_table: 'Редагувати таблицю',
delete_table: 'Видалити таблицю',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: 'Двойной клик для редактирования',
},
},
};

378
src/i18n/locales/zh_CN.ts Normal file
View File

@@ -0,0 +1,378 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const zh_CN: LanguageTranslation = {
translation: {
menu: {
file: {
file: '文件',
new: '新建',
open: '打开',
save: '保存',
import_database: '导入数据库',
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: '主题',
change_language: '语言',
show_dependencies: '展示依赖',
hide_dependencies: '隐藏依赖',
},
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:
'此关系图中有 {{schemasCount}} 个模式,当前显示:{{formattedSchemas}}。',
dont_show_again: '不再展示',
change_schema: '更改',
none: '无',
},
theme: {
system: '系统',
light: '浅色',
dark: '深色',
},
zoom: {
on: '启用',
off: '禁用',
},
last_saved: '上次保存时间:',
saved: '已保存',
diagrams: '关系图',
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: '全部折叠',
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: '添加索引',
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。',
// TODO: Add translations
step_2: '如果您使用“Result to Grid”功能请将非 XML 数据的最大提取字符数更改为 9999999。',
},
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: 'AI 正在为 {{databaseType}} 生成 SQL 语句...',
description: '此操作最多需要 30 秒。',
},
error: {
message:
'生成 SQL 脚本时出错。请稍后再试,或者 <0>联系我们</0>。',
description:
'随时使用您的 OPENAI_TOKEN在<0>这里</0>查看手册。',
},
},
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: '导出',
},
import_diagram_dialog: {
title: '导入关系图',
description: '在下方粘贴关系图的 JSON',
cancel: '取消',
import: '导入',
error: {
title: '导入关系图时出错',
description:
'关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 chartdb.io@gmail.com',
},
},
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: '编辑表',
delete_table: '删除表',
},
snap_to_grid_tooltip: '对齐到网格(按住 {{key}}',
tool_tips: {
double_click_to_edit: '双击编辑',
},
},
};
export const zh_CNMetadata: LanguageMetadata = {
name: '简体中文',
code: 'zh_CN',
};

377
src/i18n/locales/zh_TW.ts Normal file
View File

@@ -0,0 +1,377 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const zh_TW: LanguageTranslation = {
translation: {
menu: {
file: {
file: '檔案',
new: '新增',
open: '開啟',
save: '儲存',
import_database: '匯入資料庫',
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: '主題',
change_language: '變更語言',
show_dependencies: '顯示相依性',
hide_dependencies: '隱藏相依性',
},
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: '多重 Schema',
description:
'此圖表中包含 {{schemasCount}} 個 Schema目前顯示{{formattedSchemas}}。',
dont_show_again: '不再顯示',
change_schema: '變更',
none: '無',
},
theme: {
system: '系統',
light: '淺色',
dark: '深色',
},
zoom: {
on: '開啟',
off: '關閉',
},
last_saved: '上次儲存於',
saved: '已儲存',
diagrams: '圖表',
loading_diagram: '正在載入圖表...',
deselect_all: '取消所有選取',
select_all: '全選',
clear: '清除',
show_more: '顯示更多',
show_less: '顯示較少',
copy_to_clipboard: '複製到剪貼簿',
copied: '已複製!',
side_panel: {
schema: 'Schema:',
filter_by_schema: '依 Schema 篩選',
search_schema: '搜尋 Schema...',
no_schemas_found: '未找到 Schema。',
view_all_options: '顯示所有選項...',
tables_section: {
tables: '表格',
add_table: '新增表格',
filter: '篩選',
collapse: '全部摺疊',
table: {
fields: '欄位',
nullable: '可為 NULL?',
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: '變更 Schema',
add_field: '新增欄位',
add_index: '新增索引',
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: '若使用「結果至網格」,請更改非 XML 資料的最大取得字元數(設定為 9999999。',
},
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: '將圖表 Schema 匯出為 {{databaseType}} 格式的腳本',
close: '關閉',
loading: {
text: 'AI 正在生成 {{databaseType}} 的 SQL...',
description: '最多需要 30 秒。',
},
error: {
message:
'生成 SQL 腳本時發生錯誤。稍後再試,或<0>聯繫我們</0>。',
description:
'可以自由使用 OPENAI_TOKEN詳細說明可參考<0>此處</0>。',
},
},
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: '選擇 Schema',
description: '目前顯示多個 Schema請為新表格選擇一個。',
cancel: '取消',
confirm: '確認',
},
update_table_schema_dialog: {
title: '變更 Schema',
description: '更新表格「{{tableName}}」的 Schema',
cancel: '取消',
confirm: '變更',
},
star_us_dialog: {
title: '協助我們改善!',
description: '請在 GitHub 上給我們一顆星,只需點擊一下!',
close: '先不要',
confirm: '當然!',
},
export_diagram_dialog: {
title: '匯出圖表',
description: '選擇匯出格式:',
format_json: 'JSON',
cancel: '取消',
export: '匯出',
},
import_diagram_dialog: {
title: '匯入圖表',
description: '請在下方貼上圖表的 JSON',
cancel: '取消',
import: '匯入',
error: {
title: '匯入圖表時發生錯誤',
description:
'圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 chartdb.io@gmail.com',
},
},
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: '編輯表格',
delete_table: '刪除表格',
},
snap_to_grid_tooltip: '對齊網格(按住 {{key}}',
tool_tips: {
double_click_to_edit: '雙擊以編輯',
},
},
};
export const zh_TWMetadata: LanguageMetadata = {
name: '繁體中文',
code: 'zh_TW',
};

View File

@@ -1,3 +1,4 @@
import { z } from 'zod';
import { DatabaseType } from '../../domain/database-type';
import { genericDataTypes } from './generic-data-types';
import { mariadbDataTypes } from './mariadb-data-types';
@@ -11,6 +12,11 @@ export interface DataType {
name: string;
}
export const dataTypeSchema: z.ZodType<DataType> = z.object({
id: z.string(),
name: z.string(),
});
export const dataTypeMap: Record<DatabaseType, readonly DataType[]> = {
[DatabaseType.GENERIC]: genericDataTypes,
[DatabaseType.POSTGRESQL]: postgresDataTypes,

View File

@@ -190,16 +190,39 @@ export const exportBaseSQL = (diagram: Diagram): string => {
export const exportSQL = async (
diagram: Diagram,
databaseType: DatabaseType
databaseType: DatabaseType,
options?: {
stream: boolean;
onResultStream: (text: string) => void;
signal?: AbortSignal;
}
): Promise<string> => {
const { generateText } = await import('ai');
const { createOpenAI } = await import('@ai-sdk/openai');
const [{ streamText, generateText }, { createOpenAI }] = await Promise.all([
import('ai'),
import('@ai-sdk/openai'),
]);
const openai = createOpenAI({
apiKey: OPENAI_API_KEY,
});
const sqlScript = exportBaseSQL(diagram);
const prompt = generateSQLPrompt(databaseType, sqlScript);
if (options?.stream) {
const { textStream, text } = await streamText({
model: openai('gpt-4o-mini-2024-07-18'),
prompt: prompt,
});
for await (const textPart of textStream) {
if (options.signal?.aborted) {
return '';
}
options.onResultStream(textPart);
}
return text;
}
const { text } = await generateText({
model: openai('gpt-4o-mini-2024-07-18'),
prompt: prompt,

View File

@@ -5,17 +5,24 @@ export const fixMetadataJson = async (
metadataJson: string
): Promise<string> => {
await waitFor(1000);
return metadataJson
.trim()
.replace(/^[^{]*/, '') // Remove everything before the first '{'
.replace(/}[^}]*$/, '}') // Remove everything after the last '}'
.replace(/^\s+|\s+$/g, '')
.replace(/^"|"$/g, '')
.replace(/^'|'$/g, '')
.replace(/(?<=:\s*)""(?=\s*[,}])/g, '___EMPTY___') // Temporarily replace empty strings
.replace(/""/g, '"') // Replace remaining double quotes
.replace(/___EMPTY___/g, '""') // Restore empty strings
.replace(/\n/g, '');
// TODO: remove this temporary eslint disable
return (
metadataJson
.trim()
.replace(/^[^{]*/, '') // Remove everything before the first '{'
.replace(/}[^}]*$/, '}') // Remove everything after the last '}'
.replace(/^\s+|\s+$/g, '')
.replace(/^"|"$/g, '')
.replace(/^'|'$/g, '')
/* eslint-disable-next-line no-useless-escape */
.replace(/\"/g, '___ESCAPED_QUOTE___') // Temporarily replace empty strings
.replace(/(?<=:\s*)""(?=\s*[,}])/g, '___EMPTY___') // Temporarily replace empty strings
.replace(/""/g, '"') // Replace remaining double quotes
.replace(/___ESCAPED_QUOTE___/g, '"') // Restore empty strings
.replace(/___EMPTY___/g, '""') // Restore empty strings
.replace(/\n/g, '')
);
};
export const isStringMetadataJson = (metadataJsonString: string): boolean => {

View File

@@ -1,3 +1,4 @@
import { z } from 'zod';
import type { ViewInfo } from '../data/import-metadata/metadata-types/view-info';
import { DatabaseType } from './database-type';
import {
@@ -17,6 +18,15 @@ export interface DBDependency {
createdAt: number;
}
export const dbDependencySchema: z.ZodType<DBDependency> = z.object({
id: z.string(),
schema: z.string().optional(),
tableId: z.string(),
dependentSchema: z.string().optional(),
dependentTableId: z.string(),
createdAt: z.number(),
});
export const shouldShowDependencyBySchemaFilter = (
dependency: DBDependency,
filteredSchemas?: string[]

View File

@@ -1,4 +1,5 @@
import type { DataType } from '../data/data-types/data-types';
import { z } from 'zod';
import { dataTypeSchema, type DataType } from '../data/data-types/data-types';
import type { ColumnInfo } from '../data/import-metadata/metadata-types/column-info';
import type { AggregatedIndexInfo } from '../data/import-metadata/metadata-types/index-info';
import type { PrimaryKeyInfo } from '../data/import-metadata/metadata-types/primary-key-info';
@@ -22,6 +23,22 @@ export interface DBField {
comments?: string;
}
export const dbFieldSchema: z.ZodType<DBField> = z.object({
id: z.string(),
name: z.string(),
type: dataTypeSchema,
primaryKey: z.boolean(),
unique: z.boolean(),
nullable: z.boolean(),
createdAt: z.number(),
characterMaximumLength: z.string().optional(),
precision: z.number().optional(),
scale: z.number().optional(),
default: z.string().optional(),
collation: z.string().optional(),
comments: z.string().optional(),
});
export const createFieldsFromMetadata = ({
columns,
tableSchema,

View File

@@ -1,3 +1,4 @@
import { z } from 'zod';
import type { AggregatedIndexInfo } from '../data/import-metadata/metadata-types/index-info';
import { generateId } from '../utils';
import type { DBField } from './db-field';
@@ -10,6 +11,14 @@ export interface DBIndex {
createdAt: number;
}
export const dbIndexSchema: z.ZodType<DBIndex> = z.object({
id: z.string(),
name: z.string(),
unique: z.boolean(),
fieldIds: z.array(z.string()),
createdAt: z.number(),
});
export const createIndexesFromMetadata = ({
aggregatedIndexes,
fields,

View File

@@ -1,3 +1,4 @@
import { z } from 'zod';
import type { ForeignKeyInfo } from '../data/import-metadata/metadata-types/foreign-key-info';
import type { DBField } from './db-field';
import {
@@ -21,6 +22,20 @@ export interface DBRelationship {
createdAt: number;
}
export const dbRelationshipSchema: z.ZodType<DBRelationship> = z.object({
id: z.string(),
name: z.string(),
sourceSchema: z.string().optional(),
sourceTableId: z.string(),
targetSchema: z.string().optional(),
targetTableId: z.string(),
sourceFieldId: z.string(),
targetFieldId: z.string(),
sourceCardinality: z.union([z.literal('one'), z.literal('many')]),
targetCardinality: z.union([z.literal('one'), z.literal('many')]),
createdAt: z.number(),
});
export type RelationshipType =
| 'one_to_one'
| 'one_to_many'

View File

@@ -1,5 +1,13 @@
import { createIndexesFromMetadata, type DBIndex } from './db-index';
import { createFieldsFromMetadata, type DBField } from './db-field';
import {
createIndexesFromMetadata,
dbIndexSchema,
type DBIndex,
} from './db-index';
import {
createFieldsFromMetadata,
dbFieldSchema,
type DBField,
} from './db-field';
import type { TableInfo } from '../data/import-metadata/metadata-types/table-info';
import { createAggregatedIndexes } from '../data/import-metadata/metadata-types/index-info';
import { materializedViewColor, viewColor, randomColor } from '@/lib/colors';
@@ -16,6 +24,7 @@ import {
} from './db-schema';
import { DatabaseType } from './database-type';
import type { DatabaseMetadata } from '../data/import-metadata/metadata-types/database-metadata';
import { z } from 'zod';
export interface DBTable {
id: string;
@@ -34,6 +43,23 @@ export interface DBTable {
hidden?: boolean;
}
export const dbTableSchema: z.ZodType<DBTable> = z.object({
id: z.string(),
name: z.string(),
schema: z.string().optional(),
x: z.number(),
y: z.number(),
fields: z.array(dbFieldSchema),
indexes: z.array(dbIndexSchema),
color: z.string(),
isView: z.boolean(),
isMaterializedView: z.boolean().optional(),
createdAt: z.number(),
width: z.number().optional(),
comments: z.string().optional(),
hidden: z.boolean().optional(),
});
export const shouldShowTablesBySchemaFilter = (
table: DBTable,
filteredSchemas?: string[]

View File

@@ -1,12 +1,23 @@
import { z } from 'zod';
import type { DatabaseMetadata } from '../data/import-metadata/metadata-types/database-metadata';
import type { DatabaseEdition } from './database-edition';
import { DatabaseEdition } from './database-edition';
import { DatabaseType } from './database-type';
import type { DBDependency } from './db-dependency';
import { createDependenciesFromMetadata } from './db-dependency';
import {
createDependenciesFromMetadata,
dbDependencySchema,
} from './db-dependency';
import type { DBRelationship } from './db-relationship';
import { createRelationshipsFromMetadata } from './db-relationship';
import {
createRelationshipsFromMetadata,
dbRelationshipSchema,
} from './db-relationship';
import type { DBTable } from './db-table';
import { adjustTablePositions, createTablesFromMetadata } from './db-table';
import {
adjustTablePositions,
createTablesFromMetadata,
dbTableSchema,
} from './db-table';
import { generateDiagramId } from '@/lib/utils';
export interface Diagram {
id: string;
@@ -20,6 +31,18 @@ export interface Diagram {
updatedAt: Date;
}
export const diagramSchema: z.ZodType<Diagram> = z.object({
id: z.string(),
name: z.string(),
databaseType: z.nativeEnum(DatabaseType),
databaseEdition: z.nativeEnum(DatabaseEdition).optional(),
tables: z.array(dbTableSchema).optional(),
relationships: z.array(dbRelationshipSchema).optional(),
dependencies: z.array(dbDependencySchema).optional(),
createdAt: z.date(),
updatedAt: z.date(),
});
export const loadFromDatabaseMetadata = async ({
databaseType,
databaseMetadata,

View File

@@ -0,0 +1,32 @@
import { diagramSchema, type Diagram } from './domain/diagram';
import { cloneDiagram, generateDiagramId, generateId } from './utils';
export const runningIdGenerator = (): (() => string) => {
let id = 0;
return () => (id++).toString();
};
const cloneDiagramWithRunningIds = (diagram: Diagram) =>
cloneDiagram(diagram, runningIdGenerator());
const cloneDiagramWithIds = (diagram: Diagram): Diagram => ({
...cloneDiagram(diagram, generateId),
id: generateDiagramId(),
});
export const diagramToJSONOutput = (diagram: Diagram): string => {
const clonedDiagram = cloneDiagramWithRunningIds(diagram);
return JSON.stringify(clonedDiagram, null, 2);
};
export const diagramFromJSONInput = (json: string): Diagram => {
const loadedDiagram = JSON.parse(json);
const diagram = diagramSchema.parse({
...loadedDiagram,
createdAt: new Date(),
updatedAt: new Date(),
});
return cloneDiagramWithIds(diagram);
};

View File

@@ -1,6 +1,12 @@
import { type ClassValue, clsx } from 'clsx';
import { customAlphabet } from 'nanoid';
import { twMerge } from 'tailwind-merge';
import type { Diagram } from './domain/diagram';
import type { DBTable } from './domain/db-table';
import type { DBField } from './domain/db-field';
import type { DBIndex } from './domain/db-index';
import type { DBRelationship } from './domain/db-relationship';
import type { DBDependency } from './domain/db-dependency';
const randomId = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 25);
const UUID_KEY = 'uuid';
@@ -88,3 +94,89 @@ export const decodeBase64ToUtf8 = (base64: string) => {
export const waitFor = async (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
export const cloneDiagram = (
diagram: Diagram,
generateId: () => string
): Diagram => {
const diagramId = generateId();
const idsMap = new Map<string, string>();
diagram.tables?.forEach((table) => {
idsMap.set(table.id, generateId());
table.fields.forEach((field) => {
idsMap.set(field.id, generateId());
});
table.indexes.forEach((index) => {
idsMap.set(index.id, generateId());
});
});
diagram.relationships?.forEach((relationship) => {
idsMap.set(relationship.id, generateId());
});
diagram.dependencies?.forEach((dependency) => {
idsMap.set(dependency.id, generateId());
});
const getNewId = (id: string) => {
const newId = idsMap.get(id);
if (!newId) {
throw new Error(`Id not found for ${id}`);
}
return newId;
};
const tables: DBTable[] =
diagram.tables?.map((table) => {
const newTable: DBTable = { ...table, id: getNewId(table.id) };
newTable.fields = table.fields.map(
(field): DBField => ({
...field,
id: getNewId(field.id),
})
);
newTable.indexes = table.indexes.map(
(index): DBIndex => ({
...index,
id: getNewId(index.id),
})
);
return newTable;
}) ?? [];
const relationships: DBRelationship[] =
diagram.relationships?.map(
(relationship): DBRelationship => ({
...relationship,
id: getNewId(relationship.id),
sourceTableId: getNewId(relationship.sourceTableId),
targetTableId: getNewId(relationship.targetTableId),
sourceFieldId: getNewId(relationship.sourceFieldId),
targetFieldId: getNewId(relationship.targetFieldId),
})
) ?? [];
const dependencies: DBDependency[] =
diagram.dependencies?.map(
(dependency): DBDependency => ({
...dependency,
id: getNewId(dependency.id),
dependentTableId: getNewId(dependency.dependentTableId),
tableId: getNewId(dependency.tableId),
})
) ?? [];
return {
...diagram,
id: diagramId,
dependencies,
relationships,
tables,
createdAt: new Date(),
updatedAt: new Date(),
};
};

View File

@@ -1,5 +1,5 @@
import { Spinner } from '@/components/spinner/spinner';
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { useLoaderData, useNavigate } from 'react-router-dom';
import type { TemplatePageLoaderData } from '../template-page/template-page';
import { convertTemplateToNewDiagram } from '@/templates-data/template-utils';
@@ -12,6 +12,7 @@ import { ThemeProvider } from '@/context/theme-context/theme-provider';
export const CloneTemplateComponent: React.FC = () => {
const navigate = useNavigate();
const { addDiagram, deleteDiagram } = useStorage();
const clonedBefore = useRef<boolean>(false);
const data = useLoaderData() as TemplatePageLoaderData;
const template = data.template;
@@ -21,6 +22,11 @@ export const CloneTemplateComponent: React.FC = () => {
return;
}
if (clonedBefore.current) {
return;
}
clonedBefore.current = true;
const diagram = convertTemplateToNewDiagram(template);
await deleteDiagram(diagram.id);

View File

@@ -22,6 +22,7 @@ import {
MiniMap,
Controls,
useReactFlow,
useKeyPress,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import equal from 'fast-deep-equal';
@@ -36,7 +37,7 @@ import {
} from './table-node/table-node-field';
import { Toolbar } from './toolbar/toolbar';
import { useToast } from '@/components/toast/use-toast';
import { Pencil, LayoutGrid, AlertTriangle } from 'lucide-react';
import { Pencil, LayoutGrid, AlertTriangle, Magnet } from 'lucide-react';
import { Button } from '@/components/button/button';
import { useLayout } from '@/hooks/use-layout';
import { useBreakpoint } from '@/hooks/use-breakpoint';
@@ -66,7 +67,7 @@ import {
import type { Graph } from '@/lib/graph';
import { createGraph, removeVertex } from '@/lib/graph';
import type { ChartDBEvent } from '@/context/chartdb-context/chartdb-context';
import { debounce } from '@/lib/utils';
import { cn, debounce, getOperatingSystem } from '@/lib/utils';
import type { DependencyEdgeType } from './dependency-edge';
import { DependencyEdge } from './dependency-edge';
import {
@@ -148,6 +149,8 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
const [edges, setEdges, onEdgesChange] =
useEdgesState<EdgeType>(initialEdges);
const [snapToGridEnabled, setSnapToGridEnabled] = useState(false);
useEffect(() => {
setIsInitialLoadingNodes(true);
}, [initialTables]);
@@ -688,6 +691,9 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
setTimeout(() => setHighlightOverlappingTables(false), 600);
}, []);
const shiftPressed = useKeyPress('Shift');
const operatingSystem = getOperatingSystem();
return (
<CanvasContextMenu>
<div className="relative flex h-full">
@@ -712,6 +718,8 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
type: 'relationship-edge',
}}
panOnScroll={scrollAction === 'pan'}
snapToGrid={shiftPressed || snapToGridEnabled}
snapGrid={[20, 20]}
>
<Controls
position="top-left"
@@ -722,24 +730,57 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
>
<div className="flex flex-col items-center gap-2 md:flex-row">
{!readonly ? (
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="secondary"
className="size-8 p-1 shadow-none"
onClick={
showReorderConfirmation
}
>
<LayoutGrid className="size-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t('toolbar.reorder_diagram')}
</TooltipContent>
</Tooltip>
<>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="secondary"
className="size-8 p-1 shadow-none"
onClick={
showReorderConfirmation
}
>
<LayoutGrid className="size-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t('toolbar.reorder_diagram')}
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="secondary"
className={cn(
'size-8 p-1 shadow-none',
snapToGridEnabled ||
shiftPressed
? 'bg-pink-600 text-white hover:bg-pink-500 dark:hover:bg-pink-700 hover:text-white'
: ''
)}
onClick={() =>
setSnapToGridEnabled(
(prev) => !prev
)
}
>
<Magnet className="size-4" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t('snap_to_grid_tooltip', {
key:
operatingSystem === 'mac'
? '⇧'
: 'Shift',
})}
</TooltipContent>
</Tooltip>
</>
) : null}
<div

View File

@@ -28,6 +28,11 @@ import { useLayout } from '@/hooks/use-layout';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { useTranslation } from 'react-i18next';
import { useDialog } from '@/hooks/use-dialog';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
export interface TableListItemHeaderProps {
table: DBTable;
@@ -65,10 +70,8 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
useClickAway(inputRef, editTableName);
useKeyPressEvent('Enter', editTableName);
const enterEditMode = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.stopPropagation();
const enterEditMode = (e: React.MouseEvent) => {
e.stopPropagation();
setEditMode(true);
};
@@ -219,7 +222,7 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
return (
<div className="group flex h-11 flex-1 items-center justify-between gap-1 overflow-hidden">
<div className="flex min-w-0 flex-1">
<div className="flex min-w-0 flex-1 px-1">
{editMode ? (
<Input
ref={inputRef}
@@ -232,12 +235,24 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
className="h-7 w-full focus-visible:ring-0"
/>
) : (
<div className="truncate">
{table.name}
<span className="text-xs text-muted-foreground">
{schemaToDisplay ? ` (${schemaToDisplay})` : ''}
</span>
</div>
<Tooltip>
<TooltipTrigger asChild>
<div
onDoubleClick={enterEditMode}
className="text-editable truncate px-2 py-0.5"
>
{table.name}
<span className="text-xs text-muted-foreground">
{schemaToDisplay
? ` (${schemaToDisplay})`
: ''}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
{t('tool_tips.double_click_to_edit')}
</TooltipContent>
</Tooltip>
)}
</div>
<div className="flex flex-row-reverse">

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Label } from '@/components/label/label';
import { Button } from '@/components/button/button';
import { Check, Pencil } from 'lucide-react';
import { Check } from 'lucide-react';
import { Input } from '@/components/input/input';
import { useChartDB } from '@/hooks/use-chartdb';
import { useClickAway, useKeyPressEvent } from 'react-use';
@@ -10,6 +10,11 @@ import { DiagramIcon } from '@/components/diagram-icon/diagram-icon';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { labelVariants } from '@/components/label/label-variants';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
export interface DiagramNameProps {}
@@ -39,54 +44,73 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
useKeyPressEvent('Enter', editDiagramName);
const enterEditMode = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
event: React.MouseEvent<HTMLHeadingElement, MouseEvent>
) => {
event.stopPropagation();
setEditMode(true);
};
return (
<>
<DiagramIcon diagram={currentDiagram} />
<div className="flex">
{isDesktop ? <Label>{t('diagrams')}/</Label> : null}
</div>
<div className="flex flex-row items-center gap-1">
{editMode ? (
<>
<Input
ref={inputRef}
autoFocus
type="text"
placeholder={diagramName}
value={editedDiagramName}
onClick={(e) => e.stopPropagation()}
onChange={(e) =>
setEditedDiagramName(e.target.value)
}
className="ml-1 h-7 focus-visible:ring-0"
/>
<Button
variant="ghost"
className="hidden size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 group-hover:flex dark:text-slate-400 dark:hover:text-slate-300"
onClick={editDiagramName}
>
<Check />
</Button>
</>
) : (
<>
<h1 className={cn(labelVariants())}>{diagramName}</h1>
<Button
variant="ghost"
className="hidden size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 group-hover:flex dark:text-slate-400 dark:hover:text-slate-300"
onClick={enterEditMode}
>
<Pencil />
</Button>
</>
<div className="group">
<div
className={cn(
'flex flex-1 flex-row items-center justify-center px-2 py-1',
{
'text-editable': !editMode,
}
)}
>
<DiagramIcon diagram={currentDiagram} />
<div className="flex">
{isDesktop ? <Label>{t('diagrams')}/</Label> : null}
</div>
<div className="flex flex-row items-center gap-1">
{editMode ? (
<>
<Input
ref={inputRef}
autoFocus
type="text"
placeholder={diagramName}
value={editedDiagramName}
onClick={(e) => e.stopPropagation()}
onChange={(e) =>
setEditedDiagramName(e.target.value)
}
className="ml-1 h-7 focus-visible:ring-0"
/>
<Button
variant="ghost"
className="flex size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300"
onClick={editDiagramName}
>
<Check />
</Button>
</>
) : (
<>
<Tooltip>
<TooltipTrigger asChild>
<h1
className={cn(
labelVariants(),
'group-hover:underline'
)}
onDoubleClick={(e) => {
enterEditMode(e);
}}
>
{diagramName}
</h1>
</TooltipTrigger>
<TooltipContent>
{t('tool_tips.double_click_to_edit')}
</TooltipContent>
</Tooltip>
</>
)}
</div>
</div>
</>
</div>
);
};

View File

@@ -48,6 +48,8 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
openImportDatabaseDialog,
showAlert,
openExportImageDialog,
openExportDiagramDialog,
openImportDiagramDialog,
} = useDialog();
const { setTheme, theme } = useTheme();
const { hideSidePanel, isSidePanelShowed, showSidePanel } = useLayout();
@@ -204,9 +206,9 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
);
return (
<nav className="flex h-20 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 justify-between gap-x-3 md:justify-normal">
<div className="flex py-[10px] font-primary md:items-center md:py-0">
<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 items-center justify-between pt-[8px] font-primary md:py-[10px]">
<a
href="https://chartdb.io"
className="cursor-pointer"
@@ -222,408 +224,374 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
className="h-4 max-w-fit"
/>
</a>
{!isDesktop ? (
<div className="flex items-center">{renderStars()}</div>
) : null}
</div>
<div>
<Menubar className="border-none shadow-none">
<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,
})
<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
}
>
{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,
})
</MenubarShortcut>
</MenubarItem>
<MenubarItem onClick={updateDiagramUpdatedAt}>
{t('menu.file.save')}
<MenubarShortcut>
{
keyboardShortcutsForOS[
KeyboardShortcutAction.SAVE_DIAGRAM
].keyCombinationLabel
}
>
{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')}
</MenubarItem>
<MenubarSeparator />
<MenubarItem onClick={showOrHideCardinality}>
{showCardinality
? t('menu.view.hide_cardinality')
: t('menu.view.show_cardinality')}
</MenubarItem>
<MenubarItem onClick={showOrHideDependencies}>
{showDependenciesOnCanvas
? t('menu.view.hide_dependencies')
: t('menu.view.show_dependencies')}
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.view.zoom_on_scroll')}
</MenubarSubTrigger>
<MenubarSubContent>
</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')}
</MenubarItem>
<MenubarSeparator />
<MenubarItem onClick={showOrHideCardinality}>
{showCardinality
? t('menu.view.hide_cardinality')
: t('menu.view.show_cardinality')}
</MenubarItem>
<MenubarItem onClick={showOrHideDependencies}>
{showDependenciesOnCanvas
? t('menu.view.hide_dependencies')
: t('menu.view.show_dependencies')}
</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>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.view.change_language')}
</MenubarSubTrigger>
<MenubarSubContent>
{languages.map((language) => (
<MenubarCheckboxItem
checked={scrollAction === 'zoom'}
key={language.code}
onClick={() =>
setScrollAction('zoom')
changeLanguage(language.code)
}
checked={
i18n.language === language.code
}
>
{t('zoom.on')}
{language.name}
</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>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.view.change_language')}
</MenubarSubTrigger>
<MenubarSubContent>
{languages.map((language) => (
<MenubarCheckboxItem
key={language.code}
onClick={() =>
changeLanguage(
language.code
)
}
checked={
i18n.language ===
language.code
}
>
{language.name}
</MenubarCheckboxItem>
))}
</MenubarSubContent>
</MenubarSub>
</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>
))}
</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>
{isDesktop ? (
<>
<div className="group flex flex-1 flex-row items-center justify-center">
<DiagramName />
</div>
<DiagramName />
<div className="hidden flex-1 items-center justify-end gap-2 sm:flex">
<LastSaved />
{renderStars()}
</div>
</>
) : (
<div className="flex flex-1 flex-row justify-between gap-2">
<div className="group flex flex-1 flex-row items-center">
<DiagramName />
</div>
<div className="flex items-center">
<LastSaved />
</div>
<div className="flex items-center">{renderStars()}</div>
<div className="flex flex-1 justify-center pb-2 pt-1">
<DiagramName />
</div>
)}
</nav>

View File

@@ -139,7 +139,7 @@ const TemplatesPageComponent: React.FC = () => {
</h4>
{allTags ? (
<ListMenu
className="mt-1 w-44 shrink-0"
className="mt-1 shrink-0"
items={allTags.map((currentTag) => ({
title: currentTag,
href: `/templates/tags/${currentTag}`,

View File

@@ -1,90 +1,16 @@
import type { Diagram } from '@/lib/domain/diagram';
import type { Template } from './templates-data';
import { generateId, removeDups } from '@/lib/utils';
import type { DBTable } from '@/lib/domain/db-table';
import type { DBField } from '@/lib/domain/db-field';
import type { DBIndex } from '@/lib/domain/db-index';
import type { DBRelationship } from '@/lib/domain/db-relationship';
import type { DBDependency } from '@/lib/domain/db-dependency';
import { cloneDiagram, generateId, removeDups } from '@/lib/utils';
export const convertTemplateToNewDiagram = (template: Template): Diagram => {
// const diagramId = generateDiagramId();
const diagramId = template.diagram.id;
const idsMap = new Map<string, string>();
template.diagram.tables?.forEach((table) => {
idsMap.set(table.id, generateId());
table.fields.forEach((field) => {
idsMap.set(field.id, generateId());
});
table.indexes.forEach((index) => {
idsMap.set(index.id, generateId());
});
});
template.diagram.relationships?.forEach((relationship) => {
idsMap.set(relationship.id, generateId());
});
template.diagram.dependencies?.forEach((dependency) => {
idsMap.set(dependency.id, generateId());
});
const getNewId = (id: string) => {
const newId = idsMap.get(id);
if (!newId) {
throw new Error(`Id not found for ${id}`);
}
return newId;
};
const tables: DBTable[] =
template.diagram.tables?.map((table) => {
const newTable: DBTable = { ...table, id: getNewId(table.id) };
newTable.fields = table.fields.map(
(field): DBField => ({
...field,
id: getNewId(field.id),
})
);
newTable.indexes = table.indexes.map(
(index): DBIndex => ({
...index,
id: getNewId(index.id),
})
);
return newTable;
}) ?? [];
const relationships: DBRelationship[] =
template.diagram.relationships?.map(
(relationship): DBRelationship => ({
...relationship,
id: getNewId(relationship.id),
sourceTableId: getNewId(relationship.sourceTableId),
targetTableId: getNewId(relationship.targetTableId),
sourceFieldId: getNewId(relationship.sourceFieldId),
targetFieldId: getNewId(relationship.targetFieldId),
})
) ?? [];
const dependencies: DBDependency[] =
template.diagram.dependencies?.map(
(dependency): DBDependency => ({
...dependency,
id: getNewId(dependency.id),
dependentTableId: getNewId(dependency.dependentTableId),
tableId: getNewId(dependency.tableId),
})
) ?? [];
const clonedDiagram = cloneDiagram(template.diagram, generateId);
return {
...template.diagram,
...clonedDiagram,
id: diagramId,
dependencies,
relationships,
tables,
};
};

View File

@@ -4,6 +4,16 @@ import { visualNovelDb } from './templates/visual-novel-db';
import { airbnbDb } from './templates/airbnb-db';
import { wordpressDb } from './templates/wordpress-db';
import { pokemonDb } from './templates/pokemon-db';
import { adonisAclDb } from './templates/adonis-acl-db';
import { akauntingDb } from './templates/akaunting-db';
import { djangoDb } from './templates/django-db';
import { twitterDb } from './templates/twitter-db';
import { laravelDb } from './templates/laravel-db';
import { laravelSparkDb } from './templates/laravel-spark-db';
import { voyagerDb } from './templates/voyager-db';
import { koelDb } from './templates/koel-db';
import { laravelPermissionDb } from './templates/laravel-permission-db';
import { gravityDb } from './templates/gravity-db';
export interface Template {
slug: string;
@@ -20,8 +30,18 @@ export interface Template {
export const templates: Template[] = [
employeeDb,
visualNovelDb,
pokemonDb,
airbnbDb,
wordpressDb,
pokemonDb,
djangoDb,
laravelDb,
twitterDb,
visualNovelDb,
adonisAclDb,
akauntingDb,
laravelSparkDb,
voyagerDb,
koelDb,
laravelPermissionDb,
gravityDb,
];

View File

@@ -0,0 +1,667 @@
import { DatabaseType } from '@/lib/domain/database-type';
import type { Template } from '../templates-data';
import image from '@/assets/templates/adonis-acl.png';
import imageDark from '@/assets/templates/adonis-acl-dark.png';
export const adonisAclDb: Template = {
slug: 'adonis-acl-database',
name: 'Adonis Acl Database',
shortDescription: 'Role based permissions',
description:
'Adonis ACL adds role based permissions to built in Auth System of Adonis Framework.',
image,
imageDark,
tags: ['Postgres', 'Open Source', 'Node.js'],
featured: true,
url: 'https://github.com/enniel/adonis-acl',
diagram: {
id: 'adonis_acl_db',
name: 'adonis-acl-database',
createdAt: new Date(),
updatedAt: new Date(),
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
id: '4tfy7o1t3ln1373iyxtzpz8t5',
name: 'permission_user',
schema: 'public',
x: 441.0506024096385,
y: -94.22037476830401,
fields: [
{
id: 'kx4al18p0mzdkcnr1lpo6h75n',
name: 'id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: true,
unique: true,
nullable: false,
default: "nextval('permission_user_id_seq'::regclass)",
createdAt: Date.now(),
},
{
id: 'zjbpw5umqxilj7urdfmbsc3oy',
name: 'permission_id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'inmxxha9vdpnfeupr788gwfw1',
name: 'user_id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'vimawd57a8ft226yznzhts8gh',
name: 'created_at',
type: {
id: 'timestamp_with_time_zone',
name: 'timestamp with time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: '2fzwubtg6tkpglg9rvej86k73',
name: 'updated_at',
type: {
id: 'timestamp_with_time_zone',
name: 'timestamp with time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
],
indexes: [
{
id: '1usshdeu8pbxa3l4itt814ldc',
name: 'permission_user_permission_id_user_id_key',
unique: true,
fieldIds: [
'zjbpw5umqxilj7urdfmbsc3oy',
'inmxxha9vdpnfeupr788gwfw1',
],
createdAt: Date.now(),
},
{
id: 'o4zsinm8juyrgd318f7thopu2',
name: 'idx_permission_user_permission_id',
unique: false,
fieldIds: ['zjbpw5umqxilj7urdfmbsc3oy'],
createdAt: Date.now(),
},
{
id: 'oq2qxxzd2n8mphs4lh7jyzjxk',
name: 'idx_permission_user_user_id',
unique: false,
fieldIds: ['inmxxha9vdpnfeupr788gwfw1'],
createdAt: Date.now(),
},
{
id: 'i60fxthzz9o372y4tbhb5cuhm',
name: 'permission_user_pkey',
unique: true,
fieldIds: ['kx4al18p0mzdkcnr1lpo6h75n'],
createdAt: Date.now(),
},
],
color: '#4dee8a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: '6v24e5bdz4vi9e757spbcea6d',
name: 'role_user',
schema: 'public',
x: 374.94791473586656,
y: 337.7814643188136,
fields: [
{
id: 'vaaupx4bcejm1kqx4jze9wytx',
name: 'id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: true,
unique: true,
nullable: false,
default: "nextval('role_user_id_seq'::regclass)",
createdAt: Date.now(),
},
{
id: '29dylthvlf2v5vh41dp1kyxbr',
name: 'role_id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'g4i6e70u835inwwcjs9tdiwpf',
name: 'user_id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: '3tfwqm0igug2j0vm6sv6nt6i5',
name: 'created_at',
type: {
id: 'timestamp_with_time_zone',
name: 'timestamp with time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'ho7doyax6cr77qpvsboz6jymz',
name: 'updated_at',
type: {
id: 'timestamp_with_time_zone',
name: 'timestamp with time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
],
indexes: [
{
id: '7ngkqnaew4m8h3ejoxdercox1',
name: 'role_user_pkey',
unique: true,
fieldIds: ['vaaupx4bcejm1kqx4jze9wytx'],
createdAt: Date.now(),
},
{
id: 'dz3poedtl3z6wkwanxgv98zxi',
name: 'role_user_role_id_user_id_key',
unique: true,
fieldIds: [
'29dylthvlf2v5vh41dp1kyxbr',
'g4i6e70u835inwwcjs9tdiwpf',
],
createdAt: Date.now(),
},
{
id: 'l8j5np655am15rgrfja4qzpdt',
name: 'idx_role_user_user_id',
unique: false,
fieldIds: ['g4i6e70u835inwwcjs9tdiwpf'],
createdAt: Date.now(),
},
{
id: 'ie7wixunsiwfzp8udcpxmsj7p',
name: 'idx_role_user_role_id',
unique: false,
fieldIds: ['29dylthvlf2v5vh41dp1kyxbr'],
createdAt: Date.now(),
},
],
color: '#ffe374',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: 'i298i343vjq652fdswhwaysr7',
name: 'users',
schema: 'public',
x: 90.5794253938833,
y: 140.8111214087117,
fields: [
{
id: 'zs7cvtl01rtle7xts2mwqavg3',
name: 'id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: true,
unique: true,
nullable: false,
default: "nextval('users_id_seq'::regclass)",
createdAt: Date.now(),
},
],
indexes: [
{
id: 'vxdig4dza0x7d7k70a8k6m7ap',
name: 'users_pkey',
unique: true,
fieldIds: ['zs7cvtl01rtle7xts2mwqavg3'],
createdAt: Date.now(),
},
],
color: '#8a61f5',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: 'mueqxc4u5k58cz26hqu1ku7sx',
name: 'permission_role',
schema: 'public',
x: 1283.7775718257649,
y: 56.92159406858201,
fields: [
{
id: '8s76u8u0ivylxlhhya7geg3t7',
name: 'id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: true,
unique: true,
nullable: false,
default: "nextval('permission_role_id_seq'::regclass)",
createdAt: Date.now(),
},
{
id: 'jkyfn71uasmxjj22bsxxug6wb',
name: 'permission_id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'i1y14qiro8bqu0nhaj4quyd9p',
name: 'role_id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 's86pfp5iewn9mwci4m75oro0j',
name: 'created_at',
type: {
id: 'timestamp_with_time_zone',
name: 'timestamp with time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'v10o7negcp8tliaiie8iste38',
name: 'updated_at',
type: {
id: 'timestamp_with_time_zone',
name: 'timestamp with time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
],
indexes: [
{
id: '4uhhvnt29q91z55tfpr1uib38',
name: 'permission_role_permission_id_role_id_key',
unique: true,
fieldIds: [
'jkyfn71uasmxjj22bsxxug6wb',
'i1y14qiro8bqu0nhaj4quyd9p',
],
createdAt: Date.now(),
},
{
id: '8o6apfea6ifduymoxd0teibi0',
name: 'idx_permission_role_role_id',
unique: false,
fieldIds: ['i1y14qiro8bqu0nhaj4quyd9p'],
createdAt: Date.now(),
},
{
id: 'lahuo79xsq2brj2dni67hpip1',
name: 'idx_permission_role_permission_id',
unique: false,
fieldIds: ['jkyfn71uasmxjj22bsxxug6wb'],
createdAt: Date.now(),
},
{
id: '6hqnbyj0uzzxulurr56uak38x',
name: 'permission_role_pkey',
unique: true,
fieldIds: ['8s76u8u0ivylxlhhya7geg3t7'],
createdAt: Date.now(),
},
],
color: '#8eb7ff',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: 't6c4vthncqe0gxza814wuzcjl',
name: 'permissions',
schema: 'public',
x: 877.67859128823,
y: -156.20315106580168,
fields: [
{
id: 'iywp08732p9q7pltm6pqqmmk6',
name: 'id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: true,
unique: true,
nullable: false,
default: "nextval('permissions_id_seq'::regclass)",
createdAt: Date.now(),
},
{
id: 'z9p6x72tx45bfv0iwq64jfobp',
name: 'slug',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: false,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: 'ipix7t0lz3pn78leic3s9xrjy',
name: 'name',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'kli69miojwm6e0hseacy8o5hy',
name: 'description',
type: {
id: 'text',
name: 'text',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'wjd77kdm6ecdythpwarpvu658',
name: 'created_at',
type: {
id: 'timestamp_with_time_zone',
name: 'timestamp with time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'h4ir4c21y2uibomt5twrdcqgi',
name: 'updated_at',
type: {
id: 'timestamp_with_time_zone',
name: 'timestamp with time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
],
indexes: [
{
id: '7wofvazg9gu4z15gazvnu29d2',
name: 'permissions_pkey',
unique: true,
fieldIds: ['iywp08732p9q7pltm6pqqmmk6'],
createdAt: Date.now(),
},
{
id: 'upxsqobs4g597kbbpfmyqnnkh',
name: 'permissions_slug_key',
unique: true,
fieldIds: ['z9p6x72tx45bfv0iwq64jfobp'],
createdAt: Date.now(),
},
],
color: '#b067e9',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: 'wigyqmreqg7oo1361a4v4tf1o',
name: 'roles',
schema: 'public',
x: 837.7233549582947,
y: 272.313623725672,
fields: [
{
id: 'bz9n3ntxo22bb7mu9t9wwv67x',
name: 'id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: true,
unique: true,
nullable: false,
default: "nextval('roles_id_seq'::regclass)",
createdAt: Date.now(),
},
{
id: 'wmrytzy38hipx342qjznsy8zx',
name: 'slug',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: false,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: 'hk3g5yid08iozkvooc4htuabs',
name: 'name',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'h0gp2hi6nf14dlw0wo14h1gyn',
name: 'description',
type: {
id: 'text',
name: 'text',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'd2kwx6qx2cd7epm7m5pikp718',
name: 'created_at',
type: {
id: 'timestamp_with_time_zone',
name: 'timestamp with time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'xq1udcys21dn8rwn6vatyy47a',
name: 'updated_at',
type: {
id: 'timestamp_with_time_zone',
name: 'timestamp with time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
],
indexes: [
{
id: 'ngrpk9ugw0en5ej20jst3fdj2',
name: 'roles_pkey',
unique: true,
fieldIds: ['bz9n3ntxo22bb7mu9t9wwv67x'],
createdAt: Date.now(),
},
{
id: '4od6wsi5v258oflij1uw3d43b',
name: 'roles_slug_key',
unique: true,
fieldIds: ['wmrytzy38hipx342qjznsy8zx'],
createdAt: Date.now(),
},
],
color: '#ff6363',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
],
relationships: [
{
id: '47fwpcozxyz27e30pm1ib56zm',
name: 'permission_user_user_id_fkey',
sourceSchema: 'public',
targetSchema: 'public',
sourceTableId: '4tfy7o1t3ln1373iyxtzpz8t5',
targetTableId: 'i298i343vjq652fdswhwaysr7',
sourceFieldId: 'inmxxha9vdpnfeupr788gwfw1',
targetFieldId: 'zs7cvtl01rtle7xts2mwqavg3',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: '9s8jc4cycjn9ql94ncqjzrbec',
name: 'permission_user_permission_id_fkey',
sourceSchema: 'public',
targetSchema: 'public',
sourceTableId: '4tfy7o1t3ln1373iyxtzpz8t5',
targetTableId: 't6c4vthncqe0gxza814wuzcjl',
sourceFieldId: 'zjbpw5umqxilj7urdfmbsc3oy',
targetFieldId: 'iywp08732p9q7pltm6pqqmmk6',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'dmxvyh7b90codoe0aosraa94h',
name: 'role_user_user_id_fkey',
sourceSchema: 'public',
targetSchema: 'public',
sourceTableId: '6v24e5bdz4vi9e757spbcea6d',
targetTableId: 'i298i343vjq652fdswhwaysr7',
sourceFieldId: 'g4i6e70u835inwwcjs9tdiwpf',
targetFieldId: 'zs7cvtl01rtle7xts2mwqavg3',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'f0t9gyglmjmnd5apytli0p9ip',
name: 'role_user_role_id_fkey',
sourceSchema: 'public',
targetSchema: 'public',
sourceTableId: '6v24e5bdz4vi9e757spbcea6d',
targetTableId: 'wigyqmreqg7oo1361a4v4tf1o',
sourceFieldId: '29dylthvlf2v5vh41dp1kyxbr',
targetFieldId: 'bz9n3ntxo22bb7mu9t9wwv67x',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'hpvk9671hn872b3zr4p5fkisr',
name: 'permission_role_role_id_fkey',
sourceSchema: 'public',
targetSchema: 'public',
sourceTableId: 'mueqxc4u5k58cz26hqu1ku7sx',
targetTableId: 'wigyqmreqg7oo1361a4v4tf1o',
sourceFieldId: 'i1y14qiro8bqu0nhaj4quyd9p',
targetFieldId: 'bz9n3ntxo22bb7mu9t9wwv67x',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'lvv365mewcakaozzmwv8qo78c',
name: 'permission_role_permission_id_fkey',
sourceSchema: 'public',
targetSchema: 'public',
sourceTableId: 'mueqxc4u5k58cz26hqu1ku7sx',
targetTableId: 't6c4vthncqe0gxza814wuzcjl',
sourceFieldId: 'jkyfn71uasmxjj22bsxxug6wb',
targetFieldId: 'iywp08732p9q7pltm6pqqmmk6',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
],
dependencies: [],
},
};

View File

@@ -4,17 +4,17 @@ import image from '@/assets/templates/airbnb.png';
import imageDark from '@/assets/templates/airbnb-dark.png';
export const airbnbDb: Template = {
slug: 'airbnb-db',
slug: 'airbnb-database',
name: 'Airbnb',
shortDescription: 'Short-term Vacation Rentals',
description: 'Example database schema diagram for Airbnb',
image,
imageDark,
tags: ['postgres', 'example apps'],
tags: ['Postgres', 'Example Apps'],
featured: true,
diagram: {
id: 'airbnb_db',
name: 'airbnb-db',
name: 'airbnb-database',
createdAt: new Date(),
updatedAt: new Date(),
databaseType: DatabaseType.POSTGRESQL,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,18 +4,18 @@ import image from '@/assets/templates/employeedb.png';
import imageDark from '@/assets/templates/employeedb-dark.png';
export const employeeDb: Template = {
slug: 'employees-db',
slug: 'employees-database',
name: 'Employees',
shortDescription: 'Employees, departments, and salaries',
description:
'A schema for database of employees, departments, and salaries.',
image,
imageDark,
tags: ['mysql'],
tags: ['MySQL'],
featured: true,
diagram: {
id: 'employees_db',
name: 'employees-db',
name: 'employees-database',
createdAt: new Date(),
updatedAt: new Date(),
databaseType: DatabaseType.MYSQL,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,322 @@
import { DatabaseType } from '@/lib/domain/database-type';
import type { Template } from '../templates-data';
import image from '@/assets/templates/laravel-db.png';
import imageDark from '@/assets/templates/laravel-db-dark.png';
export const laravelDb: Template = {
slug: 'laravel-database',
name: 'Laravel',
shortDescription: 'PHP web framework',
description:
'With elegant syntax, simplifying web development by streamlining common tasks across projects',
image,
imageDark,
tags: ['Postgres', 'Open Source', 'Laravel', 'PHP'],
featured: true,
url: 'https://github.com/laravel/laravel',
diagram: {
id: 'laravel_db',
name: 'laravel-database',
createdAt: new Date(),
updatedAt: new Date(),
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
id: 'tzwvz1wtn84fny03tzl1sl0ho',
name: 'failed_jobs',
schema: 'public',
x: 737.7682179548738,
y: 139.69501050040327,
fields: [
{
id: 'stybn7gf7n84qhv3kizvxz13p',
name: 'id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: true,
unique: true,
nullable: false,
default: "nextval('failed_jobs_id_seq'::regclass)",
createdAt: Date.now(),
},
{
id: '9fyo2qdmsglsw7ctthmzlep1j',
name: 'connection',
type: {
id: 'text',
name: 'text',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'ut52fvqjpa0u27jd2t6t92tom',
name: 'queue',
type: {
id: 'text',
name: 'text',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'plvfv0t9im2km2v0a0qzt8udo',
name: 'payload',
type: {
id: 'text',
name: 'text',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'g5ef3gfi72xw0xeb0x6ox4b7r',
name: 'exception',
type: {
id: 'text',
name: 'text',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'ez9j04lvylcn570ne77xjsbta',
name: 'failed_at',
type: {
id: 'timestamp_with_time_zone',
name: 'timestamp with time zone',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
],
indexes: [
{
id: 'ht83plkt884y1myetskx9redz',
name: 'idx_failed_jobs_failed_at',
unique: false,
fieldIds: ['ez9j04lvylcn570ne77xjsbta'],
createdAt: Date.now(),
},
{
id: 'ztnvgd4m167ut9473h1kiofge',
name: 'failed_jobs_pkey',
unique: true,
fieldIds: ['stybn7gf7n84qhv3kizvxz13p'],
createdAt: Date.now(),
},
],
color: '#ffe374',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: 'ytrk4k6ihrm2fhkodz3t2ovzb',
name: 'migrations',
schema: 'public',
x: 146.59849058742998,
y: 208.72981137066984,
fields: [
{
id: 'uldhz9mhxosp9dqfmmpvabzta',
name: 'id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: true,
unique: true,
nullable: false,
default: "nextval('migrations_id_seq'::regclass)",
createdAt: Date.now(),
},
{
id: 'wjccp1hwlalr87ct56q35mf11',
name: 'migration',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'jomrea7wdjx21q2510vyxz0y7',
name: 'batch',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
],
indexes: [
{
id: 'tbyc6yvknlhyff4pw6ideqnde',
name: 'migrations_pkey',
unique: true,
fieldIds: ['uldhz9mhxosp9dqfmmpvabzta'],
createdAt: Date.now(),
},
],
color: '#4dee8a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: 'yw7xlj5rjjevf292enea0qbgv',
name: 'users',
schema: 'public',
x: 422.62737943432694,
y: -112.94222230620639,
fields: [
{
id: '79ny0my0rv9ztzg7ww735rmrj',
name: 'id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: true,
unique: true,
nullable: false,
default: "nextval('users_id_seq'::regclass)",
createdAt: Date.now(),
},
{
id: 'lzzou36zm6l6r4vmizdf24q8x',
name: 'name',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'lh4oc6cam3kb161f1thohs95j',
name: 'email',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: false,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: 'luulms1e4cbai05978ufc0xdi',
name: 'email_verified_at',
type: {
id: 'timestamp_with_time_zone',
name: 'timestamp with time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'xai5t98b0syebn21hkdvcf6sg',
name: 'password',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'b93oi649bi053g51mfobb94mq',
name: 'remember_token',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: '76dz2udxcbudh5c8sdmauwsas',
name: 'created_at',
type: {
id: 'timestamp_with_time_zone',
name: 'timestamp with time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: '267qdi3o75bdg9lsf5wuhku9s',
name: 'updated_at',
type: {
id: 'timestamp_with_time_zone',
name: 'timestamp with time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
],
indexes: [
{
id: 'uc19mnyl8xllnfjoaz3qkesi5',
name: 'idx_users_email',
unique: false,
fieldIds: ['lh4oc6cam3kb161f1thohs95j'],
createdAt: Date.now(),
},
{
id: 'tgx4fsgm3j6q793g7ttfdr8em',
name: 'users_email_key',
unique: true,
fieldIds: ['lh4oc6cam3kb161f1thohs95j'],
createdAt: Date.now(),
},
{
id: 'kmp3sogww6j7lovhrv25dolxp',
name: 'users_pkey',
unique: true,
fieldIds: ['79ny0my0rv9ztzg7ww735rmrj'],
createdAt: Date.now(),
},
],
color: '#ff6b8a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
],
relationships: [],
dependencies: [],
},
};

View File

@@ -0,0 +1,488 @@
import { DatabaseType } from '@/lib/domain/database-type';
import type { Template } from '../templates-data';
import image from '@/assets/templates/laravel-permission-db.png';
import imageDark from '@/assets/templates/laravel-permission-db-dark.png';
export const laravelPermissionDb: Template = {
slug: 'laravel-permission-database',
name: 'Laravel Permission',
shortDescription: 'Roles and Permission For Laravel',
description:
'Associate users with roles and permissions (Laravel-Permission on github)',
image,
imageDark,
tags: ['Postgres', 'Open Source', 'Laravel', 'PHP'],
featured: true,
url: 'https://github.com/spatie/laravel-permission',
diagram: {
id: 'laravel_permission_db',
name: 'laravel-permission-database',
createdAt: new Date(),
updatedAt: new Date(),
databaseType: DatabaseType.POSTGRESQL,
tables: [
{
id: '2620qxtifbx20r7mbpjeujv4j',
name: 'roles',
schema: 'public',
x: 464.87710843373475,
y: 100,
fields: [
{
id: 'poke4psscnh9kcnjhksaaujnf',
name: 'id',
type: {
id: 'bigint',
name: 'bigint',
},
primaryKey: true,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: 'zmecmt42pnfcvm60fymzzvb4a',
name: 'name',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'qaq3tbo01ijru1x03f3kg1evr',
name: 'guard_name',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'y9d6p7p7easu8soxbu6t3eydc',
name: 'created_at',
type: {
id: 'timestamp_without_time_zone',
name: 'timestamp without time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: 'qoci8vbx8jvoqmdtnmpany9gb',
name: 'updated_at',
type: {
id: 'timestamp_without_time_zone',
name: 'timestamp without time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
],
indexes: [
{
id: '1en34nunvxrt4275dsgcvww4l',
name: 'roles_pkey',
unique: true,
fieldIds: ['poke4psscnh9kcnjhksaaujnf'],
createdAt: Date.now(),
},
{
id: 'spqunz0hxv2bg874w0m1y36r4',
name: 'unique_role_name',
unique: true,
fieldIds: [
'zmecmt42pnfcvm60fymzzvb4a',
'qaq3tbo01ijru1x03f3kg1evr',
],
createdAt: Date.now(),
},
],
color: '#4dee8a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: '4bfk337wvmggh25gtuv9xu2l2',
name: 'model_has_permissions',
schema: 'public',
x: -272.1075069508805,
y: -78.51195551436513,
fields: [
{
id: 'o6hhvn4ygrxnqnrzt3078st9i',
name: 'permission_id',
type: {
id: 'bigint',
name: 'bigint',
},
primaryKey: true,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'zxtynvat7f4oxf2c1zpqal1fk',
name: 'model_type',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: true,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'r5bioxa56ghm7mlk4r9x9a5o6',
name: 'model_id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: true,
unique: false,
nullable: false,
createdAt: Date.now(),
},
],
indexes: [
{
id: 'bf1u0pks34k83iozivuhjvwha',
name: 'idx_permission_model_type',
unique: false,
fieldIds: [
'o6hhvn4ygrxnqnrzt3078st9i',
'zxtynvat7f4oxf2c1zpqal1fk',
],
createdAt: Date.now(),
},
{
id: '8vdxiolssrpakzuv7twx3yk5o',
name: 'model_has_permissions_pkey',
unique: true,
fieldIds: [
'o6hhvn4ygrxnqnrzt3078st9i',
'r5bioxa56ghm7mlk4r9x9a5o6',
'zxtynvat7f4oxf2c1zpqal1fk',
],
createdAt: Date.now(),
},
{
id: 'l00lk7nit3rr3rja4jzwpbg9z',
name: 'idx_model_id_type',
unique: false,
fieldIds: [
'r5bioxa56ghm7mlk4r9x9a5o6',
'zxtynvat7f4oxf2c1zpqal1fk',
],
createdAt: Date.now(),
},
],
color: '#ff6b8a',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: 'd1ehpum03i9bnnfawx9rq3zq7',
name: 'model_has_roles',
schema: 'public',
x: 33.680815569972424,
y: 111.89050046339204,
fields: [
{
id: '6jgasj9vo69w60b5237un1zx3',
name: 'role_id',
type: {
id: 'bigint',
name: 'bigint',
},
primaryKey: true,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'hghqz8b4rcwkmp90xzxgcvtnq',
name: 'model_type',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: true,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'xmqnxspc9au3kr8ii4a58xm9y',
name: 'model_id',
type: {
id: 'integer',
name: 'integer',
},
primaryKey: true,
unique: false,
nullable: false,
createdAt: Date.now(),
},
],
indexes: [
{
id: '6n1klb9uu2h1cla1fvzrjbi95',
name: 'idx_model_id_type_roles',
unique: false,
fieldIds: [
'xmqnxspc9au3kr8ii4a58xm9y',
'hghqz8b4rcwkmp90xzxgcvtnq',
],
createdAt: Date.now(),
},
{
id: '9h1sf1k29qbz3sv2itdfrk4dk',
name: 'model_has_roles_pkey',
unique: true,
fieldIds: [
'6jgasj9vo69w60b5237un1zx3',
'xmqnxspc9au3kr8ii4a58xm9y',
'hghqz8b4rcwkmp90xzxgcvtnq',
],
createdAt: Date.now(),
},
{
id: 'c86wjo6kj2ovehsxxa2enllp9',
name: 'idx_role_model_type',
unique: false,
fieldIds: [
'6jgasj9vo69w60b5237un1zx3',
'hghqz8b4rcwkmp90xzxgcvtnq',
],
createdAt: Date.now(),
},
],
color: '#c05dcf',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: 'n1ybvm5upr14p0628603p30f7',
name: 'role_has_permissions',
schema: 'public',
x: 501.8594995366079,
y: -188.2224281742354,
fields: [
{
id: '5gweun1ja59v4j0iwew7qtmug',
name: 'permission_id',
type: {
id: 'bigint',
name: 'bigint',
},
primaryKey: true,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'ax62xq5y074x11xlo5fgilbxw',
name: 'role_id',
type: {
id: 'bigint',
name: 'bigint',
},
primaryKey: true,
unique: false,
nullable: false,
createdAt: Date.now(),
},
],
indexes: [
{
id: '5qpx9oexxq8hpeth52284mcpo',
name: 'role_has_permissions_pkey',
unique: true,
fieldIds: [
'5gweun1ja59v4j0iwew7qtmug',
'ax62xq5y074x11xlo5fgilbxw',
],
createdAt: Date.now(),
},
{
id: 'upkdmo20fjun4mliamza4hddh',
name: 'idx_permission_role',
unique: false,
fieldIds: [
'5gweun1ja59v4j0iwew7qtmug',
'ax62xq5y074x11xlo5fgilbxw',
],
createdAt: Date.now(),
},
],
color: '#ffe374',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
{
id: 's6ocy1b7i68o367j8f3jpp0x9',
name: 'permissions',
schema: 'public',
x: 59.09101019462469,
y: -303.5113994439296,
fields: [
{
id: 'xbh22rbm01xf67vlb4iob7s2m',
name: 'id',
type: {
id: 'bigint',
name: 'bigint',
},
primaryKey: true,
unique: true,
nullable: false,
createdAt: Date.now(),
},
{
id: 'p6w2uqym56yh11li8j7ie80y6',
name: 'name',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'kdg8mlkz9xv23yigbev3548o1',
name: 'guard_name',
type: {
id: 'character_varying',
name: 'character varying',
},
primaryKey: false,
unique: false,
nullable: false,
createdAt: Date.now(),
},
{
id: 'lnuejviuvqhbe5xis5lx6g88u',
name: 'created_at',
type: {
id: 'timestamp_without_time_zone',
name: 'timestamp without time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
{
id: '6e7k9krkbuw3mu50j1l2j1a5p',
name: 'updated_at',
type: {
id: 'timestamp_without_time_zone',
name: 'timestamp without time zone',
},
primaryKey: false,
unique: false,
nullable: true,
createdAt: Date.now(),
},
],
indexes: [
{
id: 'h0nxliia478xydaxwh3ez348i',
name: 'unique_permission_name',
unique: true,
fieldIds: [
'p6w2uqym56yh11li8j7ie80y6',
'kdg8mlkz9xv23yigbev3548o1',
],
createdAt: Date.now(),
},
{
id: '6kwd6xdwm5ffff0e6nc0pth9d',
name: 'permissions_pkey',
unique: true,
fieldIds: ['xbh22rbm01xf67vlb4iob7s2m'],
createdAt: Date.now(),
},
],
color: '#ff9f74',
isView: false,
isMaterializedView: false,
createdAt: Date.now(),
},
],
relationships: [
{
id: 'm90ygayyu34f13rsqhkuntip6',
name: 'model_has_permissions_permission_id_fkey',
sourceSchema: 'public',
targetSchema: 'public',
sourceTableId: '4bfk337wvmggh25gtuv9xu2l2',
targetTableId: 's6ocy1b7i68o367j8f3jpp0x9',
sourceFieldId: 'o6hhvn4ygrxnqnrzt3078st9i',
targetFieldId: 'xbh22rbm01xf67vlb4iob7s2m',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'pqohktmdce5leh9xy89an1dvu',
name: 'role_has_permissions_role_id_fkey',
sourceSchema: 'public',
targetSchema: 'public',
sourceTableId: 'n1ybvm5upr14p0628603p30f7',
targetTableId: '2620qxtifbx20r7mbpjeujv4j',
sourceFieldId: 'ax62xq5y074x11xlo5fgilbxw',
targetFieldId: 'poke4psscnh9kcnjhksaaujnf',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'qyuoq0fn7j2ldvt9952o5quom',
name: 'model_has_roles_role_id_fkey',
sourceSchema: 'public',
targetSchema: 'public',
sourceTableId: 'd1ehpum03i9bnnfawx9rq3zq7',
targetTableId: '2620qxtifbx20r7mbpjeujv4j',
sourceFieldId: '6jgasj9vo69w60b5237un1zx3',
targetFieldId: 'poke4psscnh9kcnjhksaaujnf',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
{
id: 'td5vwiqogewiwky7uexog5hj1',
name: 'role_has_permissions_permission_id_fkey',
sourceSchema: 'public',
targetSchema: 'public',
sourceTableId: 'n1ybvm5upr14p0628603p30f7',
targetTableId: 's6ocy1b7i68o367j8f3jpp0x9',
sourceFieldId: '5gweun1ja59v4j0iwew7qtmug',
targetFieldId: 'xbh22rbm01xf67vlb4iob7s2m',
sourceCardinality: 'many',
targetCardinality: 'one',
createdAt: Date.now(),
},
],
dependencies: [],
},
};

File diff suppressed because it is too large Load Diff

View File

@@ -4,18 +4,18 @@ import image from '@/assets/templates/pokemon.png';
import imageDark from '@/assets/templates/pokemon-dark.png';
export const pokemonDb: Template = {
slug: 'pokemon-db',
slug: 'pokemon-database',
name: 'Pokemon',
shortDescription: 'Pokemon information',
description: 'Mysql Relational of 722 pokemons. 14 Tables 5 views.',
image,
imageDark,
tags: ['mysql', 'pokemon', 'example apps'],
tags: ['MySQL', 'Pokemon', 'Example Apps'],
featured: true,
url: 'https://github.com/brianr852/Pokemon-Database',
diagram: {
id: 'pokemon_db',
name: 'pokemon-db',
name: 'pokemon-database',
createdAt: new Date(),
updatedAt: new Date(),
databaseType: DatabaseType.MYSQL,

File diff suppressed because it is too large Load Diff

View File

@@ -4,19 +4,19 @@ import image from '@/assets/templates/visual-novel-db.png';
import imageDark from '@/assets/templates/visual-novel-db-dark.png';
export const visualNovelDb: Template = {
slug: 'visual-novel-db',
name: 'The Visual Novel Database',
shortDescription: 'The Visual Novel Database',
slug: 'visual-novel-database',
name: 'Visual Novel Database',
shortDescription: 'Visual Novel Database',
description:
'A comprehensive database for information about visual novels.',
image,
imageDark,
tags: ['postgres'],
tags: ['Postgres', 'Visual Novel Database'],
featured: true,
url: 'https://vndb.org',
diagram: {
id: 'visual_novel_db',
name: 'visual-novel-db',
name: 'visual-novel-database',
createdAt: new Date(),
updatedAt: new Date(),
databaseType: DatabaseType.POSTGRESQL,

File diff suppressed because it is too large Load Diff

View File

@@ -4,19 +4,19 @@ import image from '@/assets/templates/wordpress-db.png';
import imageDark from '@/assets/templates/wordpress-db-dark.png';
export const wordpressDb: Template = {
slug: 'wordpress-db',
slug: 'wordpress-database',
name: 'WordPress',
shortDescription: 'An open-source PHP Content Management System',
description:
'(CMS) ideal for building websites, blogs, or apps. Flexible, customizable, and designed for developers to expand',
image,
imageDark,
tags: ['mysql', 'open source', 'WordPress', 'php'],
tags: ['MySQL', 'Open Source', 'WordPress', 'PHP'],
featured: true,
url: 'https://wordpress.org',
diagram: {
id: 'wordpress_db',
name: 'wordpress-db',
name: 'wordpress-database',
createdAt: new Date(),
updatedAt: new Date(),
databaseType: DatabaseType.MYSQL,

View File

@@ -77,12 +77,17 @@ module.exports = {
'50%': { transform: 'scale(1.05)' },
'100%': { transform: 'scale(1)' },
},
blink: {
'0%, 100%': { opacity: '1' },
'50%': { opacity: '0' },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
scale: 'scale 1s ease-in-out 1',
'scale-2': 'scale-2 1s ease-in-out 2',
blink: 'blink 1s infinite',
},
},
},