Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
42c159605d | ||
|
78c427f38e | ||
|
bae74d1693 | ||
|
123f40f39e | ||
|
e3129cec74 | ||
|
5508c1e084 | ||
|
9f2893319a | ||
|
125a39fb5b | ||
|
4ca1832732 | ||
|
3609bfea4d | ||
|
94a5d84fae | ||
|
85e691fcbe | ||
|
709ccff8fa | ||
|
6c7eb4609d | ||
|
2c69b08eae | ||
|
84e7591d05 | ||
|
545e8578c9 | ||
|
f1d073d053 | ||
|
20b3396ec2 | ||
|
b305be82ae | ||
|
1430d2c236 |
27
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
@@ -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 ./
|
||||
|
@@ -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
@@ -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"
|
||||
}
|
||||
|
@@ -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",
|
||||
|
BIN
src/assets/templates/adonis-acl-dark.png
Normal file
After Width: | Height: | Size: 290 KiB |
BIN
src/assets/templates/adonis-acl.png
Normal file
After Width: | Height: | Size: 322 KiB |
BIN
src/assets/templates/akaunting-dark.png
Normal file
After Width: | Height: | Size: 327 KiB |
BIN
src/assets/templates/akaunting.png
Normal file
After Width: | Height: | Size: 345 KiB |
BIN
src/assets/templates/django-db-dark.png
Normal file
After Width: | Height: | Size: 354 KiB |
BIN
src/assets/templates/django-db.png
Normal file
After Width: | Height: | Size: 389 KiB |
BIN
src/assets/templates/gravity-db-dark.png
Normal file
After Width: | Height: | Size: 382 KiB |
BIN
src/assets/templates/gravity-db.png
Normal file
After Width: | Height: | Size: 415 KiB |
BIN
src/assets/templates/koel-db-dark.png
Normal file
After Width: | Height: | Size: 369 KiB |
BIN
src/assets/templates/koel-db.png
Normal file
After Width: | Height: | Size: 402 KiB |
BIN
src/assets/templates/laravel-db-dark.png
Normal file
After Width: | Height: | Size: 168 KiB |
BIN
src/assets/templates/laravel-db.png
Normal file
After Width: | Height: | Size: 172 KiB |
BIN
src/assets/templates/laravel-permission-db-dark.png
Normal file
After Width: | Height: | Size: 215 KiB |
BIN
src/assets/templates/laravel-permission-db.png
Normal file
After Width: | Height: | Size: 216 KiB |
BIN
src/assets/templates/laravel-spark-db-dark.png
Normal file
After Width: | Height: | Size: 412 KiB |
BIN
src/assets/templates/laravel-spark-db.png
Normal file
After Width: | Height: | Size: 449 KiB |
BIN
src/assets/templates/twitter-db-dark.png
Normal file
After Width: | Height: | Size: 370 KiB |
BIN
src/assets/templates/twitter-db.png
Normal file
After Width: | Height: | Size: 404 KiB |
BIN
src/assets/templates/voyager-db-dark.png
Normal file
After Width: | Height: | Size: 401 KiB |
BIN
src/assets/templates/voyager-db.png
Normal file
After Width: | Height: | Size: 431 KiB |
62
src/components/alert/alert.tsx
Normal 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 };
|
@@ -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>
|
||||
|
168
src/components/file-uploader/file-uploader.tsx
Normal 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>
|
||||
);
|
||||
};
|
@@ -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>
|
||||
|
@@ -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,
|
||||
});
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
@@ -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
|
||||
|
110
src/dialogs/export-diagram-dialog/export-diagram-dialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
@@ -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>
|
||||
|
||||
|
129
src/dialogs/import-diagram-dialog/import-diagram-dialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
128
src/globals.css
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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({
|
||||
|
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -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: 'संपादित करने के लिए डबल-क्लिक करें',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -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
@@ -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',
|
||||
};
|
@@ -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
@@ -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',
|
||||
};
|
@@ -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
@@ -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
@@ -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',
|
||||
};
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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 => {
|
||||
|
@@ -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[]
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
|
@@ -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'
|
||||
|
@@ -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[]
|
||||
|
@@ -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,
|
||||
|
32
src/lib/export-import-utils.ts
Normal 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);
|
||||
};
|
@@ -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(),
|
||||
};
|
||||
};
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
|
@@ -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">
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
@@ -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>
|
||||
|
@@ -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}`,
|
||||
|
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -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,
|
||||
];
|
||||
|
667
src/templates-data/templates/adonis-acl-db.ts
Normal 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: [],
|
||||
},
|
||||
};
|
@@ -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,
|
||||
|
1138
src/templates-data/templates/akaunting-db.ts
Normal file
1047
src/templates-data/templates/django-db.ts
Normal 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,
|
||||
|
1102
src/templates-data/templates/gravity-db.ts
Normal file
1180
src/templates-data/templates/koel-db.ts
Normal file
322
src/templates-data/templates/laravel-db.ts
Normal 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: [],
|
||||
},
|
||||
};
|
488
src/templates-data/templates/laravel-permission-db.ts
Normal 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: [],
|
||||
},
|
||||
};
|
2158
src/templates-data/templates/laravel-spark-db.ts
Normal 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,
|
||||
|
1191
src/templates-data/templates/twitter-db.ts
Normal 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,
|
||||
|
1443
src/templates-data/templates/voyager-db.ts
Normal 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,
|
||||
|
@@ -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',
|
||||
},
|
||||
},
|
||||
},
|
||||
|