Compare commits

...

40 Commits

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

* feat: added traditional chinese language translation

---------

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

* feat: add Simplified Chinese

* fix linter Update zh_CN.ts

---------

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

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

* remove use client

* fix build

* add error parse indication

* add import from initial dialog

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

* add translations & useKeyPress

* fix build

* fix build

---------

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

* Update src/i18n/locales/ru.ts

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

* Apply suggestions from code review

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

* Refined and added missing fields to RU translation

* Refined and added missing fields to RU translation

---------

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

* chore: fix lint issues

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

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

* naming + some padding

---------

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

* add tag

* layout fixes

---------

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

* fix build

* fix tags

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2024-11-09 03:38:03 +02:00
☁️dungsil
b305be82ae fix(i18n): add korean (#362)
Co-authored-by: Jonathan Fishner <jonathanfishner11@gmail.com>
2024-11-07 16:27:24 +02:00
Jonathan Fishner
1430d2c236 fix(import json): for Check Script Result, default with quotes (#358) 2024-11-07 16:18:56 +02:00
Guy Ben-Aharon
acf8ade23c chore(main): release 1.0.1 (#320) 2024-11-07 00:05:51 +02:00
Guy Ben-Aharon
aa884b49ce fix(offline): add support when running on isolated network (#359) 2024-11-06 23:56:05 +02:00
Jonathan Fishner
acb736e44f fix(smart query): import postgres FKs (#357) 2024-11-06 22:49:32 +02:00
Guy Ben-Aharon
180886c588 fix(template): separator in case of empty url (#355) 2024-11-06 18:24:25 +02:00
Guy Ben-Aharon
e993476fad add template url (#354) 2024-11-06 18:07:02 +02:00
Guy Ben-Aharon
efaddeebb4 fix(templates): align database icon (#351) 2024-11-06 13:41:28 +02:00
Francis Chartrand
93f623a13a fix(select-box): allow using tab & space to show choices (#336)
* styles(select-box): allow using tab & space to show choices

* use code instead of key

---------

Co-authored-by: Guy Ben-Aharon <guybenah@gmail.com>
2024-11-06 13:29:25 +02:00
Guy Ben-Aharon
87a40cff61 fix: open default diagram after deleting current diagram (#350) 2024-11-06 12:01:49 +02:00
Guy Ben-Aharon
f00c9b9a03 refactor languages menu (#347) 2024-11-06 10:33:44 +02:00
Помаранча
20b2ae436c Add uk language (#338)
* Create uk.ts

added Ukrainian language. I don't know what kind of service it is, but I just helped with the translation into my native language

* Update uk.ts

now all untranslated item (2) is translated

* fix build

* add language to menu

---------

Co-authored-by: Guy Ben-Aharon <guybenah@gmail.com>
2024-11-06 10:20:14 +02:00
Guy Ben-Aharon
820a4640da template title change (#346) 2024-11-06 09:52:21 +02:00
Jonathan Fishner
0193853035 add pokemon database to our templates (#335) 2024-11-05 21:23:31 +02:00
Jonathan Fishner
b40344675e Update README.md (#332) 2024-11-05 14:57:24 +02:00
Guy Ben-Aharon
df7e687f61 chartdb.png image update (#330) 2024-11-05 14:03:55 +02:00
Guy Ben-Aharon
ad10d26f13 chartdb.png image name update (#329) 2024-11-05 13:58:48 +02:00
Jonathan Fishner
588c64b380 Tempaltes keywords (#325)
* fix for tempaltes keywords

* remove keywords

* update templates description

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2024-11-05 13:40:57 +02:00
Guy Ben-Aharon
3d3efc5e82 add canonical link to templates (#327) 2024-11-05 10:54:05 +02:00
Guy Ben-Aharon
d8a20ebbd9 fix(templates): fetch templates data from router (#321) 2024-11-04 18:01:04 +02:00
Jonathan Fishner
ebce8827ea fix(templates): add two more templates (Airbnb, Wordpress) (#317)
* add two more templates (Airbnb, Wordpress)

* fix slugs

* fix templates sizes

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2024-11-04 15:41:51 +02:00
99 changed files with 20424 additions and 932 deletions

View File

@@ -1,5 +1,46 @@
# 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)
### Bug Fixes
* **offline:** add support when running on isolated network ([#359](https://github.com/chartdb/chartdb/issues/359)) ([aa884b4](https://github.com/chartdb/chartdb/commit/aa884b49ce16d70f67881bdc940993c1fe901796))
* open default diagram after deleting current diagram ([#350](https://github.com/chartdb/chartdb/issues/350)) ([87a40cf](https://github.com/chartdb/chartdb/commit/87a40cff615b04b678642ba2d6e097c38b26d239))
* **select-box:** allow using tab & space to show choices ([#336](https://github.com/chartdb/chartdb/issues/336)) ([93f623a](https://github.com/chartdb/chartdb/commit/93f623a13a61e9143638fbe7e8346f07e37a26b2))
* **smart query:** import postgres FKs ([#357](https://github.com/chartdb/chartdb/issues/357)) ([acb736e](https://github.com/chartdb/chartdb/commit/acb736e44fd50d29a85b4eff42e20780aef710ed))
* **templates:** add two more templates (Airbnb, Wordpress) ([#317](https://github.com/chartdb/chartdb/issues/317)) ([ebce882](https://github.com/chartdb/chartdb/commit/ebce8827eab049eefa0eebcb0ec2540698bc0e15))
* **templates:** align database icon ([#351](https://github.com/chartdb/chartdb/issues/351)) ([efaddee](https://github.com/chartdb/chartdb/commit/efaddeebb4f24235d82f4e2bf7423fbf48b97187))
* **template:** separator in case of empty url ([#355](https://github.com/chartdb/chartdb/issues/355)) ([180886c](https://github.com/chartdb/chartdb/commit/180886c5882f2329c797fc284b255012d21f5b5c))
* **templates:** fetch templates data from router ([#321](https://github.com/chartdb/chartdb/issues/321)) ([d8a20eb](https://github.com/chartdb/chartdb/commit/d8a20ebbd9118989690a40fcd3aa59fb156b446f))
## 1.0.0 (2024-11-04)

View File

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

View File

@@ -15,8 +15,8 @@
<h3 align="center">
<a href="https://discord.gg/QeFwyWSKwC">Community</a> &bull;
<a href="https://www.chartdb.io">Website</a> &bull;
<a href="https://app.chartdb.io/examples">Demo</a>
<a href="https://www.chartdb.io?ref=github_readme">Website</a> &bull;
<a href="https://app.chartdb.io?ref=github_readme">Demo</a>
</h3>
<h4 align="center">
@@ -38,7 +38,7 @@
---
<p align="center">
<img width='700px' src="./public/ChartDB.png">
<img width='700px' src="./public/chartdb.png">
</p>
### 🎉 ChartDB
@@ -71,7 +71,7 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif
## Getting Started
Use the [cloud version](https://app.chartdb.io/) or deploy locally:
Use the [cloud version](https://app.chartdb.io?ref=github_readme_2) or deploy locally:
### How To Use
@@ -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
```
@@ -105,7 +105,7 @@ Open your browser and navigate to `http://localhost:8080`.
## Try it on our website
1. Go to [ChartDB.io](https://chartdb.io)
1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2)
2. Click "Go to app"
3. Choose the database that you are using.
4. Take the magic query and run it in your database.

8
package-lock.json generated
View File

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

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.0.0",
"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",

View File

Before

Width:  |  Height:  |  Size: 882 KiB

After

Width:  |  Height:  |  Size: 882 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 369 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 KiB

View File

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

View File

@@ -9,12 +9,15 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip';
import { useTranslation } from 'react-i18next';
import { DarkTheme } from './themes/dark';
import { LightTheme } from './themes/light';
import './config.ts';
export interface CodeSnippetProps {
className?: string;
code: string;
language?: 'sql' | 'shell';
loading?: boolean;
autoScroll?: boolean;
isComplete?: boolean;
}
export const Editor = lazy(() =>
@@ -24,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();
@@ -46,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);
@@ -62,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}
@@ -117,6 +143,9 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
contextmenu: false,
}}
/>
{!isComplete ? (
<div className="absolute bottom-2 right-2 size-2 animate-blink rounded-full bg-pink-600" />
) : null}
</Suspense>
)}
</div>

View File

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

View File

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

View File

@@ -156,9 +156,19 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
[options, value, multiple]
);
const handleKeyDown = React.useCallback(
(e: React.KeyboardEvent) => {
if (!isOpen && e.code.toLowerCase() === 'space') {
e.preventDefault();
onOpenChange(true);
}
},
[isOpen, onOpenChange]
);
return (
<Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
<PopoverTrigger asChild>
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
<div
className={cn(
`flex min-h-[36px] cursor-pointer items-center justify-between rounded-md border px-3 py-1 data-[state=open]:border-ring ${disabled ? 'bg-muted pointer-events-none' : ''}`,

View File

@@ -11,8 +11,6 @@ import type { DBRelationship } from '@/lib/domain/db-relationship';
import { useStorage } from '@/hooks/use-storage';
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
import type { Diagram } from '@/lib/domain/diagram';
import { useNavigate } from 'react-router-dom';
import { useConfig } from '@/hooks/use-config';
import type { DatabaseEdition } from '@/lib/domain/database-edition';
import type { DBSchema } from '@/lib/domain/db-schema';
import {
@@ -34,13 +32,11 @@ export const ChartDBProvider: React.FC<
> = ({ children, diagram, readonly }) => {
let db = useStorage();
const events = useEventEmitter<ChartDBEvent>();
const navigate = useNavigate();
const { setSchemasFilter, schemasFilter } = useLocalConfig();
const { addUndoAction, resetRedoStack, resetUndoStack } =
useRedoUndoStack();
const [diagramId, setDiagramId] = useState('');
const [diagramName, setDiagramName] = useState('');
const { updateConfig } = useConfig();
const [diagramCreatedAt, setDiagramCreatedAt] = useState<Date>(new Date());
const [diagramUpdatedAt, setDiagramUpdatedAt] = useState<Date>(new Date());
const [databaseType, setDatabaseType] = useState<DatabaseType>(
@@ -173,34 +169,13 @@ export const ChartDBProvider: React.FC<
resetRedoStack();
resetUndoStack();
const [config] = await Promise.all([
db.getConfig(),
await Promise.all([
db.deleteDiagramTables(diagramId),
db.deleteDiagramRelationships(diagramId),
db.deleteDiagram(diagramId),
db.deleteDiagramDependencies(diagramId),
]);
if (config?.defaultDiagramId === diagramId) {
const diagrams = await db.listDiagrams();
if (diagrams.length > 0) {
const defaultDiagramId = diagrams[0].id;
await updateConfig({ defaultDiagramId });
navigate(`/diagrams/${defaultDiagramId}`);
} else {
await updateConfig({ defaultDiagramId: '' });
navigate('/');
}
}
}, [
db,
diagramId,
navigate,
resetRedoStack,
resetUndoStack,
updateConfig,
]);
}, [db, diagramId, resetRedoStack, resetUndoStack]);
const updateDiagramUpdatedAt: ChartDBContext['updateDiagramUpdatedAt'] =
useCallback(async () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ export const HelmetData: React.FC = () => (
/>
<meta
property="og:image"
content="https://app.chartdb.io/ChartDB.png"
content="https://app.chartdb.io/chartdb.png"
/>
<meta property="og:url" content="https://app.chartdb.io" />
<meta name="twitter:card" content="summary_large_image" />
@@ -32,7 +32,7 @@ export const HelmetData: React.FC = () => (
/>
<meta
name="twitter:image"
content="https://github.com/chartdb/chartdb/raw/main/public/ChartDB.png"
content="https://github.com/chartdb/chartdb/raw/main/public/chartdb.png"
/>
<title>ChartDB - Database schema diagrams visualizer</title>
</Helmet>

View File

@@ -1,12 +1,33 @@
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import type { LanguageMetadata } from './types';
import { en, enMetadata } from './locales/en';
import { es } from './locales/es';
import { fr } from './locales/fr';
import { de } from './locales/de';
import { hi } from './locales/hi';
import { ja } from './locales/ja';
import { pt_BR } from './locales/pt_BR';
import { es, esMetadata } from './locales/es';
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,
esMetadata,
frMetadata,
deMetadata,
hiMetadata,
jaMetadata,
ko_KRMetadata,
pt_BRMetadata,
ukMetadata,
ruMetadata,
zh_CNMetadata,
zh_TWMetadata,
];
const resources = {
en,
@@ -15,7 +36,12 @@ const resources = {
de,
hi,
ja,
ko_KR,
pt_BR,
uk,
ru,
zh_CN,
zh_TW,
};
i18n.use(initReactI18next).init({

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@@ -0,0 +1,387 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ko_KR: LanguageTranslation = {
translation: {
menu: {
file: {
file: '파일',
new: '새 다이어그램',
open: '열기',
save: '저장',
import_database: '데이터베이스 가져오기',
export_sql: 'SQL로 저장',
export_as: '다른 형식으로 저장',
delete_diagram: '다이어그램 삭제',
exit: '종료',
},
edit: {
edit: '편집',
undo: '실행 취소',
redo: '다시 실행',
clear: '모두 지우기',
},
view: {
view: '보기',
show_sidebar: '사이드바 보이기',
hide_sidebar: '사이드바 숨기기',
hide_cardinality: '카디널리티 숨기기',
show_cardinality: '카디널리티 보이기',
zoom_on_scroll: '스크롤 시 확대',
theme: '테마',
change_language: '언어/Language',
show_dependencies: '종속성 보이기',
hide_dependencies: '종속성 숨기기',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: '도움말',
visit_website: 'ChartDB 사이트 방문',
join_discord: 'Discord 가입',
schedule_a_call: 'Talk with us!',
},
},
delete_diagram_alert: {
title: '다이어그램 삭제',
description:
'이 작업은 되돌릴 수 없으며 다이어그램이 영구적으로 삭제됩니다.',
cancel: '취소',
delete: '삭제',
},
clear_diagram_alert: {
title: '다이어그램 지우기',
description:
'이 작업은 되돌릴 수 없으며 다이어그램의 모든 데이터가 지워집니다.',
cancel: '취소',
clear: '지우기',
},
reorder_diagram_alert: {
title: '다이어그램 재정렬',
description:
'이 작업은 모든 다이어그램이 재정렬됩니다. 계속하시겠습니까?',
reorder: '재정렬',
cancel: '취소',
},
multiple_schemas_alert: {
title: '다중 스키마',
description:
'현재 다이어그램에 {{schemasCount}}개의 스키마가 있습니다. Currently displaying: {{formattedSchemas}}.',
dont_show_again: '다시 보여주지 마세요',
change_schema: '변경',
none: '없음',
},
theme: {
system: '시스템 설정에 따름',
light: '밝게',
dark: '어둡게',
},
zoom: {
on: '활성화',
off: '비활성화',
},
last_saved: '최근 저장일시: ',
saved: '저장됨',
diagrams: '다이어그램',
loading_diagram: '다이어그램 로딩중...',
deselect_all: '모두 선택 해제',
select_all: '모두 선택',
clear: '지우기',
show_more: '더 보기',
show_less: '간략히',
copy_to_clipboard: '클립보드에 복사',
copied: '복사됨!',
side_panel: {
schema: '스키마:',
filter_by_schema: '스키마로 필터링',
search_schema: '스키마 검색...',
no_schemas_found: '스키마를 찾을 수 없습니다.',
view_all_options: '전체 옵션 보기...',
tables_section: {
tables: '테이블',
add_table: '테이블 추가',
filter: '필터',
collapse: '모두 접기',
table: {
fields: '필드',
nullable: 'null 여부',
primary_key: '기본키',
indexes: '인덱스',
comments: '주석',
no_comments: '주석 없음',
add_field: '필드 추가',
add_index: '인덱스 추가',
index_select_fields: '필드 선택',
no_types_found: '타입을 찾을 수 없습니다.',
field_name: '이름',
field_type: '타입',
field_actions: {
title: '필드 속성',
unique: '유니크 여부',
comments: '주석',
no_comments: '주석 없음',
delete_field: '필드 삭제',
},
index_actions: {
title: '인덱스 속성',
name: '인덱스 명',
unique: '유니크 여부',
delete_index: '인덱스 삭제',
},
table_actions: {
title: '테이블 작업',
change_schema: '스키마 변경',
add_field: '필드 추가',
add_index: '인덱스 추가',
delete_table: '테이블 삭제',
},
},
empty_state: {
title: '테이블 없음',
description: '테이블을 만들어 시작하세요.',
},
},
relationships_section: {
relationships: '연관 관계',
filter: '필터',
add_relationship: '연관 관계 추가',
collapse: '모두 접기',
relationship: {
primary: '주 테이블',
foreign: '참조 테이블',
cardinality: '카디널리티',
delete_relationship: '제거',
relationship_actions: {
title: '연관 관계 작업',
delete_relationship: '연관 관계 삭제',
},
},
empty_state: {
title: '연관 관계',
description: '테이블 연결을 위해 연관 관계를 생성하세요',
},
},
dependencies_section: {
dependencies: '종속성',
filter: '필터',
collapse: '모두 접기',
dependency: {
table: '테이블',
dependent_table: '뷰 테이블',
delete_dependency: '삭제',
dependency_actions: {
title: '종속성 작업',
delete_dependency: '뷰 테이블 삭제',
},
},
empty_state: {
title: '뷰 테이블 없음',
description: '뷰 테이블을 만들어 시작하세요.',
},
},
},
toolbar: {
zoom_in: '확대',
zoom_out: '축소',
save: '저장',
show_all: '전체 저장',
undo: '실행 취소',
redo: '다시 실행',
reorder_diagram: '다이어그램 재정렬',
highlight_overlapping_tables: '겹치는 테이블 강조 표시',
},
new_diagram_dialog: {
database_selection: {
title: '당신의 데이터베이스 종류가 무엇인가요?',
description:
'각 데이터베이스에는 고유한 기능과 특징이 있습니다.',
check_examples_long: '예제 확인',
check_examples_short: '예제들',
},
import_database: {
title: '당신의 데이터베이스를 가져오세요',
database_edition: '데이터베이스 세부 종류:',
step_1: '데이터베이스에서 아래의 SQL을 실행해주세요:',
step_2: '이곳에 결과를 붙여넣어주세요:',
script_results_placeholder: '이곳에 스크립트 결과를 입력...',
ssms_instructions: {
button_text: 'SSMS을 사용하시는 경우',
title: '지침',
step_1: '도구 > 옵션 > 쿼리 응답 > SQL Server',
step_2: '"결과를 그리드로 표시"를 사용하는 경우 비 XML 데이터에 대해 검색되는 최대 문자 수를 변경합니다. (9999999로 설정)',
},
instructions_link: '도움이 필요하신가요? 영상 가이드 보기',
check_script_result: '스크립트 결과 확인',
},
cancel: '취소',
back: '뒤로가기',
// TODO: Translate
import_from_file: 'Import from File',
empty_diagram: '빈 다이어그램으로 시작',
continue: '계속',
import: '가져오기',
},
open_diagram_dialog: {
title: '다이어그램 열기',
description: '아래의 목록에서 다이어그램을 선택하세요.',
table_columns: {
name: '이름',
created_at: '생성일시',
last_modified: '최근 수정일시',
tables_count: '테이블 갯수',
},
cancel: '취소',
open: '열기',
},
export_sql_dialog: {
title: 'SQL로 내보내기',
description: '다이어그램 스키마를 {{databaseType}} SQL로 내보내기',
close: '닫기',
loading: {
text: '{{databaseType}} SQL을 AI가 생성하고 있습니다...',
description: '30초 정도 걸릴 수 있습니다.',
},
error: {
message:
'SQL 생성에 실패하였습니다. 잠시후 다시 시도해주세요 계속해서 증상이 발생하는 경우 <0>우리에게 연락해주세요</0>.',
description:
'당신의 OPENAI_TOKEN가 있는 경우, <0>여기에서</0> 메뉴얼을 참고하여 사용하실 수 있습니다.',
},
},
create_relationship_dialog: {
title: '연관 관계 생성',
primary_table: '주 테이블',
primary_field: '주 필드',
referenced_table: '참조 테이블',
referenced_field: '참조 필드',
primary_table_placeholder: '테이블 선택',
primary_field_placeholder: '필드 선택',
referenced_table_placeholder: '테이블 선택',
referenced_field_placeholder: '필드 선택',
no_tables_found: '테이블을 찾을 수 없습니다',
no_fields_found: '필드를 찾을 수 없습니다',
create: '생성',
cancel: '취소',
},
import_database_dialog: {
title: '현재 다이어그램 가져오기',
override_alert: {
title: '데이터베이스 가져오기',
content: {
alert: '이 다이어그램을 가져오면 기존 테이블 및 연관 관계에 영향을 미칩니다.',
new_tables:
'<bold>{{newTablesNumber}}</bold>개의 신규 테이블 생성됨',
new_relationships:
'<bold>{{newRelationshipsNumber}}</bold>개의 신규 연관 관계 생성됨',
tables_override:
'<bold>{{tablesOverrideNumber}}</bold>개의 테이블이 덮어씌워짐',
proceed: '정말로 가져오시겠습니까?',
},
import: '가져오기',
cancel: '취소',
},
},
export_image_dialog: {
title: '이미지로 내보내기',
description: '내보낼 배율을 선택해주세요:',
scale_1x: '1x 기본',
scale_2x: '2x (권장)',
scale_3x: '3x',
scale_4x: '4x',
cancel: '취소',
export: '내보내기',
},
new_table_schema_dialog: {
title: '스키마 선택',
description:
'현재 여러 스키마가 표시됩니다. 새 테이블을 위해 하나를 선택합니다.',
cancel: '취소',
confirm: 'Confirm',
},
update_table_schema_dialog: {
title: '스키마 변경',
description: '"{{tableName}}" 테이블 스키마를 수정합니다',
cancel: '취소',
confirm: '변경',
},
star_us_dialog: {
title: '개선할 수 있도록 도와주세요!',
description:
'GitHub에 별을 찍어주시겠습니까? 클릭 한번이면 됩니다!',
close: '아직은 괜찮아요',
confirm: '당연하죠!',
},
// TODO: Translate
export_diagram_dialog: {
title: 'Export Diagram',
description: 'Choose the format for export:',
format_json: 'JSON',
cancel: 'Cancel',
export: 'Export',
},
// TODO: Translate
import_diagram_dialog: {
title: 'Import Diagram',
description: 'Paste the diagram JSON below:',
cancel: 'Cancel',
import: 'Import',
error: {
title: 'Error importing diagram',
description:
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: '일대일 (1:1)',
one_to_many: '일대다 (1:N)',
many_to_one: '다대일 (N:1)',
many_to_many: '다대다 (N:N)',
},
canvas_context_menu: {
new_table: '새 테이블',
new_relationship: '새 연관관계',
},
table_node_context_menu: {
edit_table: '테이블 수정',
delete_table: '테이블 삭제',
},
// TODO: Add translations
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
tool_tips: {
double_click_to_edit: '더블클릭하여 편집',
},
},
};
export const ko_KRMetadata: LanguageMetadata = {
name: '한국어',
code: 'ko_KR',
};

View File

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

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

@@ -0,0 +1,383 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const ru: LanguageTranslation = {
translation: {
menu: {
file: {
file: 'Файл',
new: 'Создать',
open: 'Открыть',
save: 'Сохранить',
import_database: 'Импортировать базу данных',
export_sql: 'Экспорт SQL',
export_as: 'Экспортировать как',
delete_diagram: 'Удалить диаграмму',
exit: 'Выход',
},
edit: {
edit: 'Изменение',
undo: 'Отменить',
redo: 'Вернуть',
clear: 'Очистить',
},
view: {
view: 'Вид',
show_sidebar: 'Показать боковую панель',
hide_sidebar: 'Скрыть боковую панель',
hide_cardinality: 'Скрыть множественность связи',
show_cardinality: 'Показать множественность связи',
zoom_on_scroll: 'Увеличение при прокрутке',
theme: 'Тема',
change_language: 'Сменить язык',
show_dependencies: 'Показать зависимости',
hide_dependencies: 'Скрыть зависимости',
},
share: {
share: 'Поделиться',
export_diagram: 'Экспорт кода диаграммы',
import_diagram: 'Импорт кода диаграммы',
},
help: {
help: 'Помощь',
visit_website: 'Перейти на сайт ChartDB',
join_discord: 'Присоединиться к сообществу в Discord',
schedule_a_call: 'Поговорите с нами!',
},
},
delete_diagram_alert: {
title: 'Удалить диаграмму',
description:
'Это действие нельзя отменить. Это навсегда удалит диаграмму.',
cancel: 'Отменить',
delete: 'Удалить',
},
clear_diagram_alert: {
title: 'Очистить диаграмму',
description:
'Это действие нельзя отменить. Это навсегда удалит все данные в диаграмме.',
cancel: 'Отменить',
clear: 'Очистить',
},
reorder_diagram_alert: {
title: 'Переупорядочить диаграмму',
description:
'Это действие переставит все таблицы на диаграмме. Хотите продолжить?',
reorder: 'Изменить порядок',
cancel: 'Отменить',
},
multiple_schemas_alert: {
title: 'Множественные схемы',
description:
'{{schemasCount}} схем в этой диаграмме. В данный момент отображается: {{formattedSchemas}}.',
dont_show_again: 'Больше не показывать',
change_schema: 'Изменить',
none: 'никто',
},
theme: {
system: 'Системная',
light: 'Светлая',
dark: 'Темная',
},
zoom: {
on: 'Включено',
off: 'Выключено',
},
last_saved: 'Последнее сохранение',
saved: 'Сохранено',
diagrams: 'Диаграммы',
loading_diagram: 'Загрузка диаграммы...',
deselect_all: 'Отменить выбор всех',
select_all: 'Выбрать все',
clear: 'Очистить',
show_more: 'Показать больше',
show_less: 'Показать меньше',
side_panel: {
schema: 'Схема:',
filter_by_schema: 'Фильтр по схеме',
search_schema: 'Схема поиска...',
no_schemas_found: 'Схемы не найдены.',
view_all_options: 'Просмотреть все варианты...',
tables_section: {
tables: 'Таблицы',
add_table: 'Добавить таблицу',
filter: 'Фильтр',
collapse: 'Свернуть все',
table: {
fields: 'Поля',
nullable: 'Может содержать NULL?',
primary_key: 'Первичный ключ,',
indexes: 'Индексы',
comments: 'Комментарии',
no_comments: 'Нет комментария',
add_field: 'Добавить поле',
add_index: 'Добавить индекс',
index_select_fields: 'Выберите поля',
no_types_found: 'Типы не найдены',
field_name: 'Имя',
field_type: 'Тип',
field_actions: {
title: 'Атрибуты поля',
unique: 'Уникальный',
comments: 'Комментарии',
no_comments: 'Нет комментария',
delete_field: 'Удалить поле',
},
index_actions: {
title: 'Атрибуты индекса',
name: 'Имя',
unique: 'Уникальный',
delete_index: 'Удалить индекс',
},
table_actions: {
title: 'Действия',
change_schema: 'Изменить схему',
add_field: 'Добавить поле',
add_index: 'Добавить индекс',
delete_table: 'Удалить таблицу',
},
},
empty_state: {
title: 'Нет таблиц',
description: 'Создайте таблицу, чтобы начать',
},
},
relationships_section: {
relationships: 'Отношения',
filter: 'Фильтр',
add_relationship: 'Добавить отношение',
collapse: 'Свернуть все',
relationship: {
primary: 'Основная таблица',
foreign: 'Справочная таблица',
cardinality: 'Тип множественности связи',
delete_relationship: 'Удалить',
relationship_actions: {
title: 'Действия',
delete_relationship: 'Удалить',
},
},
empty_state: {
title: 'Нет отношений',
description: 'Создайте связь для соединения таблиц',
},
},
dependencies_section: {
dependencies: 'Зависимости',
filter: 'Фильтр',
collapse: 'Свернуть все',
dependency: {
table: 'Стол',
dependent_table: 'Зависимый вид',
delete_dependency: 'Удалить',
dependency_actions: {
title: 'Действия',
delete_dependency: 'Удалить',
},
},
empty_state: {
title: 'Нет зависимостей',
description: 'Создайте представление, чтобы начать',
},
},
},
toolbar: {
zoom_in: 'Увеличить масштаб',
zoom_out: 'Уменьшить масштаб',
save: 'Сохранить',
show_all: 'Показать все',
undo: 'Отменить',
redo: 'Вернуть',
reorder_diagram: 'Переупорядочить диаграмму',
highlight_overlapping_tables: 'Выделение перекрывающихся таблиц',
},
new_diagram_dialog: {
database_selection: {
title: 'Какова ваша база данных?',
description:
'Каждая база данных имеет свои уникальные функции и возможности.',
check_examples_long: 'Открыть примеры',
check_examples_short: 'Примеры',
},
import_database: {
title: 'Импортируйте свою базу данных',
database_edition: 'Версия базы данных:',
step_1: 'Запустите этот скрипт в своей базе данных:',
step_2: 'Вставьте вывод скрипта сюда:',
script_results_placeholder: 'Вывод скрипта здесь...',
ssms_instructions: {
button_text: 'SSMS Инструкции',
title: 'Инструкции',
step_1: 'Откройте в меню пункты Инструменты > Параметры > Результаты запроса > SQL Сервер.',
step_2: 'Если вы используете "Результат в сетке," измените Максимальное количество извлекаемых символов для данных, отличных от XML (установите на 9999999).',
},
instructions_link: 'Нужна помощь? Посмотрите, как',
check_script_result: 'Проверить результат выполнения скрипта',
},
cancel: 'Отменить',
back: 'Назад',
import_from_file: 'Импортировать из файла',
empty_diagram: 'Пустая диаграмма',
continue: 'Продолжить',
import: 'Импорт',
},
open_diagram_dialog: {
title: 'Открыть диаграмму',
description:
'Выберите диаграмму, которую нужно открыть, из списка ниже.',
table_columns: {
name: 'Имя',
created_at: 'Создано в',
last_modified: 'Последнее изменение',
tables_count: 'Таблицы',
},
cancel: 'Отмена',
open: 'Открыть',
},
export_sql_dialog: {
title: 'Экспорт SQL',
description:
'Экспортируйте схему диаграммы в {{databaseType}} скрипт',
close: 'Закрыть',
loading: {
text: 'ИИ генерирует SQL для {{databaseType}}...',
description: 'Это должно занять до 30 секунд.',
},
error: {
message:
'Ошибка создания скрипта SQL. Попробуйте еще раз позже или <0>свяжитесь с нами</0>.',
description:
'Не стесняйтесь использовать ваш OPENAI_TOKEN, см. руководство <0>здесь</0>.',
},
},
create_relationship_dialog: {
title: 'Создать отношениe',
primary_table: 'Основная таблица',
primary_field: 'Основное поле',
referenced_table: 'Ссылается на таблицу',
referenced_field: 'Ссылается на поле',
primary_table_placeholder: 'Выберите таблицу',
primary_field_placeholder: 'Выберите поле',
referenced_table_placeholder: 'Выберите таблицу',
referenced_field_placeholder: 'Выберите поле',
no_tables_found: 'Таблицы не найдены',
no_fields_found: 'Поля не найдены',
create: 'Создать',
cancel: 'Отменить',
},
import_database_dialog: {
title: 'Импорт в текущую диаграмму',
override_alert: {
title: 'Импортировать базу данных',
content: {
alert: 'Импорт этой диаграммы повлияет на существующие таблицы и связи.',
new_tables:
'<bold>{{newTablesNumber}}</bold> будут добавлены новые таблицы.',
new_relationships:
'<bold>{{newRelationshipsNumber}}</bold> будут созданы новые отношения.',
tables_override:
'<bold>{{tablesOverrideNumber}}</bold> таблицы будут перезаписаны.',
proceed: 'Хотите продолжить?',
},
import: 'Импорт',
cancel: 'Отмена',
},
},
export_image_dialog: {
title: 'Экспортировать изображение',
description: 'Выберите детализацию изображения при экспорте:',
scale_1x: '1x Обычный',
scale_2x: '2x (Рекомендовано)',
scale_3x: '3x',
scale_4x: '4x',
cancel: 'Отменить',
export: 'Экспортировать',
},
new_table_schema_dialog: {
title: 'Выбрать схему',
description:
'В настоящее время отображается несколько схем. Выберите одну для новой таблицы.',
cancel: 'Отменить',
confirm: 'Подтвердить',
},
update_table_schema_dialog: {
title: 'Изменить схему',
description: 'Обновить таблицу "{{tableName}}" схема',
cancel: 'Отменить',
confirm: 'Изменить',
},
star_us_dialog: {
title: 'Помогите нам стать лучше!',
description:
'Хотите отметить нас на GitHub? Это всего лишь один клик!',
close: 'Не сейчас',
confirm: 'Конечно!',
},
export_diagram_dialog: {
title: 'Экспорт кода диаграммы',
description: 'Выберите формат экспорта:',
format_json: 'JSON',
cancel: 'Отменить',
export: 'Экспортировать',
},
import_diagram_dialog: {
title: 'Импорт кода диаграммы',
description: 'Вставьте JSON код диаграммы ниже:',
cancel: 'Отменить',
import: 'Импортировать',
error: {
title: 'Ошибка при импорте диаграммы',
description:
'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: 'Один к одному',
one_to_many: 'Один ко многим',
many_to_one: 'Многие к одному',
many_to_many: 'Многие ко многим',
},
canvas_context_menu: {
new_table: 'Создать таблицу',
new_relationship: 'Создать отношение',
},
table_node_context_menu: {
edit_table: 'Изменить таблицу',
delete_table: 'Удалить таблицу',
},
copy_to_clipboard: 'Скопировать в буфер обмена',
copied: 'Скопировано!',
snap_to_grid_tooltip: 'Выравнивание по сетке (Удерживайте {{key}})',
tool_tips: {
double_click_to_edit: 'Кликните дважды, чтобы изменить',
},
},
};
export const ruMetadata: LanguageMetadata = {
name: 'Russian',
code: 'ru',
};

388
src/i18n/locales/uk.ts Normal file
View File

@@ -0,0 +1,388 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const uk: 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: 'Приховати залежності',
},
// TODO: Translate
share: {
share: 'Share',
export_diagram: 'Export Diagram',
import_diagram: 'Import Diagram',
},
help: {
help: 'Допомога',
visit_website: 'Відвідайте ChartDB',
join_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 Сервер.',
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}} сценарій',
close: 'Закрити',
loading: {
text: 'ШІ створює SQL для {{databaseType}}...',
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: 'Хочете позначити нас на Ґітхаб? Це лише один клік!',
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: 'Один до багатьох',
many_to_one: 'Багато до одного',
many_to_many: 'Багато до багатьох',
},
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 ukMetadata: LanguageMetadata = {
name: 'Українська',
code: 'uk',
};

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

@@ -0,0 +1,378 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const zh_CN: LanguageTranslation = {
translation: {
menu: {
file: {
file: '文件',
new: '新建',
open: '打开',
save: '保存',
import_database: '导入数据库',
export_sql: '导出 SQL 语句',
export_as: '导出为',
delete_diagram: '删除关系图',
exit: '退出',
},
edit: {
edit: '编辑',
undo: '撤销',
redo: '重做',
clear: '清空',
},
view: {
view: '视图',
show_sidebar: '展示侧边栏',
hide_sidebar: '隐藏侧边栏',
hide_cardinality: '隐藏基数',
show_cardinality: '展示基数',
zoom_on_scroll: '滚动缩放',
theme: '主题',
change_language: '语言',
show_dependencies: '展示依赖',
hide_dependencies: '隐藏依赖',
},
share: {
share: '分享',
export_diagram: '导出关系图',
import_diagram: '导入关系图',
},
help: {
help: '帮助',
visit_website: '访问 ChartDB',
join_discord: '在 Discord 上加入我们',
schedule_a_call: '和我们交流!',
},
},
delete_diagram_alert: {
title: '删除关系图',
description: '此操作无法撤销。这将永久删除关系图。',
cancel: '取消',
delete: '删除',
},
clear_diagram_alert: {
title: '清除关系图',
description: '此操作无法撤销。这将永久删除关系图中的所有数据。',
cancel: '取消',
clear: '清空',
},
reorder_diagram_alert: {
title: '重新排列关系图',
description: '此操作将重新排列关系图中的所有表。是否要继续?',
reorder: '重新排列',
cancel: '取消',
},
multiple_schemas_alert: {
title: '多个模式',
description:
'此关系图中有 {{schemasCount}} 个模式,当前显示:{{formattedSchemas}}。',
dont_show_again: '不再展示',
change_schema: '更改',
none: '无',
},
theme: {
system: '系统',
light: '浅色',
dark: '深色',
},
zoom: {
on: '启用',
off: '禁用',
},
last_saved: '上次保存时间:',
saved: '已保存',
diagrams: '关系图',
loading_diagram: '加载关系图...',
deselect_all: '取消全选',
select_all: '全选',
clear: '清空',
show_more: '展开',
show_less: '收起',
copy_to_clipboard: '复制到剪切板',
copied: '复制了!',
side_panel: {
schema: '模式:',
filter_by_schema: '按模式筛选',
search_schema: '搜索模式...',
no_schemas_found: '未找到模式。',
view_all_options: '查看所有选项...',
tables_section: {
tables: '表',
add_table: '添加表',
filter: '筛选',
collapse: '全部折叠',
table: {
fields: '字段',
nullable: '可为空?',
primary_key: '主键',
indexes: '索引',
comments: '注释',
no_comments: '空',
add_field: '添加字段',
add_index: '添加索引',
index_select_fields: '选择字段',
no_types_found: '未找到类型',
field_name: '名称',
field_type: '类型',
field_actions: {
title: '字段属性',
unique: '唯一',
comments: '注释',
no_comments: '空',
delete_field: '删除字段',
},
index_actions: {
title: '索引属性',
name: '名称',
unique: '唯一',
delete_index: '删除索引',
},
table_actions: {
title: '表操作',
change_schema: '更改模式',
add_field: '添加字段',
add_index: '添加索引',
delete_table: '删除表',
},
},
empty_state: {
title: '没有表',
description: '新建表以开始',
},
},
relationships_section: {
relationships: '关系',
filter: '筛选',
add_relationship: '添加关系',
collapse: '全部折叠',
relationship: {
primary: '主表',
foreign: '被引用表',
cardinality: '基数',
delete_relationship: '删除',
relationship_actions: {
title: '操作',
delete_relationship: '删除',
},
},
empty_state: {
title: '无关系',
description: '创建关系以连接表',
},
},
dependencies_section: {
dependencies: '依赖关系',
filter: '筛选',
collapse: '全部折叠',
dependency: {
table: '表',
dependent_table: '依赖视图',
delete_dependency: '删除',
dependency_actions: {
title: '操作',
delete_dependency: '删除',
},
},
empty_state: {
title: '无依赖',
description: '创建视图以开始',
},
},
},
toolbar: {
zoom_in: '放大',
zoom_out: '缩小',
save: '保存',
show_all: '展示全部',
undo: '撤销',
redo: '重做',
reorder_diagram: '重新排列关系图',
highlight_overlapping_tables: '突出显示重叠的表',
},
new_diagram_dialog: {
database_selection: {
title: '您是哪种数据库?',
description: '每种数据库都有其特性和功能。',
check_examples_long: '查看样例',
check_examples_short: '样例',
},
import_database: {
title: '导入您的数据库',
database_edition: '数据库类型:',
step_1: '在您的数据库中执行以下脚本:',
step_2: '将结果粘贴于此:',
script_results_placeholder: '结果...',
ssms_instructions: {
button_text: 'SSMS 说明',
title: '说明',
step_1: '前往 工具 > 选项 > 查询结果 > SQL Server。',
// TODO: Add translations
step_2: '如果您使用“Result to Grid”功能请将非 XML 数据的最大提取字符数更改为 9999999。',
},
instructions_link: '需要帮助?看看如何操作',
check_script_result: '检查脚本结果',
},
cancel: '取消',
import_from_file: '从文件导入',
back: '上一步',
empty_diagram: '新建空关系图',
continue: '下一步',
import: '导入',
},
open_diagram_dialog: {
title: '打开关系图',
description: '从下面的列表中选择一个图表打开。',
table_columns: {
name: '名称',
created_at: '创建于',
last_modified: '最后修改于',
tables_count: '表数量',
},
cancel: '取消',
open: '打开',
},
export_sql_dialog: {
title: '导出 SQL 语句',
description: '将您的图表模式导出为 {{databaseType}} 脚本。',
close: '关闭',
loading: {
text: 'AI 正在为 {{databaseType}} 生成 SQL 语句...',
description: '此操作最多需要 30 秒。',
},
error: {
message:
'生成 SQL 脚本时出错。请稍后再试,或者 <0>联系我们</0>。',
description:
'随时使用您的 OPENAI_TOKEN在<0>这里</0>查看手册。',
},
},
create_relationship_dialog: {
title: '创建关系',
primary_table: '主表',
primary_field: '主键字段',
referenced_table: '被引用表',
referenced_field: '被引用字段',
primary_table_placeholder: '选择表',
primary_field_placeholder: '选择字段',
referenced_table_placeholder: '选择表',
referenced_field_placeholder: '选择字段',
no_tables_found: '未找到表',
no_fields_found: '未找到字段',
create: '创建',
cancel: '取消',
},
import_database_dialog: {
title: '导入到当前关系图',
override_alert: {
title: '导入数据库',
content: {
alert: '导入此关系图将影响现有的表和关系。',
new_tables:
'将添加 <bold>{{newTablesNumber}}</bold> 个新表。',
new_relationships:
'将创建 <bold>{{newRelationshipsNumber}}</bold> 个新关系。',
tables_override:
'将覆盖 <bold>{{tablesOverrideNumber}}</bold> 个表。',
proceed: '您是否要继续操作?',
},
import: '导入',
cancel: '取消',
},
},
export_image_dialog: {
title: '导出图片',
description: '选择导出的缩放比例:',
scale_1x: '1x 常规',
scale_2x: '2x (推荐)',
scale_3x: '3x',
scale_4x: '4x',
cancel: '取消',
export: '导出',
},
new_table_schema_dialog: {
title: '选择模式',
description: '当前显示多个模式。请选择一个用于新表。',
cancel: '取消',
confirm: '确认',
},
update_table_schema_dialog: {
title: '更改模式',
description: '更新表 "{{tableName}}" 的模式。',
cancel: '取消',
confirm: '更改',
},
star_us_dialog: {
title: '帮助我们改进!',
description: '您想在 GitHub 上为我们加注星标吗?只需点击一下即可!',
close: '以后再说',
confirm: '当然!',
},
export_diagram_dialog: {
title: '导出关系图',
description: '选择导出格式:',
format_json: 'JSON',
cancel: '取消',
export: '导出',
},
import_diagram_dialog: {
title: '导入关系图',
description: '在下方粘贴关系图的 JSON',
cancel: '取消',
import: '导入',
error: {
title: '导入关系图时出错',
description:
'关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: '一对一',
one_to_many: '一对多',
many_to_one: '多对一',
many_to_many: '多对多',
},
canvas_context_menu: {
new_table: '新建表',
new_relationship: '新建关系',
},
table_node_context_menu: {
edit_table: '编辑表',
delete_table: '删除表',
},
snap_to_grid_tooltip: '对齐到网格(按住 {{key}}',
tool_tips: {
double_click_to_edit: '双击编辑',
},
},
};
export const zh_CNMetadata: LanguageMetadata = {
name: '简体中文',
code: 'zh_CN',
};

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

@@ -0,0 +1,377 @@
import type { LanguageMetadata, LanguageTranslation } from '../types';
export const zh_TW: LanguageTranslation = {
translation: {
menu: {
file: {
file: '檔案',
new: '新增',
open: '開啟',
save: '儲存',
import_database: '匯入資料庫',
export_sql: '匯出 SQL',
export_as: '匯出為特定格式',
delete_diagram: '刪除圖表',
exit: '退出',
},
edit: {
edit: '編輯',
undo: '復原',
redo: '重做',
clear: '清除',
},
view: {
view: '檢視',
show_sidebar: '顯示側邊欄',
hide_sidebar: '隱藏側邊欄',
hide_cardinality: '隱藏基數',
show_cardinality: '顯示基數',
zoom_on_scroll: '滾動縮放',
theme: '主題',
change_language: '變更語言',
show_dependencies: '顯示相依性',
hide_dependencies: '隱藏相依性',
},
share: {
share: '分享',
export_diagram: '匯出圖表',
import_diagram: '匯入圖表',
},
help: {
help: '幫助',
visit_website: '訪問 ChartDB 網站',
join_discord: '加入 Discord',
schedule_a_call: '與我們聯絡!',
},
},
delete_diagram_alert: {
title: '刪除圖表',
description: '此操作無法復原,圖表將被永久刪除。',
cancel: '取消',
delete: '刪除',
},
clear_diagram_alert: {
title: '清除圖表',
description: '此操作無法復原,圖表中的所有資料將被永久刪除。',
cancel: '取消',
clear: '清除',
},
reorder_diagram_alert: {
title: '重新排列圖表',
description: '此操作將重新排列圖表中的所有表格。是否繼續?',
reorder: '重新排列',
cancel: '取消',
},
multiple_schemas_alert: {
title: '多重 Schema',
description:
'此圖表中包含 {{schemasCount}} 個 Schema目前顯示{{formattedSchemas}}。',
dont_show_again: '不再顯示',
change_schema: '變更',
none: '無',
},
theme: {
system: '系統',
light: '淺色',
dark: '深色',
},
zoom: {
on: '開啟',
off: '關閉',
},
last_saved: '上次儲存於',
saved: '已儲存',
diagrams: '圖表',
loading_diagram: '正在載入圖表...',
deselect_all: '取消所有選取',
select_all: '全選',
clear: '清除',
show_more: '顯示更多',
show_less: '顯示較少',
copy_to_clipboard: '複製到剪貼簿',
copied: '已複製!',
side_panel: {
schema: 'Schema:',
filter_by_schema: '依 Schema 篩選',
search_schema: '搜尋 Schema...',
no_schemas_found: '未找到 Schema。',
view_all_options: '顯示所有選項...',
tables_section: {
tables: '表格',
add_table: '新增表格',
filter: '篩選',
collapse: '全部摺疊',
table: {
fields: '欄位',
nullable: '可為 NULL?',
primary_key: '主鍵',
indexes: '索引',
comments: '註解',
no_comments: '無註解',
add_field: '新增欄位',
add_index: '新增索引',
index_select_fields: '選擇欄位',
no_types_found: '未找到類型',
field_name: '名稱',
field_type: '類型',
field_actions: {
title: '欄位屬性',
unique: '唯一',
comments: '註解',
no_comments: '無註解',
delete_field: '刪除欄位',
},
index_actions: {
title: '索引屬性',
name: '名稱',
unique: '唯一',
delete_index: '刪除索引',
},
table_actions: {
title: '表格操作',
change_schema: '變更 Schema',
add_field: '新增欄位',
add_index: '新增索引',
delete_table: '刪除表格',
},
},
empty_state: {
title: '尚無表格',
description: '請新增表格以開始',
},
},
relationships_section: {
relationships: '關聯',
filter: '篩選',
add_relationship: '新增關聯',
collapse: '全部摺疊',
relationship: {
primary: '主表格',
foreign: '參照表格',
cardinality: '基數',
delete_relationship: '刪除',
relationship_actions: {
title: '操作',
delete_relationship: '刪除',
},
},
empty_state: {
title: '尚無關聯',
description: '請新增關聯以連接表格',
},
},
dependencies_section: {
dependencies: '相依性',
filter: '篩選',
collapse: '全部摺疊',
dependency: {
table: '表格',
dependent_table: '相依檢視',
delete_dependency: '刪除',
dependency_actions: {
title: '操作',
delete_dependency: '刪除',
},
},
empty_state: {
title: '尚無相依性',
description: '請建立檢視以開始',
},
},
},
toolbar: {
zoom_in: '放大',
zoom_out: '縮小',
save: '儲存',
show_all: '顯示全部',
undo: '復原',
redo: '重做',
reorder_diagram: '重新排列圖表',
highlight_overlapping_tables: '突出顯示重疊表格',
},
new_diagram_dialog: {
database_selection: {
title: '您使用的是哪種資料庫?',
description: '每種資料庫都有其獨特的功能和能力。',
check_examples_long: '查看範例',
check_examples_short: '範例',
},
import_database: {
title: '匯入資料庫',
database_edition: '資料庫版本:',
step_1: '請在資料庫中執行以下腳本:',
step_2: '將腳本結果貼到此處:',
script_results_placeholder: '在此處貼上腳本結果...',
ssms_instructions: {
button_text: 'SSMS 操作步驟',
title: '操作步驟',
step_1: '導航至 工具 > 選項 > 查詢結果 > SQL Server。',
step_2: '若使用「結果至網格」,請更改非 XML 資料的最大取得字元數(設定為 9999999。',
},
instructions_link: '需要幫助?觀看教學影片',
check_script_result: '檢查腳本結果',
},
cancel: '取消',
import_from_file: '從檔案匯入',
back: '返回',
empty_diagram: '空白圖表',
continue: '繼續',
import: '匯入',
},
open_diagram_dialog: {
title: '開啟圖表',
description: '請從以下列表中選擇一個圖表。',
table_columns: {
name: '名稱',
created_at: '創建時間',
last_modified: '最後修改時間',
tables_count: '表格數',
},
cancel: '取消',
open: '開啟',
},
export_sql_dialog: {
title: '匯出 SQL',
description: '將圖表 Schema 匯出為 {{databaseType}} 格式的腳本',
close: '關閉',
loading: {
text: 'AI 正在生成 {{databaseType}} 的 SQL...',
description: '最多需要 30 秒。',
},
error: {
message:
'生成 SQL 腳本時發生錯誤。稍後再試,或<0>聯繫我們</0>。',
description:
'可以自由使用 OPENAI_TOKEN詳細說明可參考<0>此處</0>。',
},
},
create_relationship_dialog: {
title: '新增關聯',
primary_table: '主表格',
primary_field: '主欄位',
referenced_table: '參照表格',
referenced_field: '參照欄位',
primary_table_placeholder: '選擇表格',
primary_field_placeholder: '選擇欄位',
referenced_table_placeholder: '選擇表格',
referenced_field_placeholder: '選擇欄位',
no_tables_found: '未找到表格',
no_fields_found: '未找到欄位',
create: '建立',
cancel: '取消',
},
import_database_dialog: {
title: '匯入至當前圖表',
override_alert: {
title: '匯入資料庫',
content: {
alert: '匯入此圖表將影響現有表格和關聯。',
new_tables:
'<bold>{{newTablesNumber}}</bold> 個新表格將被新增。',
new_relationships:
'<bold>{{newRelationshipsNumber}}</bold> 個新關聯將被建立。',
tables_override:
'<bold>{{tablesOverrideNumber}}</bold> 個表格將被覆蓋。',
proceed: '是否繼續?',
},
import: '匯入',
cancel: '取消',
},
},
export_image_dialog: {
title: '匯出圖片',
description: '請選擇匯出的倍率:',
scale_1x: '1x 標準',
scale_2x: '2x (推薦)',
scale_3x: '3x',
scale_4x: '4x',
cancel: '取消',
export: '匯出',
},
new_table_schema_dialog: {
title: '選擇 Schema',
description: '目前顯示多個 Schema請為新表格選擇一個。',
cancel: '取消',
confirm: '確認',
},
update_table_schema_dialog: {
title: '變更 Schema',
description: '更新表格「{{tableName}}」的 Schema',
cancel: '取消',
confirm: '變更',
},
star_us_dialog: {
title: '協助我們改善!',
description: '請在 GitHub 上給我們一顆星,只需點擊一下!',
close: '先不要',
confirm: '當然!',
},
export_diagram_dialog: {
title: '匯出圖表',
description: '選擇匯出格式:',
format_json: 'JSON',
cancel: '取消',
export: '匯出',
},
import_diagram_dialog: {
title: '匯入圖表',
description: '請在下方貼上圖表的 JSON',
cancel: '取消',
import: '匯入',
error: {
title: '匯入圖表時發生錯誤',
description:
'圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 chartdb.io@gmail.com',
},
},
relationship_type: {
one_to_one: '一對一',
one_to_many: '一對多',
many_to_one: '多對一',
many_to_many: '多對多',
},
canvas_context_menu: {
new_table: '新建表格',
new_relationship: '新建關聯',
},
table_node_context_menu: {
edit_table: '編輯表格',
delete_table: '刪除表格',
},
snap_to_grid_tooltip: '對齊網格(按住 {{key}}',
tool_tips: {
double_click_to_edit: '雙擊以編輯',
},
},
};
export const zh_TWMetadata: LanguageMetadata = {
name: '繁體中文',
code: 'zh_TW',
};

View File

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

View File

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

View File

@@ -68,29 +68,46 @@ WITH fk_info${databaseEdition ? '_' + databaseEdition : ''} AS (
',"fk_def":"', replace(fk_def, '"', ''),
'"}')), ',') as fk_metadata
FROM (
SELECT connamespace::regnamespace::text AS schema_name,
conname AS foreign_key_name,
CASE
WHEN strpos(conrelid::regclass::text, '.') > 0
THEN split_part(conrelid::regclass::text, '.', 2)
ELSE conrelid::regclass::text
END AS table_name,
(regexp_matches(pg_get_constraintdef(oid), '(?i)FOREIGN KEY \\("?(\\w+)"?\\) REFERENCES (?:"?(\\w+)"?\\.)?"?(\\w+)"?\\("?(\\w+)"?\\)', 'g'))[1] AS fk_column,
(regexp_matches(pg_get_constraintdef(oid), '(?i)FOREIGN KEY \\("?(\\w+)"?\\) REFERENCES (?:"?(\\w+)"?\\.)?"?(\\w+)"?\\("?(\\w+)"?\\)', 'g'))[2] AS reference_schema,
(regexp_matches(pg_get_constraintdef(oid), '(?i)FOREIGN KEY \\("?(\\w+)"?\\) REFERENCES (?:"?(\\w+)"?\\.)?"?(\\w+)"?\\("?(\\w+)"?\\)', 'g'))[3] AS reference_table,
(regexp_matches(pg_get_constraintdef(oid), '(?i)FOREIGN KEY \\("?(\\w+)"?\\) REFERENCES (?:"?(\\w+)"?\\.)?"?(\\w+)"?\\("?(\\w+)"?\\)', 'g'))[4] AS reference_column,
pg_get_constraintdef(oid) as fk_def
FROM
pg_constraint
WHERE
contype = 'f'
AND connamespace::regnamespace::text NOT IN ('information_schema', 'pg_catalog')${
databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
? timescaleFilters
: databaseEdition === DatabaseEdition.POSTGRESQL_SUPABASE
? supabaseFilters
: ''
}
SELECT c.conname AS foreign_key_name,
n.nspname AS schema_name,
CASE
WHEN position('.' in conrelid::regclass::text) > 0
THEN split_part(conrelid::regclass::text, '.', 2)
ELSE conrelid::regclass::text
END AS table_name,
a.attname AS fk_column,
nr.nspname AS reference_schema,
CASE
WHEN position('.' in confrelid::regclass::text) > 0
THEN split_part(confrelid::regclass::text, '.', 2)
ELSE confrelid::regclass::text
END AS reference_table,
af.attname AS reference_column,
pg_get_constraintdef(c.oid) as fk_def
FROM
pg_constraint AS c
JOIN
pg_attribute AS a ON a.attnum = ANY(c.conkey) AND a.attrelid = c.conrelid
JOIN
pg_class AS cl ON cl.oid = c.conrelid
JOIN
pg_namespace AS n ON n.oid = cl.relnamespace
JOIN
pg_attribute AS af ON af.attnum = ANY(c.confkey) AND af.attrelid = c.confrelid
JOIN
pg_class AS clf ON clf.oid = c.confrelid
JOIN
pg_namespace AS nr ON nr.oid = clf.relnamespace
WHERE
c.contype = 'f'
AND connamespace::regnamespace::text NOT IN ('information_schema', 'pg_catalog')${
databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
? timescaleFilters
: databaseEdition ===
DatabaseEdition.POSTGRESQL_SUPABASE
? supabaseFilters
: ''
}
) AS x
), pk_info AS (
SELECT array_to_string(array_agg(CONCAT('{"schema":"', replace(schema_name, '"', ''), '"',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -35,6 +35,7 @@ import { DialogProvider } from '@/context/dialog-context/dialog-provider';
import { KeyboardShortcutsProvider } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts-provider';
import { Spinner } from '@/components/spinner/spinner';
import { Helmet } from 'react-helmet-async';
import { useStorage } from '@/hooks/use-storage';
const OPEN_STAR_US_AFTER_SECONDS = 30;
const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1;
@@ -73,6 +74,7 @@ const EditorPageComponent: React.FC = () => {
} = useLocalConfig();
const { toast } = useToast();
const { t } = useTranslation();
const { listDiagrams } = useStorage();
useEffect(() => {
if (!config) {
@@ -106,7 +108,15 @@ const EditorPageComponent: React.FC = () => {
navigate(`/diagrams/${config.defaultDiagramId}`);
}
} else {
openCreateDiagramDialog();
const diagrams = await listDiagrams();
if (diagrams.length > 0) {
const defaultDiagramId = diagrams[0].id;
await updateConfig({ defaultDiagramId });
navigate(`/diagrams/${defaultDiagramId}`);
} else {
openCreateDiagramDialog();
}
}
};
loadDefaultDiagram();
@@ -115,6 +125,7 @@ const EditorPageComponent: React.FC = () => {
openCreateDiagramDialog,
config,
navigate,
listDiagrams,
loadDiagram,
resetRedoStack,
resetUndoStack,

View File

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

View File

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

View File

@@ -30,16 +30,11 @@ import { useHistory } from '@/hooks/use-history';
import { useTranslation } from 'react-i18next';
import { useLayout } from '@/hooks/use-layout';
import { useTheme } from '@/hooks/use-theme';
import { enMetadata } from '@/i18n/locales/en';
import { esMetadata } from '@/i18n/locales/es';
import { deMetadata } from '@/i18n/locales/de';
import { jaMetadata } from '@/i18n/locales/ja';
import { useLocalConfig } from '@/hooks/use-local-config';
import { frMetadata } from '@/i18n/locales/fr';
import { hiMetadata } from '@/i18n/locales/hi';
import { DiagramName } from './diagram-name';
import { LastSaved } from './last-saved';
import { pt_BRMetadata } from '@/i18n/locales/pt_BR';
import { languages } from '@/i18n/i18n';
import { useNavigate } from 'react-router-dom';
export interface TopNavbarProps {}
@@ -53,6 +48,8 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
openImportDatabaseDialog,
showAlert,
openExportImageDialog,
openExportDiagramDialog,
openImportDiagramDialog,
} = useDialog();
const { setTheme, theme } = useTheme();
const { hideSidePanel, isSidePanelShowed, showSidePanel } = useLayout();
@@ -70,6 +67,12 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
const { isMd: isDesktop } = useBreakpoint('md');
const { config, updateConfig } = useConfig();
const { exportImage } = useExportImage();
const navigate = useNavigate();
const handleDeleteDiagramAction = useCallback(() => {
deleteDiagram();
navigate('/');
}, [deleteDiagram, navigate]);
const createNewDiagram = () => {
openCreateDiagramDialog();
@@ -203,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"
@@ -221,471 +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: deleteDiagram,
})
<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')
}
>
{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>
<MenubarCheckboxItem
onClick={() =>
changeLanguage(enMetadata.code)
changeLanguage(language.code)
}
checked={
i18n.language ===
enMetadata.code
i18n.language === language.code
}
>
{enMetadata.name}
{language.name}
</MenubarCheckboxItem>
<MenubarCheckboxItem
onClick={() =>
changeLanguage(esMetadata.code)
}
checked={
i18n.language ===
esMetadata.code
}
>
{esMetadata.name}
</MenubarCheckboxItem>
<MenubarCheckboxItem
onClick={() =>
changeLanguage(frMetadata.code)
}
checked={
i18n.language ===
frMetadata.code
}
>
{frMetadata.name}
</MenubarCheckboxItem>
<MenubarCheckboxItem
onClick={() =>
changeLanguage(deMetadata.code)
}
checked={
i18n.language ===
deMetadata.code
}
>
{deMetadata.name}
</MenubarCheckboxItem>
<MenubarCheckboxItem
onClick={() =>
changeLanguage(hiMetadata.code)
}
checked={
i18n.language ===
hiMetadata.code
}
>
{hiMetadata.name}
</MenubarCheckboxItem>
<MenubarCheckboxItem
onClick={() =>
changeLanguage(jaMetadata.code)
}
checked={
i18n.language ===
jaMetadata.code
}
>
{jaMetadata.name}
</MenubarCheckboxItem>
<MenubarCheckboxItem
onClick={() =>
changeLanguage(
pt_BRMetadata.code
)
}
checked={
i18n.language ===
pt_BRMetadata.code
}
>
{pt_BRMetadata.name}
</MenubarCheckboxItem>
</MenubarSubContent>
</MenubarSub>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>
{t('menu.help.help')}
</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={openChartDBIO}>
{t('menu.help.visit_website')}
</MenubarItem>
<MenubarItem onClick={openJoinDiscord}>
{t('menu.help.join_discord')}
</MenubarItem>
<MenubarItem onClick={openCalendly}>
{t('menu.help.schedule_a_call')}
</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
</div>
))}
</MenubarSubContent>
</MenubarSub>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>{t('menu.share.share')}</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={openExportDiagramDialog}>
{t('menu.share.export_diagram')}
</MenubarItem>
<MenubarItem onClick={openImportDiagramDialog}>
{t('menu.share.import_diagram')}
</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>{t('menu.help.help')}</MenubarTrigger>
<MenubarContent>
<MenubarItem onClick={openChartDBIO}>
{t('menu.help.visit_website')}
</MenubarItem>
<MenubarItem onClick={openJoinDiscord}>
{t('menu.help.join_discord')}
</MenubarItem>
<MenubarItem onClick={openCalendly}>
{t('menu.help.schedule_a_call')}
</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
</div>
{isDesktop ? (
<>
<div className="group flex flex-1 flex-row items-center justify-center">
<DiagramName />
</div>
<DiagramName />
<div className="hidden flex-1 items-center justify-end gap-2 sm:flex">
<LastSaved />
{renderStars()}
</div>
</>
) : (
<div className="flex flex-1 flex-row justify-between gap-2">
<div className="group flex flex-1 flex-row items-center">
<DiagramName />
</div>
<div className="flex items-center">
<LastSaved />
</div>
<div className="flex items-center">{renderStars()}</div>
<div className="flex flex-1 justify-center pb-2 pt-1">
<DiagramName />
</div>
)}
</nav>

View File

@@ -32,6 +32,7 @@ import { ReactFlowProvider } from '@xyflow/react';
import { ChartDBProvider } from '@/context/chartdb-context/chartdb-provider';
import { Helmet } from 'react-helmet-async';
import { APP_URL, HOST_URL } from '@/lib/env';
import { Link } from '@/components/link/link';
export interface TemplatePageLoaderData {
template: Template | undefined;
@@ -65,25 +66,22 @@ const TemplatePageComponent: React.FC = () => {
<Helmet>
{template ? (
<>
{HOST_URL !== 'https://chartdb.io' ? (
<link
rel="canonical"
href={`https://chartdb.io/templates/${templateSlug}`}
/>
) : null}
<title>
Database schema diagram for {template.name} |
ChartDB
{`Database schema diagram for - ${template.name} | ChartDB`}
</title>
<meta
name="title"
content={`Database schema for - ${template.name} | ChartDB`}
/>
<meta
name="description"
content={`${template.shortDescription}: ${template.description}`}
/>
<meta
name="keywords"
content={`${template.keywords.join(', ')}`}
/>
<meta
property="og:title"
content={`Database schema for - ${template.name} | ChartDB`}
content={`Database schema diagram for - ${template.name} | ChartDB`}
/>
<meta
property="og:url"
@@ -98,7 +96,7 @@ const TemplatePageComponent: React.FC = () => {
content={`${HOST_URL}${template.image}`}
/>
<meta property="og:type" content="website" />
<meta property="og:site_name" content="ChartDB" />
<meta
name="twitter:title"
content={`Database schema for - ${template.name} | ChartDB`}
@@ -115,6 +113,8 @@ const TemplatePageComponent: React.FC = () => {
name="twitter:card"
content="summary_large_image"
/>
<meta name="twitter:site" content="@ChartDB_io" />
<meta name="twitter:creator" content="@ChartDB_io" />
</>
) : (
<title>Database Schema Diagram | ChartDB</title>
@@ -174,8 +174,11 @@ const TemplatePageComponent: React.FC = () => {
</Breadcrumb>
<div className="flex flex-col items-center gap-4 md:flex-row md:items-start md:justify-between md:gap-0">
<div className="flex flex-col pr-0 md:pr-20">
<h1 className="font-primary text-2xl font-bold">
<h1 className="flex flex-col font-primary text-2xl font-bold">
{template?.name}
<span className="text-sm font-normal text-muted-foreground">
Database schema diagram
</span>
</h1>
<h2 className="mt-3">
<span className="font-semibold">
@@ -244,6 +247,25 @@ const TemplatePageComponent: React.FC = () => {
</span>
</div>
</div>
<Separator />
{template.url ? (
<>
<div>
<h4 className="mb-1 text-base font-semibold md:text-left">
Url
</h4>
<Link
className="break-all text-sm text-muted-foreground"
href={`${template.url}?ref=chartdb`}
target="_blank"
>
{template.url}
</Link>
</div>
<Separator />
</>
) : null}
<div>
<h4 className="mb-1 text-base font-semibold md:text-left">
Tags

View File

@@ -26,7 +26,7 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({ template }) => {
className="h-2 rounded-t-[6px]"
style={{ backgroundColor: randomColor() }}
></div>
<div className="grow overflow-hidden p-1">
<div className="overflow-hidden p-1">
<img
src={
effectiveTheme === 'dark'
@@ -43,7 +43,7 @@ export const TemplateCard: React.FC<TemplateCardProps> = ({ template }) => {
{template.name}
</h3>
</div>
<div className="flex flex-row">
<div className="flex h-full flex-col justify-start pt-1">
<Tooltip>
<TooltipTrigger className="mr-1">
<img

View File

@@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import React from 'react';
import ChartDBLogo from '@/assets/logo-light.png';
import ChartDBDarkLogo from '@/assets/logo-dark.png';
import { useTheme } from '@/hooks/use-theme';
@@ -7,54 +7,67 @@ import { ThemeProvider } from '@/context/theme-context/theme-provider';
import { Component, Star } from 'lucide-react';
import { ListMenu } from '@/components/list-menu/list-menu';
import { TemplateCard } from './template-card/template-card';
import { useMatches, useParams } from 'react-router-dom';
import { useLoaderData, useMatches, useParams } from 'react-router-dom';
import type { Template } from '@/templates-data/templates-data';
import { Spinner } from '@/components/spinner/spinner';
import { removeDups } from '@/lib/utils';
import { Helmet } from 'react-helmet-async';
import { HOST_URL } from '@/lib/env';
export interface TemplatesPageLoaderData {
templates: Template[] | undefined;
allTags: string[] | undefined;
}
const TemplatesPageComponent: React.FC = () => {
const { effectiveTheme } = useTheme();
const data = useLoaderData() as TemplatesPageLoaderData;
const { templates, allTags } = data ?? {};
const { tag } = useParams<{ tag: string }>();
const matches = useMatches();
const [templates, setTemplates] = React.useState<Template[]>();
const [tags, setTags] = React.useState<string[]>();
const isFeatured = matches.some(
(match) => match.id === 'templates_featured'
);
const isAllTemplates = matches.some((match) => match.id === 'templates');
const isTags = matches.some((match) => match.id === 'templates_tags');
useEffect(() => {
const loadTemplates = async () => {
const { templates: loadedTemplates } = await import(
'@/templates-data/templates-data'
);
let templatesToLoad = loadedTemplates;
if (isFeatured) {
templatesToLoad = loadedTemplates.filter((t) => t.featured);
}
if (isTags && tag) {
templatesToLoad = loadedTemplates.filter((t) =>
t.tags.includes(tag)
);
}
setTemplates(templatesToLoad);
setTags(removeDups(loadedTemplates?.flatMap((t) => t.tags) ?? []));
};
loadTemplates();
}, [isFeatured, isTags, tag]);
return (
<>
<Helmet>
<title>ChartDB - Database Schema Templates</title>
{HOST_URL !== 'https://chartdb.io' ? (
<link rel="canonical" href="https://chartdb.io/templates" />
) : null}
<title>Database Schema Diagram Templates | ChartDB</title>
<meta
name="description"
content="Discover a collection of real-world database schema diagrams, featuring example applications and popular open-source projects."
/>
<meta
property="og:title"
content="Database Schema Diagram Templates | ChartDB"
/>
<meta property="og:url" content={`${HOST_URL}/templates`} />
<meta
property="og:description"
content="Discover a collection of real-world database schema diagrams, featuring example applications and popular open-source projects."
/>
<meta property="og:image" content={`${HOST_URL}/chartdb.png`} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="ChartDB" />
<meta
name="twitter:title"
content="Database Schema Diagram Templates | ChartDB"
/>
<meta
name="twitter:description"
content="Discover a collection of real-world database schema diagrams, featuring example applications and popular open-source projects."
/>
<meta
name="twitter:image"
content={`${HOST_URL}/chartdb.png`}
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@ChartDB_io" />
<meta name="twitter:creator" content="@ChartDB_io" />
</Helmet>
<section className="flex w-screen flex-col bg-background">
@@ -92,10 +105,9 @@ const TemplatesPageComponent: React.FC = () => {
Database Schema Templates
</h1>
<h2 className="mt-1 font-primary text-base text-muted-foreground">
Explore a collection of real-world database schemas
drawn from real-world live applications and open-source
projects. Use these as a foundation or source of
inspiration when designing your apps architecture.
Discover a collection of real-world database schema
diagrams, featuring example applications and popular
open-source projects.
</h2>
{!templates ? (
<Spinner
@@ -125,10 +137,10 @@ const TemplatesPageComponent: React.FC = () => {
<h4 className="mt-4 text-left text-sm font-semibold">
Tags
</h4>
{tags ? (
{allTags ? (
<ListMenu
className="mt-1 w-44 shrink-0"
items={tags.map((currentTag) => ({
className="mt-1 shrink-0"
items={allTags.map((currentTag) => ({
title: currentTag,
href: `/templates/tags/${currentTag}`,
selected: tag === currentTag,

View File

@@ -2,6 +2,8 @@ import React from 'react';
import type { RouteObject } from 'react-router-dom';
import { createBrowserRouter } from 'react-router-dom';
import type { TemplatePageLoaderData } from './pages/template-page/template-page';
import type { TemplatesPageLoaderData } from './pages/templates-page/templates-page';
import { getTemplatesAndAllTags } from './templates-data/template-utils';
const routes: RouteObject[] = [
...['', 'diagrams/:diagramId'].map((path) => ({
@@ -38,6 +40,15 @@ const routes: RouteObject[] = [
element: <TemplatesPage />,
};
},
loader: async (): Promise<TemplatesPageLoaderData> => {
const { tags, templates } = await getTemplatesAndAllTags();
return {
allTags: tags,
templates,
};
},
},
{
id: 'templates_featured',
@@ -50,6 +61,16 @@ const routes: RouteObject[] = [
element: <TemplatesPage />,
};
},
loader: async (): Promise<TemplatesPageLoaderData> => {
const { tags, templates } = await getTemplatesAndAllTags({
featured: true,
});
return {
allTags: tags,
templates,
};
},
},
{
id: 'templates_tags',
@@ -62,6 +83,16 @@ const routes: RouteObject[] = [
element: <TemplatesPage />,
};
},
loader: async ({ params }): Promise<TemplatesPageLoaderData> => {
const { tags, templates } = await getTemplatesAndAllTags({
tag: params.tag,
});
return {
allTags: tags,
templates,
};
},
},
{
id: 'templates_templateSlug',

View File

@@ -1,89 +1,42 @@
import type { Diagram } from '@/lib/domain/diagram';
import type { Template } from './templates-data';
import { generateId } 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,
};
};
export const getTemplatesAndAllTags = async ({
featured,
tag,
}: {
featured?: boolean;
tag?: string;
} = {}): Promise<{ templates: Template[]; tags: string[] }> => {
const { templates } = await import('@/templates-data/templates-data');
const allTags = removeDups(templates?.flatMap((t) => t.tags) ?? []);
if (featured) {
return {
templates: templates.filter((t) => t.featured),
tags: allTags,
};
}
if (tag) {
return {
templates: templates.filter((t) => t.tags.includes(tag)),
tags: allTags,
};
}
return { templates, tags: allTags };
};

View File

@@ -1,6 +1,19 @@
import type { Diagram } from '@/lib/domain/diagram';
import { employeeDb } from './templates/employee-db';
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;
@@ -11,9 +24,24 @@ export interface Template {
imageDark: string;
diagram: Diagram;
tags: string[];
keywords: string[];
featured: boolean;
url?: string;
}
export const templates: Template[] = [employeeDb, visualNovelDb];
export const templates: Template[] = [
employeeDb,
pokemonDb,
airbnbDb,
wordpressDb,
djangoDb,
laravelDb,
twitterDb,
visualNovelDb,
adonisAclDb,
akauntingDb,
laravelSparkDb,
voyagerDb,
koelDb,
laravelPermissionDb,
gravityDb,
];

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,31 +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 | vndb',
shortDescription: 'The Visual Novel Database | vndb',
description: 'A comprehensive database for information about visual novels',
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'],
keywords: [
'VNDB',
'visual novel database schema',
'visual novel database template',
'database schema visualization',
'visual novel database design',
'ChartDB',
'VNDB schema diagram',
'relational database structure',
'VNDB development',
'VNDB tables',
'database template for VNDB',
],
tags: ['Postgres', 'Visual Novel Database'],
featured: true,
url: 'https://vndb.org',
diagram: {
id: 'visual_novel_db',
name: 'visual-novel-db',
name: 'visual-novel-database',
createdAt: new Date(),
updatedAt: new Date(),
databaseType: DatabaseType.POSTGRESQL,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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