mirror of
https://github.com/chartdb/chartdb.git
synced 2025-10-23 07:11:56 +00:00
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
44eac7daff | ||
|
502472b083 | ||
|
52d2ea596c | ||
|
bd67ccfbcf | ||
|
62beb68fa1 | ||
|
09b1275475 | ||
|
5dd7fe75d1 | ||
|
2939320a15 | ||
|
a643852837 | ||
|
467ff697c9 | ||
|
d6919f3033 | ||
|
56382a9fdc | ||
|
e06eb2a48e | ||
|
543b716c77 | ||
|
b55d631146 | ||
|
ef118929ad | ||
|
68f48190c9 | ||
|
bba265ad43 | ||
|
cbc4e85a14 | ||
|
26a0a5b550 | ||
|
b935b7f251 | ||
|
a1c0cf102a | ||
|
ab89bad6d5 | ||
|
deb218423f | ||
|
48342471ac |
36
CHANGELOG.md
36
CHANGELOG.md
@@ -1,5 +1,41 @@
|
||||
# Changelog
|
||||
|
||||
## [1.9.0](https://github.com/chartdb/chartdb/compare/v1.8.1...v1.9.0) (2025-03-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **canvas:** highlight the Show-All button when No-Tables are visible in the canvas ([#612](https://github.com/chartdb/chartdb/issues/612)) ([62beb68](https://github.com/chartdb/chartdb/commit/62beb68fa1ec22ccd4fe5e59a8ceb9d3e8f6d374))
|
||||
* **chart max length:** add support for edit char max length ([#613](https://github.com/chartdb/chartdb/issues/613)) ([09b1275](https://github.com/chartdb/chartdb/commit/09b12754757b9625ca287d91a92cf0d83c9e2b89))
|
||||
* **chart max length:** enable edit length from data type select box ([#616](https://github.com/chartdb/chartdb/issues/616)) ([bd67ccf](https://github.com/chartdb/chartdb/commit/bd67ccfbcf66b919453ca6c0bfd71e16772b3d8e))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cardinality:** set true as default ([#583](https://github.com/chartdb/chartdb/issues/583)) ([2939320](https://github.com/chartdb/chartdb/commit/2939320a15a9ccd9eccfe46c26e04ca1edca2420))
|
||||
* **performance:** Optimize performance of field comments editing ([#610](https://github.com/chartdb/chartdb/issues/610)) ([5dd7fe7](https://github.com/chartdb/chartdb/commit/5dd7fe75d1b0378ba406c75183c5e2356730c3b4))
|
||||
* remove Buckle dialog ([#617](https://github.com/chartdb/chartdb/issues/617)) ([502472b](https://github.com/chartdb/chartdb/commit/502472b08342be425e66e2b6c94e5fe37ba14aa9))
|
||||
* **shorcuts:** add shortcut to toggle the theme ([#602](https://github.com/chartdb/chartdb/issues/602)) ([a643852](https://github.com/chartdb/chartdb/commit/a6438528375ab54d3ec7d80ac6b6ddd65ea8cf1e))
|
||||
|
||||
## [1.8.1](https://github.com/chartdb/chartdb/compare/v1.8.0...v1.8.1) (2025-03-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **add-docs:** add link to ChartDB documentation ([#597](https://github.com/chartdb/chartdb/issues/597)) ([b55d631](https://github.com/chartdb/chartdb/commit/b55d631146ff3a1f7d63c800d44b5d3d3a223c76))
|
||||
* components config ([#591](https://github.com/chartdb/chartdb/issues/591)) ([cbc4e85](https://github.com/chartdb/chartdb/commit/cbc4e85a14e24a43f9ff470518f8fe2845046bdb))
|
||||
* **docker config:** Environment Variable Handling and Configuration Logic ([#605](https://github.com/chartdb/chartdb/issues/605)) ([d6919f3](https://github.com/chartdb/chartdb/commit/d6919f30336cc846fe6e6505b5a5278aa14dcce6))
|
||||
* **empty-state:** show diff buttons on import-dbml when triggered by empty ([#574](https://github.com/chartdb/chartdb/issues/574)) ([4834247](https://github.com/chartdb/chartdb/commit/48342471ac231922f2ca4455b74a9879127a54f1))
|
||||
* **i18n:** add [FR] translation ([#579](https://github.com/chartdb/chartdb/issues/579)) ([ab89bad](https://github.com/chartdb/chartdb/commit/ab89bad6d544ba4c339a3360eeec7d29e5579511))
|
||||
* **img-export:** add ChartDB watermark to exported image ([#588](https://github.com/chartdb/chartdb/issues/588)) ([b935b7f](https://github.com/chartdb/chartdb/commit/b935b7f25111d5f72b7f8d7c552a4ea5974f791e))
|
||||
* **import-mssql:** fix import/export scripts to handle data correctly ([#598](https://github.com/chartdb/chartdb/issues/598)) ([e06eb2a](https://github.com/chartdb/chartdb/commit/e06eb2a48e6bd3bcf352f4bcf128214c7da4c1b1))
|
||||
* **menu-backup:** update export to be backup ([#590](https://github.com/chartdb/chartdb/issues/590)) ([26a0a5b](https://github.com/chartdb/chartdb/commit/26a0a5b550ef5e47e89b00d0232dc98936f63f23))
|
||||
* open create new diagram when there is no diagram ([#594](https://github.com/chartdb/chartdb/issues/594)) ([ef11892](https://github.com/chartdb/chartdb/commit/ef118929ad5d5cbfae0290061bd8ea30bd262496))
|
||||
* **open diagram:** in case there is no diagram, opens the dialog ([#593](https://github.com/chartdb/chartdb/issues/593)) ([68f4819](https://github.com/chartdb/chartdb/commit/68f48190c93f155398cca15dd7af2a025de2d45f))
|
||||
* **side-panel:** simplify how to add field and index ([#573](https://github.com/chartdb/chartdb/issues/573)) ([a1c0cf1](https://github.com/chartdb/chartdb/commit/a1c0cf102add4fb235e913e75078139b3961341b))
|
||||
* **sql_server_export:** use sql server export ([#600](https://github.com/chartdb/chartdb/issues/600)) ([56382a9](https://github.com/chartdb/chartdb/commit/56382a9fdc5e3044f8811873dd8a79f590771896))
|
||||
* **sqlite-import:** import nuallable columns correctly + add json type ([#571](https://github.com/chartdb/chartdb/issues/571)) ([deb2184](https://github.com/chartdb/chartdb/commit/deb218423f77f0c0945a93005696456f62b00ce3))
|
||||
|
||||
## [1.8.0](https://github.com/chartdb/chartdb/compare/v1.7.0...v1.8.0) (2025-02-13)
|
||||
|
||||
|
||||
|
@@ -1,17 +1,20 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "src/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "src/components",
|
||||
"utils": "src/lib/utils",
|
||||
"ui": "src/components/ui",
|
||||
"lib": "src/lib",
|
||||
"hooks": "src/hooks"
|
||||
}
|
||||
}
|
||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "chartdb",
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "chartdb",
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^0.0.51",
|
||||
"@dbml/core": "^3.9.5",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "chartdb",
|
||||
"private": true,
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
@@ -24,12 +24,19 @@ export interface SelectBoxOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
regex?: string;
|
||||
extractRegex?: RegExp;
|
||||
}
|
||||
|
||||
export interface SelectBoxProps {
|
||||
options: SelectBoxOption[];
|
||||
value?: string[] | string;
|
||||
onChange?: (values: string[] | string) => void;
|
||||
valueSuffix?: string;
|
||||
optionSuffix?: (option: SelectBoxOption) => string;
|
||||
onChange?: (
|
||||
values: string[] | string,
|
||||
regexMatches?: string[] | string
|
||||
) => void;
|
||||
placeholder?: string;
|
||||
inputPlaceholder?: string;
|
||||
emptyPlaceholder?: string;
|
||||
@@ -55,10 +62,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
className,
|
||||
options,
|
||||
value,
|
||||
valueSuffix,
|
||||
onChange,
|
||||
multiple,
|
||||
oneLine,
|
||||
selectAll,
|
||||
optionSuffix,
|
||||
deselectAll,
|
||||
clearText,
|
||||
showClear,
|
||||
@@ -86,7 +95,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
);
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(selectedValue: string) => {
|
||||
(selectedValue: string, regexMatches?: string[]) => {
|
||||
if (multiple) {
|
||||
const newValue =
|
||||
value?.includes(selectedValue) && Array.isArray(value)
|
||||
@@ -94,7 +103,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
: [...(value ?? []), selectedValue];
|
||||
onChange?.(newValue);
|
||||
} else {
|
||||
onChange?.(selectedValue);
|
||||
onChange?.(selectedValue, regexMatches);
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
@@ -199,6 +208,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
(opt) => opt.value === value
|
||||
)?.label
|
||||
}
|
||||
{valueSuffix ? valueSuffix : ''}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
@@ -239,11 +249,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
align="center"
|
||||
>
|
||||
<Command
|
||||
filter={(value, search) =>
|
||||
value.toLowerCase().includes(search.toLowerCase())
|
||||
filter={(value, search, keywords) => {
|
||||
if (
|
||||
keywords?.length &&
|
||||
keywords.some((keyword) =>
|
||||
new RegExp(keyword).test(search)
|
||||
)
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return value
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase())
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
: 0;
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<CommandInput
|
||||
@@ -302,14 +323,36 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
const isSelected =
|
||||
Array.isArray(value) &&
|
||||
value.includes(option.value);
|
||||
|
||||
const isRegexMatch =
|
||||
option.regex &&
|
||||
new RegExp(option.regex)?.test(
|
||||
searchTerm
|
||||
);
|
||||
|
||||
const matches = option.extractRegex
|
||||
? searchTerm.match(
|
||||
option.extractRegex
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
className="flex items-center"
|
||||
key={option.value}
|
||||
keywords={
|
||||
option.regex
|
||||
? [option.regex]
|
||||
: undefined
|
||||
}
|
||||
// value={option.value}
|
||||
onSelect={() =>
|
||||
handleSelect(
|
||||
option.value
|
||||
option.value,
|
||||
matches?.map(
|
||||
(match) =>
|
||||
match.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -327,7 +370,15 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
)}
|
||||
<div className="flex items-center truncate">
|
||||
<span>
|
||||
{option.label}
|
||||
{isRegexMatch
|
||||
? searchTerm
|
||||
: option.label}
|
||||
{!isRegexMatch &&
|
||||
optionSuffix
|
||||
? optionSuffix(
|
||||
option
|
||||
)
|
||||
: ''}
|
||||
</span>
|
||||
{option.description && (
|
||||
<span className="ml-1 text-xs text-muted-foreground">
|
||||
@@ -337,19 +388,20 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!multiple &&
|
||||
{((!multiple &&
|
||||
option.value ===
|
||||
value && (
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
option.value ===
|
||||
value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
value) ||
|
||||
isRegexMatch) && (
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
option.value ===
|
||||
value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
|
@@ -22,6 +22,8 @@ import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
import { useEventEmitter } from 'ahooks';
|
||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||
import { storageInitialValue } from '../storage-context/storage-context';
|
||||
import { useDiff } from '../diff-context/use-diff';
|
||||
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
|
||||
|
||||
export interface ChartDBProviderProps {
|
||||
diagram?: Diagram;
|
||||
@@ -30,7 +32,8 @@ export interface ChartDBProviderProps {
|
||||
|
||||
export const ChartDBProvider: React.FC<
|
||||
React.PropsWithChildren<ChartDBProviderProps>
|
||||
> = ({ children, diagram, readonly }) => {
|
||||
> = ({ children, diagram, readonly: readonlyProp }) => {
|
||||
const { hasDiff } = useDiff();
|
||||
let db = useStorage();
|
||||
const events = useEventEmitter<ChartDBEvent>();
|
||||
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
||||
@@ -53,9 +56,33 @@ export const ChartDBProvider: React.FC<
|
||||
const [dependencies, setDependencies] = useState<DBDependency[]>(
|
||||
diagram?.dependencies ?? []
|
||||
);
|
||||
const { events: diffEvents } = useDiff();
|
||||
|
||||
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
|
||||
const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
|
||||
setTables((tables) =>
|
||||
[...tables, ...(tablesAdded ?? [])].map((table) => {
|
||||
const fields = fieldsAdded.get(table.id);
|
||||
return fields
|
||||
? { ...table, fields: [...table.fields, ...fields] }
|
||||
: table;
|
||||
})
|
||||
);
|
||||
setRelationships((relationships) => [
|
||||
...relationships,
|
||||
...(relationshipsAdded ?? []),
|
||||
]);
|
||||
}, []);
|
||||
|
||||
diffEvents.useSubscription(diffCalculatedHandler);
|
||||
|
||||
const defaultSchemaName = defaultSchemas[databaseType];
|
||||
|
||||
const readonly = useMemo(
|
||||
() => readonlyProp ?? hasDiff ?? false,
|
||||
[readonlyProp, hasDiff]
|
||||
);
|
||||
|
||||
if (readonly) {
|
||||
db = storageInitialValue;
|
||||
}
|
||||
|
@@ -7,6 +7,8 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
|
||||
import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
|
||||
import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
|
||||
import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
|
||||
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
||||
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
|
||||
|
||||
export interface DialogContext {
|
||||
// Create diagram dialog
|
||||
@@ -14,7 +16,9 @@ export interface DialogContext {
|
||||
closeCreateDiagramDialog: () => void;
|
||||
|
||||
// Open diagram dialog
|
||||
openOpenDiagramDialog: () => void;
|
||||
openOpenDiagramDialog: (
|
||||
params?: Omit<OpenDiagramDialogProps, 'dialog'>
|
||||
) => void;
|
||||
closeOpenDiagramDialog: () => void;
|
||||
|
||||
// Export SQL dialog
|
||||
@@ -43,10 +47,6 @@ export interface DialogContext {
|
||||
openStarUsDialog: () => void;
|
||||
closeStarUsDialog: () => void;
|
||||
|
||||
// Buckle dialog
|
||||
openBuckleDialog: () => void;
|
||||
closeBuckleDialog: () => void;
|
||||
|
||||
// Export image dialog
|
||||
openExportImageDialog: (
|
||||
params: Omit<ExportImageDialogProps, 'dialog'>
|
||||
@@ -66,7 +66,9 @@ export interface DialogContext {
|
||||
closeImportDiagramDialog: () => void;
|
||||
|
||||
// Import DBML dialog
|
||||
openImportDBMLDialog: () => void;
|
||||
openImportDBMLDialog: (
|
||||
params?: Omit<ImportDBMLDialogProps, 'dialog'>
|
||||
) => void;
|
||||
closeImportDBMLDialog: () => void;
|
||||
}
|
||||
|
||||
@@ -91,8 +93,6 @@ export const dialogContext = createContext<DialogContext>({
|
||||
closeExportDiagramDialog: emptyFn,
|
||||
openImportDiagramDialog: emptyFn,
|
||||
closeImportDiagramDialog: emptyFn,
|
||||
openBuckleDialog: emptyFn,
|
||||
closeBuckleDialog: emptyFn,
|
||||
openImportDBMLDialog: emptyFn,
|
||||
closeImportDBMLDialog: emptyFn,
|
||||
});
|
||||
|
@@ -2,6 +2,7 @@ import React, { useCallback, useState } from 'react';
|
||||
import type { DialogContext } from './dialog-context';
|
||||
import { dialogContext } from './dialog-context';
|
||||
import { CreateDiagramDialog } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
|
||||
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
|
||||
import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
|
||||
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
||||
import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
||||
@@ -18,7 +19,7 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
|
||||
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';
|
||||
import { BuckleDialog } from '@/dialogs/buckle-dialog/buckle-dialog';
|
||||
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
||||
import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
||||
|
||||
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
@@ -26,6 +27,17 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
}) => {
|
||||
const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false);
|
||||
const [openOpenDiagramDialog, setOpenOpenDiagramDialog] = useState(false);
|
||||
const [openDiagramDialogParams, setOpenDiagramDialogParams] =
|
||||
useState<Omit<OpenDiagramDialogProps, 'dialog'>>();
|
||||
|
||||
const openOpenDiagramDialogHandler: DialogContext['openOpenDiagramDialog'] =
|
||||
useCallback(
|
||||
(props) => {
|
||||
setOpenDiagramDialogParams(props);
|
||||
setOpenOpenDiagramDialog(true);
|
||||
},
|
||||
[setOpenOpenDiagramDialog]
|
||||
);
|
||||
|
||||
const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] =
|
||||
useState(false);
|
||||
@@ -41,7 +53,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
);
|
||||
|
||||
const [openStarUsDialog, setOpenStarUsDialog] = useState(false);
|
||||
const [openBuckleDialog, setOpenBuckleDialog] = useState(false);
|
||||
|
||||
// Export image dialog
|
||||
const [openExportImageDialog, setOpenExportImageDialog] = useState(false);
|
||||
@@ -111,13 +122,15 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
|
||||
// Import DBML dialog
|
||||
const [openImportDBMLDialog, setOpenImportDBMLDialog] = useState(false);
|
||||
const [importDBMLDialogParams, setImportDBMLDialogParams] =
|
||||
useState<Omit<ImportDBMLDialogProps, 'dialog'>>();
|
||||
|
||||
return (
|
||||
<dialogContext.Provider
|
||||
value={{
|
||||
openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
|
||||
closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
|
||||
openOpenDiagramDialog: () => setOpenOpenDiagramDialog(true),
|
||||
openOpenDiagramDialog: openOpenDiagramDialogHandler,
|
||||
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
|
||||
openExportSQLDialog: openExportSQLDialogHandler,
|
||||
closeExportSQLDialog: () => setOpenExportSQLDialog(false),
|
||||
@@ -132,8 +145,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
closeTableSchemaDialog: () => setOpenTableSchemaDialog(false),
|
||||
openStarUsDialog: () => setOpenStarUsDialog(true),
|
||||
closeStarUsDialog: () => setOpenStarUsDialog(false),
|
||||
closeBuckleDialog: () => setOpenBuckleDialog(false),
|
||||
openBuckleDialog: () => setOpenBuckleDialog(true),
|
||||
closeExportImageDialog: () => setOpenExportImageDialog(false),
|
||||
openExportImageDialog: openExportImageDialogHandler,
|
||||
openExportDiagramDialog: () => setOpenExportDiagramDialog(true),
|
||||
@@ -142,13 +153,19 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
|
||||
closeImportDiagramDialog: () =>
|
||||
setOpenImportDiagramDialog(false),
|
||||
openImportDBMLDialog: () => setOpenImportDBMLDialog(true),
|
||||
openImportDBMLDialog: (params) => {
|
||||
setImportDBMLDialogParams(params);
|
||||
setOpenImportDBMLDialog(true);
|
||||
},
|
||||
closeImportDBMLDialog: () => setOpenImportDBMLDialog(false),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} />
|
||||
<OpenDiagramDialog dialog={{ open: openOpenDiagramDialog }} />
|
||||
<OpenDiagramDialog
|
||||
dialog={{ open: openOpenDiagramDialog }}
|
||||
{...openDiagramDialogParams}
|
||||
/>
|
||||
<ExportSQLDialog
|
||||
dialog={{ open: openExportSQLDialog }}
|
||||
{...exportSQLDialogParams}
|
||||
@@ -172,8 +189,10 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
/>
|
||||
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
|
||||
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
|
||||
<BuckleDialog dialog={{ open: openBuckleDialog }} />
|
||||
<ImportDBMLDialog dialog={{ open: openImportDBMLDialog }} />
|
||||
<ImportDBMLDialog
|
||||
dialog={{ open: openImportDBMLDialog }}
|
||||
{...importDBMLDialogParams}
|
||||
/>
|
||||
</dialogContext.Provider>
|
||||
);
|
||||
};
|
||||
|
433
src/context/diff-context/diff-check/diff-check.ts
Normal file
433
src/context/diff-context/diff-check/diff-check.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import type {
|
||||
ChartDBDiff,
|
||||
DiffMap,
|
||||
DiffObject,
|
||||
FieldDiffAttribute,
|
||||
} from '../types';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import type { DBIndex } from '@/lib/domain/db-index';
|
||||
|
||||
export function getDiffMapKey({
|
||||
diffObject,
|
||||
objectId,
|
||||
attribute,
|
||||
}: {
|
||||
diffObject: DiffObject;
|
||||
objectId: string;
|
||||
attribute?: string;
|
||||
}): string {
|
||||
return attribute
|
||||
? `${diffObject}-${attribute}-${objectId}`
|
||||
: `${diffObject}-${objectId}`;
|
||||
}
|
||||
|
||||
export function generateDiff({
|
||||
diagram,
|
||||
newDiagram,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
newDiagram: Diagram;
|
||||
}): {
|
||||
diffMap: DiffMap;
|
||||
changedTables: Map<string, boolean>;
|
||||
changedFields: Map<string, boolean>;
|
||||
} {
|
||||
const newDiffs = new Map<string, ChartDBDiff>();
|
||||
const changedTables = new Map<string, boolean>();
|
||||
const changedFields = new Map<string, boolean>();
|
||||
|
||||
// Compare tables
|
||||
compareTables({ diagram, newDiagram, diffMap: newDiffs, changedTables });
|
||||
|
||||
// Compare fields and indexes for matching tables
|
||||
compareTableContents({
|
||||
diagram,
|
||||
newDiagram,
|
||||
diffMap: newDiffs,
|
||||
changedTables,
|
||||
changedFields,
|
||||
});
|
||||
|
||||
// Compare relationships
|
||||
compareRelationships({ diagram, newDiagram, diffMap: newDiffs });
|
||||
|
||||
return { diffMap: newDiffs, changedTables, changedFields };
|
||||
}
|
||||
|
||||
// Compare tables between diagrams
|
||||
function compareTables({
|
||||
diagram,
|
||||
newDiagram,
|
||||
diffMap,
|
||||
changedTables,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
newDiagram: Diagram;
|
||||
diffMap: DiffMap;
|
||||
changedTables: Map<string, boolean>;
|
||||
}) {
|
||||
const oldTables = diagram.tables || [];
|
||||
const newTables = newDiagram.tables || [];
|
||||
|
||||
// Check for added tables
|
||||
for (const newTable of newTables) {
|
||||
if (!oldTables.find((t) => t.id === newTable.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({ diffObject: 'table', objectId: newTable.id }),
|
||||
{
|
||||
object: 'table',
|
||||
type: 'added',
|
||||
tableId: newTable.id,
|
||||
}
|
||||
);
|
||||
changedTables.set(newTable.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for removed tables
|
||||
for (const oldTable of oldTables) {
|
||||
if (!newTables.find((t) => t.id === oldTable.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({ diffObject: 'table', objectId: oldTable.id }),
|
||||
{
|
||||
object: 'table',
|
||||
type: 'removed',
|
||||
tableId: oldTable.id,
|
||||
}
|
||||
);
|
||||
changedTables.set(oldTable.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for table name and comments changes
|
||||
for (const oldTable of oldTables) {
|
||||
const newTable = newTables.find((t) => t.id === oldTable.id);
|
||||
|
||||
if (!newTable) continue;
|
||||
|
||||
if (oldTable.name !== newTable.name) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: oldTable.id,
|
||||
attribute: 'name',
|
||||
}),
|
||||
{
|
||||
object: 'table',
|
||||
type: 'changed',
|
||||
tableId: oldTable.id,
|
||||
attributes: 'name',
|
||||
newValue: newTable.name,
|
||||
oldValue: oldTable.name,
|
||||
}
|
||||
);
|
||||
|
||||
changedTables.set(oldTable.id, true);
|
||||
}
|
||||
|
||||
if (oldTable.comments !== newTable.comments) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: oldTable.id,
|
||||
attribute: 'comments',
|
||||
}),
|
||||
{
|
||||
object: 'table',
|
||||
type: 'changed',
|
||||
tableId: oldTable.id,
|
||||
attributes: 'comments',
|
||||
newValue: newTable.comments,
|
||||
oldValue: oldTable.comments,
|
||||
}
|
||||
);
|
||||
|
||||
changedTables.set(oldTable.id, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compare fields and indexes for matching tables
|
||||
function compareTableContents({
|
||||
diagram,
|
||||
newDiagram,
|
||||
diffMap,
|
||||
changedTables,
|
||||
changedFields,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
newDiagram: Diagram;
|
||||
diffMap: DiffMap;
|
||||
changedTables: Map<string, boolean>;
|
||||
changedFields: Map<string, boolean>;
|
||||
}) {
|
||||
const oldTables = diagram.tables || [];
|
||||
const newTables = newDiagram.tables || [];
|
||||
|
||||
// For each table that exists in both diagrams
|
||||
for (const oldTable of oldTables) {
|
||||
const newTable = newTables.find((t) => t.id === oldTable.id);
|
||||
if (!newTable) continue;
|
||||
|
||||
// Compare fields
|
||||
compareFields({
|
||||
tableId: oldTable.id,
|
||||
oldFields: oldTable.fields,
|
||||
newFields: newTable.fields,
|
||||
diffMap,
|
||||
changedTables,
|
||||
changedFields,
|
||||
});
|
||||
|
||||
// Compare indexes
|
||||
compareIndexes({
|
||||
tableId: oldTable.id,
|
||||
oldIndexes: oldTable.indexes,
|
||||
newIndexes: newTable.indexes,
|
||||
diffMap,
|
||||
changedTables,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Compare fields between tables
|
||||
function compareFields({
|
||||
tableId,
|
||||
oldFields,
|
||||
newFields,
|
||||
diffMap,
|
||||
changedTables,
|
||||
changedFields,
|
||||
}: {
|
||||
tableId: string;
|
||||
oldFields: DBField[];
|
||||
newFields: DBField[];
|
||||
diffMap: DiffMap;
|
||||
changedTables: Map<string, boolean>;
|
||||
changedFields: Map<string, boolean>;
|
||||
}) {
|
||||
// Check for added fields
|
||||
for (const newField of newFields) {
|
||||
if (!oldFields.find((f) => f.id === newField.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: newField.id,
|
||||
}),
|
||||
{
|
||||
object: 'field',
|
||||
type: 'added',
|
||||
fieldId: newField.id,
|
||||
tableId,
|
||||
}
|
||||
);
|
||||
changedTables.set(tableId, true);
|
||||
changedFields.set(newField.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for removed fields
|
||||
for (const oldField of oldFields) {
|
||||
if (!newFields.find((f) => f.id === oldField.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: oldField.id,
|
||||
}),
|
||||
{
|
||||
object: 'field',
|
||||
type: 'removed',
|
||||
fieldId: oldField.id,
|
||||
tableId,
|
||||
}
|
||||
);
|
||||
|
||||
changedTables.set(tableId, true);
|
||||
changedFields.set(oldField.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for field changes
|
||||
for (const oldField of oldFields) {
|
||||
const newField = newFields.find((f) => f.id === oldField.id);
|
||||
if (!newField) continue;
|
||||
|
||||
// Compare basic field properties
|
||||
compareFieldProperties({
|
||||
tableId,
|
||||
oldField,
|
||||
newField,
|
||||
diffMap,
|
||||
changedTables,
|
||||
changedFields,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Compare field properties
|
||||
function compareFieldProperties({
|
||||
tableId,
|
||||
oldField,
|
||||
newField,
|
||||
diffMap,
|
||||
changedTables,
|
||||
changedFields,
|
||||
}: {
|
||||
tableId: string;
|
||||
oldField: DBField;
|
||||
newField: DBField;
|
||||
diffMap: DiffMap;
|
||||
changedTables: Map<string, boolean>;
|
||||
changedFields: Map<string, boolean>;
|
||||
}) {
|
||||
const changedAttributes: FieldDiffAttribute[] = [];
|
||||
|
||||
if (oldField.name !== newField.name) {
|
||||
changedAttributes.push('name');
|
||||
}
|
||||
|
||||
if (oldField.type.id !== newField.type.id) {
|
||||
changedAttributes.push('type');
|
||||
}
|
||||
|
||||
if (oldField.primaryKey !== newField.primaryKey) {
|
||||
changedAttributes.push('primaryKey');
|
||||
}
|
||||
|
||||
if (oldField.unique !== newField.unique) {
|
||||
changedAttributes.push('unique');
|
||||
}
|
||||
|
||||
if (oldField.nullable !== newField.nullable) {
|
||||
changedAttributes.push('nullable');
|
||||
}
|
||||
|
||||
if (oldField.comments !== newField.comments) {
|
||||
changedAttributes.push('comments');
|
||||
}
|
||||
|
||||
if (changedAttributes.length > 0) {
|
||||
for (const attribute of changedAttributes) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: oldField.id,
|
||||
attribute: attribute,
|
||||
}),
|
||||
{
|
||||
object: 'field',
|
||||
type: 'changed',
|
||||
fieldId: oldField.id,
|
||||
tableId,
|
||||
attributes: attribute,
|
||||
oldValue: oldField[attribute],
|
||||
newValue: newField[attribute],
|
||||
}
|
||||
);
|
||||
}
|
||||
changedTables.set(tableId, true);
|
||||
changedFields.set(oldField.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Compare indexes between tables
|
||||
function compareIndexes({
|
||||
tableId,
|
||||
oldIndexes,
|
||||
newIndexes,
|
||||
diffMap,
|
||||
changedTables,
|
||||
}: {
|
||||
tableId: string;
|
||||
oldIndexes: DBIndex[];
|
||||
newIndexes: DBIndex[];
|
||||
diffMap: DiffMap;
|
||||
changedTables: Map<string, boolean>;
|
||||
}) {
|
||||
// Check for added indexes
|
||||
for (const newIndex of newIndexes) {
|
||||
if (!oldIndexes.find((i) => i.id === newIndex.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'index',
|
||||
objectId: newIndex.id,
|
||||
}),
|
||||
{
|
||||
object: 'index',
|
||||
type: 'added',
|
||||
indexId: newIndex.id,
|
||||
tableId,
|
||||
}
|
||||
);
|
||||
changedTables.set(tableId, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for removed indexes
|
||||
for (const oldIndex of oldIndexes) {
|
||||
if (!newIndexes.find((i) => i.id === oldIndex.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'index',
|
||||
objectId: oldIndex.id,
|
||||
}),
|
||||
{
|
||||
object: 'index',
|
||||
type: 'removed',
|
||||
indexId: oldIndex.id,
|
||||
tableId,
|
||||
}
|
||||
);
|
||||
changedTables.set(tableId, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compare relationships between diagrams
|
||||
function compareRelationships({
|
||||
diagram,
|
||||
newDiagram,
|
||||
diffMap,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
newDiagram: Diagram;
|
||||
diffMap: DiffMap;
|
||||
}) {
|
||||
const oldRelationships = diagram.relationships || [];
|
||||
const newRelationships = newDiagram.relationships || [];
|
||||
|
||||
// Check for added relationships
|
||||
for (const newRelationship of newRelationships) {
|
||||
if (!oldRelationships.find((r) => r.id === newRelationship.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'relationship',
|
||||
objectId: newRelationship.id,
|
||||
}),
|
||||
{
|
||||
object: 'relationship',
|
||||
type: 'added',
|
||||
relationshipId: newRelationship.id,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for removed relationships
|
||||
for (const oldRelationship of oldRelationships) {
|
||||
if (!newRelationships.find((r) => r.id === oldRelationship.id)) {
|
||||
diffMap.set(
|
||||
getDiffMapKey({
|
||||
diffObject: 'relationship',
|
||||
objectId: oldRelationship.id,
|
||||
}),
|
||||
{
|
||||
object: 'relationship',
|
||||
type: 'removed',
|
||||
relationshipId: oldRelationship.id,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
75
src/context/diff-context/diff-context.tsx
Normal file
75
src/context/diff-context/diff-context.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { createContext } from 'react';
|
||||
import type { DiffMap } from './types';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { EventEmitter } from 'ahooks/lib/useEventEmitter';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||
|
||||
export type DiffEventType = 'diff_calculated';
|
||||
|
||||
export type DiffEventBase<T extends DiffEventType, D> = {
|
||||
action: T;
|
||||
data: D;
|
||||
};
|
||||
|
||||
export type DiffCalculatedEvent = DiffEventBase<
|
||||
'diff_calculated',
|
||||
{
|
||||
tablesAdded: DBTable[];
|
||||
fieldsAdded: Map<string, DBField[]>;
|
||||
relationshipsAdded: DBRelationship[];
|
||||
}
|
||||
>;
|
||||
|
||||
export type DiffEvent = DiffCalculatedEvent;
|
||||
|
||||
export interface DiffContext {
|
||||
newDiagram: Diagram | null;
|
||||
diffMap: DiffMap;
|
||||
hasDiff: boolean;
|
||||
|
||||
calculateDiff: ({
|
||||
diagram,
|
||||
newDiagram,
|
||||
}: {
|
||||
diagram: Diagram;
|
||||
newDiagram: Diagram;
|
||||
}) => void;
|
||||
|
||||
// table diff
|
||||
checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
|
||||
checkIfNewTable: ({ tableId }: { tableId: string }) => boolean;
|
||||
checkIfTableRemoved: ({ tableId }: { tableId: string }) => boolean;
|
||||
getTableNewName: ({ tableId }: { tableId: string }) => string | null;
|
||||
|
||||
// field diff
|
||||
checkIfFieldHasChange: ({
|
||||
tableId,
|
||||
fieldId,
|
||||
}: {
|
||||
tableId: string;
|
||||
fieldId: string;
|
||||
}) => boolean;
|
||||
checkIfFieldRemoved: ({ fieldId }: { fieldId: string }) => boolean;
|
||||
checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
|
||||
getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
|
||||
getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
|
||||
|
||||
// relationship diff
|
||||
checkIfNewRelationship: ({
|
||||
relationshipId,
|
||||
}: {
|
||||
relationshipId: string;
|
||||
}) => boolean;
|
||||
checkIfRelationshipRemoved: ({
|
||||
relationshipId,
|
||||
}: {
|
||||
relationshipId: string;
|
||||
}) => boolean;
|
||||
|
||||
events: EventEmitter<DiffEvent>;
|
||||
}
|
||||
|
||||
export const diffContext = createContext<DiffContext | undefined>(undefined);
|
327
src/context/diff-context/diff-provider.tsx
Normal file
327
src/context/diff-context/diff-provider.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import type { DiffContext, DiffEvent } from './diff-context';
|
||||
import { diffContext } from './diff-context';
|
||||
import type { ChartDBDiff, DiffMap } from './types';
|
||||
import { generateDiff, getDiffMapKey } from './diff-check/diff-check';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import { useEventEmitter } from 'ahooks';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||
|
||||
export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [newDiagram, setNewDiagram] = React.useState<Diagram | null>(null);
|
||||
const [diffMap, setDiffMap] = React.useState<DiffMap>(
|
||||
new Map<string, ChartDBDiff>()
|
||||
);
|
||||
const [tablesChanged, setTablesChanged] = React.useState<
|
||||
Map<string, boolean>
|
||||
>(new Map<string, boolean>());
|
||||
const [fieldsChanged, setFieldsChanged] = React.useState<
|
||||
Map<string, boolean>
|
||||
>(new Map<string, boolean>());
|
||||
|
||||
const events = useEventEmitter<DiffEvent>();
|
||||
|
||||
const generateNewFieldsMap = useCallback(
|
||||
({
|
||||
diffMap,
|
||||
newDiagram,
|
||||
}: {
|
||||
diffMap: DiffMap;
|
||||
newDiagram: Diagram;
|
||||
}) => {
|
||||
const newFieldsMap = new Map<string, DBField[]>();
|
||||
|
||||
diffMap.forEach((diff) => {
|
||||
if (diff.object === 'field' && diff.type === 'added') {
|
||||
const field = newDiagram?.tables
|
||||
?.find((table) => table.id === diff.tableId)
|
||||
?.fields.find((f) => f.id === diff.fieldId);
|
||||
|
||||
if (field) {
|
||||
newFieldsMap.set(diff.tableId, [
|
||||
...(newFieldsMap.get(diff.tableId) ?? []),
|
||||
field,
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return newFieldsMap;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const findNewRelationships = useCallback(
|
||||
({
|
||||
diffMap,
|
||||
newDiagram,
|
||||
}: {
|
||||
diffMap: DiffMap;
|
||||
newDiagram: Diagram;
|
||||
}) => {
|
||||
const relationships: DBRelationship[] = [];
|
||||
diffMap.forEach((diff) => {
|
||||
if (diff.object === 'relationship' && diff.type === 'added') {
|
||||
const relationship = newDiagram?.relationships?.find(
|
||||
(rel) => rel.id === diff.relationshipId
|
||||
);
|
||||
|
||||
if (relationship) {
|
||||
relationships.push(relationship);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return relationships;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const calculateDiff: DiffContext['calculateDiff'] = useCallback(
|
||||
({ diagram, newDiagram: newDiagramArg }) => {
|
||||
const {
|
||||
diffMap: newDiffs,
|
||||
changedTables: newChangedTables,
|
||||
changedFields: newChangedFields,
|
||||
} = generateDiff({ diagram, newDiagram: newDiagramArg });
|
||||
|
||||
setDiffMap(newDiffs);
|
||||
setTablesChanged(newChangedTables);
|
||||
setFieldsChanged(newChangedFields);
|
||||
setNewDiagram(newDiagramArg);
|
||||
|
||||
events.emit({
|
||||
action: 'diff_calculated',
|
||||
data: {
|
||||
tablesAdded:
|
||||
newDiagramArg?.tables?.filter((table) => {
|
||||
const tableKey = getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: table.id,
|
||||
});
|
||||
|
||||
return (
|
||||
newDiffs.has(tableKey) &&
|
||||
newDiffs.get(tableKey)?.type === 'added'
|
||||
);
|
||||
}) ?? [],
|
||||
|
||||
fieldsAdded: generateNewFieldsMap({
|
||||
diffMap: newDiffs,
|
||||
newDiagram: newDiagramArg,
|
||||
}),
|
||||
relationshipsAdded: findNewRelationships({
|
||||
diffMap: newDiffs,
|
||||
newDiagram: newDiagramArg,
|
||||
}),
|
||||
},
|
||||
});
|
||||
},
|
||||
[setDiffMap, events, generateNewFieldsMap, findNewRelationships]
|
||||
);
|
||||
|
||||
const getTableNewName = useCallback<DiffContext['getTableNewName']>(
|
||||
({ tableId }) => {
|
||||
const tableNameKey = getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: tableId,
|
||||
attribute: 'name',
|
||||
});
|
||||
|
||||
if (diffMap.has(tableNameKey)) {
|
||||
const diff = diffMap.get(tableNameKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as string;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfTableHasChange = useCallback<
|
||||
DiffContext['checkIfTableHasChange']
|
||||
>(({ tableId }) => tablesChanged.get(tableId) ?? false, [tablesChanged]);
|
||||
|
||||
const checkIfNewTable = useCallback<DiffContext['checkIfNewTable']>(
|
||||
({ tableId }) => {
|
||||
const tableKey = getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: tableId,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(tableKey) && diffMap.get(tableKey)?.type === 'added'
|
||||
);
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfTableRemoved = useCallback<DiffContext['checkIfTableRemoved']>(
|
||||
({ tableId }) => {
|
||||
const tableKey = getDiffMapKey({
|
||||
diffObject: 'table',
|
||||
objectId: tableId,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(tableKey) &&
|
||||
diffMap.get(tableKey)?.type === 'removed'
|
||||
);
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfFieldHasChange = useCallback<
|
||||
DiffContext['checkIfFieldHasChange']
|
||||
>(
|
||||
({ fieldId }) => {
|
||||
return fieldsChanged.get(fieldId) ?? false;
|
||||
},
|
||||
[fieldsChanged]
|
||||
);
|
||||
|
||||
const checkIfFieldRemoved = useCallback<DiffContext['checkIfFieldRemoved']>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(fieldKey) &&
|
||||
diffMap.get(fieldKey)?.type === 'removed'
|
||||
);
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfNewField = useCallback<DiffContext['checkIfNewField']>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(fieldKey) && diffMap.get(fieldKey)?.type === 'added'
|
||||
);
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getFieldNewName = useCallback<DiffContext['getFieldNewName']>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
attribute: 'name',
|
||||
});
|
||||
|
||||
if (diffMap.has(fieldKey)) {
|
||||
const diff = diffMap.get(fieldKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as string;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const getFieldNewType = useCallback<DiffContext['getFieldNewType']>(
|
||||
({ fieldId }) => {
|
||||
const fieldKey = getDiffMapKey({
|
||||
diffObject: 'field',
|
||||
objectId: fieldId,
|
||||
attribute: 'type',
|
||||
});
|
||||
|
||||
if (diffMap.has(fieldKey)) {
|
||||
const diff = diffMap.get(fieldKey);
|
||||
|
||||
if (diff?.type === 'changed') {
|
||||
return diff.newValue as DataType;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfNewRelationship = useCallback<
|
||||
DiffContext['checkIfNewRelationship']
|
||||
>(
|
||||
({ relationshipId }) => {
|
||||
const relationshipKey = getDiffMapKey({
|
||||
diffObject: 'relationship',
|
||||
objectId: relationshipId,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(relationshipKey) &&
|
||||
diffMap.get(relationshipKey)?.type === 'added'
|
||||
);
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
const checkIfRelationshipRemoved = useCallback<
|
||||
DiffContext['checkIfRelationshipRemoved']
|
||||
>(
|
||||
({ relationshipId }) => {
|
||||
const relationshipKey = getDiffMapKey({
|
||||
diffObject: 'relationship',
|
||||
objectId: relationshipId,
|
||||
});
|
||||
|
||||
return (
|
||||
diffMap.has(relationshipKey) &&
|
||||
diffMap.get(relationshipKey)?.type === 'removed'
|
||||
);
|
||||
},
|
||||
[diffMap]
|
||||
);
|
||||
|
||||
return (
|
||||
<diffContext.Provider
|
||||
value={{
|
||||
newDiagram,
|
||||
diffMap,
|
||||
hasDiff: diffMap.size > 0,
|
||||
|
||||
calculateDiff,
|
||||
|
||||
// table diff
|
||||
getTableNewName,
|
||||
checkIfNewTable,
|
||||
checkIfTableRemoved,
|
||||
checkIfTableHasChange,
|
||||
|
||||
// field diff
|
||||
checkIfFieldHasChange,
|
||||
checkIfFieldRemoved,
|
||||
checkIfNewField,
|
||||
getFieldNewName,
|
||||
getFieldNewType,
|
||||
|
||||
// relationship diff
|
||||
checkIfNewRelationship,
|
||||
checkIfRelationshipRemoved,
|
||||
|
||||
events,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</diffContext.Provider>
|
||||
);
|
||||
};
|
53
src/context/diff-context/types.ts
Normal file
53
src/context/diff-context/types.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||
|
||||
export type TableDiffAttribute = 'name' | 'comments';
|
||||
|
||||
export interface TableDiff {
|
||||
object: 'table';
|
||||
type: 'added' | 'removed' | 'changed';
|
||||
tableId: string;
|
||||
attributes?: TableDiffAttribute;
|
||||
oldValue?: string;
|
||||
newValue?: string;
|
||||
}
|
||||
|
||||
export interface RelationshipDiff {
|
||||
object: 'relationship';
|
||||
type: 'added' | 'removed';
|
||||
relationshipId: string;
|
||||
}
|
||||
|
||||
export type FieldDiffAttribute =
|
||||
| 'name'
|
||||
| 'type'
|
||||
| 'primaryKey'
|
||||
| 'unique'
|
||||
| 'nullable'
|
||||
| 'comments';
|
||||
|
||||
export interface FieldDiff {
|
||||
object: 'field';
|
||||
type: 'added' | 'removed' | 'changed';
|
||||
fieldId: string;
|
||||
tableId: string;
|
||||
attributes?: FieldDiffAttribute;
|
||||
oldValue?: string | boolean | DataType;
|
||||
newValue?: string | boolean | DataType;
|
||||
}
|
||||
|
||||
export interface IndexDiff {
|
||||
object: 'index';
|
||||
type: 'added' | 'removed';
|
||||
indexId: string;
|
||||
tableId: string;
|
||||
}
|
||||
|
||||
export type ChartDBDiff = TableDiff | FieldDiff | IndexDiff | RelationshipDiff;
|
||||
|
||||
export type DiffMap = Map<string, ChartDBDiff>;
|
||||
|
||||
export type DiffObject =
|
||||
| TableDiff['object']
|
||||
| FieldDiff['object']
|
||||
| IndexDiff['object']
|
||||
| RelationshipDiff['object'];
|
10
src/context/diff-context/use-diff.ts
Normal file
10
src/context/diff-context/use-diff.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
import { diffContext } from './diff-context';
|
||||
|
||||
export const useDiff = () => {
|
||||
const context = useContext(diffContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useDiff must be used within an DiffProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useEffect, useState } from 'react';
|
||||
import type { ExportImageContext, ImageType } from './export-image-context';
|
||||
import { exportImageContext } from './export-image-context';
|
||||
import { toJpeg, toPng, toSvg } from 'html-to-image';
|
||||
@@ -6,6 +6,8 @@ import { useReactFlow } from '@xyflow/react';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
import logoDark from '@/assets/logo-dark.png';
|
||||
import logoLight from '@/assets/logo-light.png';
|
||||
|
||||
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
@@ -14,6 +16,24 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
const { setNodes, getViewport } = useReactFlow();
|
||||
const { effectiveTheme } = useTheme();
|
||||
const { diagramName } = useChartDB();
|
||||
const [logoBase64, setLogoBase64] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
// Convert logo to base64 on component mount
|
||||
const img = new Image();
|
||||
img.src = effectiveTheme === 'light' ? logoLight : logoDark;
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.drawImage(img, 0, 0);
|
||||
const base64 = canvas.toDataURL('image/png');
|
||||
setLogoBase64(base64);
|
||||
}
|
||||
};
|
||||
}, [effectiveTheme]);
|
||||
|
||||
const downloadImage = useCallback(
|
||||
(dataUrl: string, type: ImageType) => {
|
||||
@@ -128,16 +148,22 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
'http://www.w3.org/2000/svg',
|
||||
'rect'
|
||||
);
|
||||
const padding = 2000;
|
||||
backgroundRect.setAttribute('x', String(-viewport.x - padding));
|
||||
backgroundRect.setAttribute('y', String(-viewport.y - padding));
|
||||
const bgPadding = 2000;
|
||||
backgroundRect.setAttribute(
|
||||
'x',
|
||||
String(-viewport.x - bgPadding)
|
||||
);
|
||||
backgroundRect.setAttribute(
|
||||
'y',
|
||||
String(-viewport.y - bgPadding)
|
||||
);
|
||||
backgroundRect.setAttribute(
|
||||
'width',
|
||||
String(reactFlowBounds.width + 2 * padding)
|
||||
String(reactFlowBounds.width + 2 * bgPadding)
|
||||
);
|
||||
backgroundRect.setAttribute(
|
||||
'height',
|
||||
String(reactFlowBounds.height + 2 * padding)
|
||||
String(reactFlowBounds.height + 2 * bgPadding)
|
||||
);
|
||||
backgroundRect.setAttribute('fill', 'url(#background-pattern)');
|
||||
tempSvg.appendChild(backgroundRect);
|
||||
@@ -148,28 +174,110 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
);
|
||||
|
||||
try {
|
||||
const dataUrl = await imageCreateFn(viewportElement, {
|
||||
...(type === 'jpeg' || type === 'png'
|
||||
? {
|
||||
backgroundColor:
|
||||
effectiveTheme === 'light'
|
||||
? '#ffffff'
|
||||
: '#141414',
|
||||
}
|
||||
: {}),
|
||||
width: reactFlowBounds.width,
|
||||
height: reactFlowBounds.height,
|
||||
style: {
|
||||
width: `${reactFlowBounds.width}px`,
|
||||
height: `${reactFlowBounds.height}px`,
|
||||
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
|
||||
},
|
||||
quality: 1,
|
||||
pixelRatio: scale,
|
||||
skipFonts: true,
|
||||
});
|
||||
// Handle SVG export differently
|
||||
if (type === 'svg') {
|
||||
const dataUrl = await imageCreateFn(viewportElement, {
|
||||
width: reactFlowBounds.width,
|
||||
height: reactFlowBounds.height,
|
||||
style: {
|
||||
width: `${reactFlowBounds.width}px`,
|
||||
height: `${reactFlowBounds.height}px`,
|
||||
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
|
||||
},
|
||||
quality: 1,
|
||||
pixelRatio: scale,
|
||||
skipFonts: true,
|
||||
});
|
||||
downloadImage(dataUrl, type);
|
||||
return;
|
||||
}
|
||||
|
||||
downloadImage(dataUrl, type);
|
||||
// For PNG and JPEG, continue with the watermark process
|
||||
const initialDataUrl = await imageCreateFn(
|
||||
viewportElement,
|
||||
{
|
||||
backgroundColor:
|
||||
effectiveTheme === 'light'
|
||||
? '#ffffff'
|
||||
: '#141414',
|
||||
width: reactFlowBounds.width,
|
||||
height: reactFlowBounds.height,
|
||||
style: {
|
||||
width: `${reactFlowBounds.width}px`,
|
||||
height: `${reactFlowBounds.height}px`,
|
||||
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
|
||||
},
|
||||
quality: 1,
|
||||
pixelRatio: scale,
|
||||
skipFonts: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Create a canvas to combine the diagram and watermark
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
downloadImage(initialDataUrl, type);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set canvas size to match the export size
|
||||
canvas.width = reactFlowBounds.width * scale;
|
||||
canvas.height = reactFlowBounds.height * scale;
|
||||
|
||||
// Load the exported diagram
|
||||
const diagramImage = new Image();
|
||||
diagramImage.src = initialDataUrl;
|
||||
|
||||
await new Promise((resolve) => {
|
||||
diagramImage.onload = async () => {
|
||||
// Draw the diagram
|
||||
ctx.drawImage(diagramImage, 0, 0);
|
||||
|
||||
// Calculate logo size
|
||||
const logoHeight = Math.max(
|
||||
24,
|
||||
Math.floor(canvas.width * 0.024)
|
||||
);
|
||||
const padding = Math.max(
|
||||
12,
|
||||
Math.floor(logoHeight * 0.5)
|
||||
);
|
||||
|
||||
// Load and draw the logo
|
||||
const logoImage = new Image();
|
||||
logoImage.src = logoBase64;
|
||||
|
||||
await new Promise((resolve) => {
|
||||
logoImage.onload = () => {
|
||||
// Calculate logo width while maintaining aspect ratio
|
||||
const logoWidth =
|
||||
(logoImage.width / logoImage.height) *
|
||||
logoHeight;
|
||||
|
||||
// Draw logo in bottom-left corner
|
||||
ctx.globalAlpha = 0.9;
|
||||
ctx.drawImage(
|
||||
logoImage,
|
||||
padding,
|
||||
canvas.height - logoHeight - padding,
|
||||
logoWidth,
|
||||
logoHeight
|
||||
);
|
||||
ctx.globalAlpha = 1;
|
||||
resolve(null);
|
||||
};
|
||||
});
|
||||
|
||||
// Convert canvas to data URL
|
||||
const finalDataUrl = canvas.toDataURL(
|
||||
type === 'png' ? 'image/png' : 'image/jpeg'
|
||||
);
|
||||
downloadImage(finalDataUrl, type);
|
||||
resolve(null);
|
||||
};
|
||||
});
|
||||
} finally {
|
||||
viewportElement.removeChild(tempSvg);
|
||||
hideLoader();
|
||||
@@ -184,6 +292,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setNodes,
|
||||
showLoader,
|
||||
effectiveTheme,
|
||||
logoBase64,
|
||||
]
|
||||
);
|
||||
|
||||
|
@@ -39,7 +39,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
||||
useHotkeys(
|
||||
keyboardShortcutsForOS[KeyboardShortcutAction.OPEN_DIAGRAM]
|
||||
.keyCombination,
|
||||
openOpenDiagramDialog,
|
||||
() => openOpenDiagramDialog(),
|
||||
{
|
||||
preventDefault: true,
|
||||
},
|
||||
|
@@ -7,6 +7,7 @@ export enum KeyboardShortcutAction {
|
||||
SAVE_DIAGRAM = 'save_diagram',
|
||||
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
|
||||
SHOW_ALL = 'show_all',
|
||||
TOGGLE_THEME = 'toggle_theme',
|
||||
}
|
||||
|
||||
export interface KeyboardShortcut {
|
||||
@@ -63,6 +64,13 @@ export const keyboardShortcuts: Record<
|
||||
keyCombinationMac: 'meta+0',
|
||||
keyCombinationWin: 'ctrl+0',
|
||||
},
|
||||
[KeyboardShortcutAction.TOGGLE_THEME]: {
|
||||
action: KeyboardShortcutAction.TOGGLE_THEME,
|
||||
keyCombinationLabelMac: '⌘M',
|
||||
keyCombinationLabelWin: 'Ctrl+M',
|
||||
keyCombinationMac: 'meta+m',
|
||||
keyCombinationWin: 'ctrl+m',
|
||||
},
|
||||
};
|
||||
|
||||
export interface KeyboardShortcutForOS {
|
||||
|
@@ -30,12 +30,6 @@ export interface LocalConfigContext {
|
||||
starUsDialogLastOpen: number;
|
||||
setStarUsDialogLastOpen: (lastOpen: number) => void;
|
||||
|
||||
buckleWaitlistOpened: boolean;
|
||||
setBuckleWaitlistOpened: (githubRepoOpened: boolean) => void;
|
||||
|
||||
buckleDialogLastOpen: number;
|
||||
setBuckleDialogLastOpen: (lastOpen: number) => void;
|
||||
|
||||
showDependenciesOnCanvas: boolean;
|
||||
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
|
||||
|
||||
@@ -53,7 +47,7 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
||||
schemasFilter: {},
|
||||
setSchemasFilter: emptyFn,
|
||||
|
||||
showCardinality: false,
|
||||
showCardinality: true,
|
||||
setShowCardinality: emptyFn,
|
||||
|
||||
hideMultiSchemaNotification: false,
|
||||
@@ -65,12 +59,6 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
||||
starUsDialogLastOpen: 0,
|
||||
setStarUsDialogLastOpen: emptyFn,
|
||||
|
||||
buckleWaitlistOpened: false,
|
||||
setBuckleWaitlistOpened: emptyFn,
|
||||
|
||||
buckleDialogLastOpen: 0,
|
||||
setBuckleDialogLastOpen: emptyFn,
|
||||
|
||||
showDependenciesOnCanvas: false,
|
||||
setShowDependenciesOnCanvas: emptyFn,
|
||||
|
||||
|
@@ -10,8 +10,6 @@ const showCardinalityKey = 'show_cardinality';
|
||||
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
|
||||
const githubRepoOpenedKey = 'github_repo_opened';
|
||||
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
||||
const buckleWaitlistOpenedKey = 'buckle_waitlist_opened';
|
||||
const buckleDialogLastOpenKey = 'buckle_dialog_last_open';
|
||||
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
|
||||
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
|
||||
|
||||
@@ -33,7 +31,7 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
);
|
||||
|
||||
const [showCardinality, setShowCardinality] = React.useState<boolean>(
|
||||
(localStorage.getItem(showCardinalityKey) || 'false') === 'true'
|
||||
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
|
||||
);
|
||||
|
||||
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
|
||||
@@ -51,17 +49,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
|
||||
);
|
||||
|
||||
const [buckleWaitlistOpened, setBuckleWaitlistOpened] =
|
||||
React.useState<boolean>(
|
||||
(localStorage.getItem(buckleWaitlistOpenedKey) || 'false') ===
|
||||
'true'
|
||||
);
|
||||
|
||||
const [buckleDialogLastOpen, setBuckleDialogLastOpen] =
|
||||
React.useState<number>(
|
||||
parseInt(localStorage.getItem(buckleDialogLastOpenKey) || '0')
|
||||
);
|
||||
|
||||
const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
|
||||
React.useState<boolean>(
|
||||
(localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
|
||||
@@ -84,20 +71,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
|
||||
}, [githubRepoOpened]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(
|
||||
buckleDialogLastOpenKey,
|
||||
buckleDialogLastOpen.toString()
|
||||
);
|
||||
}, [buckleDialogLastOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(
|
||||
buckleWaitlistOpenedKey,
|
||||
buckleWaitlistOpened.toString()
|
||||
);
|
||||
}, [buckleWaitlistOpened]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(
|
||||
hideMultiSchemaNotificationKey,
|
||||
@@ -154,10 +127,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setStarUsDialogLastOpen,
|
||||
showDependenciesOnCanvas,
|
||||
setShowDependenciesOnCanvas,
|
||||
setBuckleDialogLastOpen,
|
||||
buckleDialogLastOpen,
|
||||
buckleWaitlistOpened,
|
||||
setBuckleWaitlistOpened,
|
||||
showMiniMapOnCanvas,
|
||||
setShowMiniMapOnCanvas,
|
||||
}}
|
||||
|
@@ -1,8 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import type { EffectiveTheme } from './theme-context';
|
||||
import { ThemeContext } from './theme-context';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import {
|
||||
KeyboardShortcutAction,
|
||||
keyboardShortcutsForOS,
|
||||
} from '../keyboard-shortcuts-context/keyboard-shortcuts';
|
||||
|
||||
export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
@@ -29,6 +34,24 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
||||
}
|
||||
}, [effectiveTheme]);
|
||||
|
||||
const handleThemeToggle = useCallback(() => {
|
||||
if (theme === 'system') {
|
||||
setTheme(effectiveTheme === 'dark' ? 'light' : 'dark');
|
||||
} else {
|
||||
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
}
|
||||
}, [theme, effectiveTheme, setTheme]);
|
||||
|
||||
useHotkeys(
|
||||
keyboardShortcutsForOS[KeyboardShortcutAction.TOGGLE_THEME]
|
||||
.keyCombination,
|
||||
handleThemeToggle,
|
||||
{
|
||||
preventDefault: true,
|
||||
},
|
||||
[handleThemeToggle]
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}>
|
||||
{children}
|
||||
|
@@ -1,80 +0,0 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDialog } from '@/hooks/use-dialog';
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/dialog/dialog';
|
||||
import { Button } from '@/components/button/button';
|
||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
|
||||
export interface BuckleDialogProps extends BaseDialogProps {}
|
||||
|
||||
export const BuckleDialog: React.FC<BuckleDialogProps> = ({ dialog }) => {
|
||||
const { setBuckleWaitlistOpened } = useLocalConfig();
|
||||
const { effectiveTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialog.open) return;
|
||||
}, [dialog.open]);
|
||||
const { closeBuckleDialog } = useDialog();
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
setBuckleWaitlistOpened(true);
|
||||
window.open('https://waitlist.buckle.dev', '_blank');
|
||||
}, [setBuckleWaitlistOpened]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...dialog}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
closeBuckleDialog();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="flex flex-col"
|
||||
showClose={false}
|
||||
onInteractOutside={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="hidden" />
|
||||
<DialogDescription className="hidden" />
|
||||
</DialogHeader>
|
||||
<div className="flex w-full flex-col items-center">
|
||||
<img
|
||||
src={
|
||||
effectiveTheme === 'light'
|
||||
? '/buckle-animated.gif'
|
||||
: '/buckle.png'
|
||||
}
|
||||
className="h-16"
|
||||
/>
|
||||
<div className="mt-6 text-center text-base">
|
||||
We've been working on something big -{' '}
|
||||
<span className="font-semibold">Ready to explore?</span>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="flex gap-1 md:justify-between">
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">Not now</Button>
|
||||
</DialogClose>
|
||||
<DialogClose asChild>
|
||||
<Button onClick={handleConfirm}>
|
||||
Try ChartDB v2.0!
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
@@ -28,7 +28,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
const [databaseType, setDatabaseType] = useState<DatabaseType>(
|
||||
DatabaseType.GENERIC
|
||||
);
|
||||
const { closeCreateDiagramDialog } = useDialog();
|
||||
const { closeCreateDiagramDialog, openImportDBMLDialog } = useDialog();
|
||||
const { updateConfig } = useConfig();
|
||||
const [scriptResult, setScriptResult] = useState('');
|
||||
const [databaseEdition, setDatabaseEdition] = useState<
|
||||
@@ -104,6 +104,10 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
await updateConfig({ defaultDiagramId: diagram.id });
|
||||
closeCreateDiagramDialog();
|
||||
navigate(`/diagrams/${diagram.id}`);
|
||||
setTimeout(
|
||||
() => openImportDBMLDialog({ withCreateEmptyDiagram: true }),
|
||||
700
|
||||
);
|
||||
}, [
|
||||
databaseType,
|
||||
addDiagram,
|
||||
@@ -112,6 +116,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
navigate,
|
||||
updateConfig,
|
||||
diagramNumber,
|
||||
openImportDBMLDialog,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
@@ -15,11 +15,10 @@ 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';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert';
|
||||
import { useExportDiagram } from '@/hooks/use-export-diagram';
|
||||
|
||||
export interface ExportDiagramDialogProps extends BaseDialogProps {}
|
||||
|
||||
@@ -27,44 +26,27 @@ export const ExportDiagramDialog: React.FC<ExportDiagramDialogProps> = ({
|
||||
dialog,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { diagramName, currentDiagram } = useChartDB();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { currentDiagram } = useChartDB();
|
||||
const { closeExportDiagramDialog } = useDialog();
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialog.open) return;
|
||||
setIsLoading(false);
|
||||
setError(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 { exportDiagram, isExporting: isLoading } = useExportDiagram();
|
||||
|
||||
const handleExport = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
await waitFor(1000);
|
||||
try {
|
||||
const json = diagramToJSONOutput(currentDiagram);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const dataUrl = URL.createObjectURL(blob);
|
||||
downloadOutput(dataUrl);
|
||||
setIsLoading(false);
|
||||
await exportDiagram({ diagram: currentDiagram });
|
||||
closeExportDiagramDialog();
|
||||
} catch (e) {
|
||||
setError(true);
|
||||
setIsLoading(false);
|
||||
|
||||
throw e;
|
||||
}
|
||||
}, [downloadOutput, currentDiagram, closeExportDiagramDialog]);
|
||||
}, [exportDiagram, currentDiagram, closeExportDiagramDialog]);
|
||||
|
||||
const outputTypeOptions: SelectBoxOption[] = useMemo(
|
||||
() =>
|
||||
|
@@ -71,10 +71,13 @@ function parseDBMLError(error: unknown): DBMLError | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface ImportDBMLDialogProps extends BaseDialogProps {}
|
||||
export interface ImportDBMLDialogProps extends BaseDialogProps {
|
||||
withCreateEmptyDiagram?: boolean;
|
||||
}
|
||||
|
||||
export const ImportDBMLDialog: React.FC<ImportDBMLDialogProps> = ({
|
||||
dialog,
|
||||
withCreateEmptyDiagram,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const initialDBML = `// Use DBML to define your database structure
|
||||
@@ -331,7 +334,11 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
showClose
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('import_dbml_dialog.title')}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{withCreateEmptyDiagram
|
||||
? t('import_dbml_dialog.example_title')
|
||||
: t('import_dbml_dialog.title')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('import_dbml_dialog.description')}
|
||||
</DialogDescription>
|
||||
@@ -369,7 +376,9 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
<div className="flex items-center gap-4">
|
||||
<DialogClose asChild>
|
||||
<Button variant="secondary">
|
||||
{t('import_dbml_dialog.cancel')}
|
||||
{withCreateEmptyDiagram
|
||||
? t('import_dbml_dialog.skip_and_empty')
|
||||
: t('import_dbml_dialog.cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
{errorMessage ? (
|
||||
@@ -389,7 +398,9 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
onClick={handleImport}
|
||||
disabled={!dbmlContent.trim() || !!errorMessage}
|
||||
>
|
||||
{t('import_dbml_dialog.import')}
|
||||
{withCreateEmptyDiagram
|
||||
? t('import_dbml_dialog.show_example')
|
||||
: t('import_dbml_dialog.import')}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
|
@@ -28,10 +28,13 @@ import { useNavigate } from 'react-router-dom';
|
||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||
import { useDebounce } from '@/hooks/use-debounce';
|
||||
|
||||
export interface OpenDiagramDialogProps extends BaseDialogProps {}
|
||||
export interface OpenDiagramDialogProps extends BaseDialogProps {
|
||||
canClose?: boolean;
|
||||
}
|
||||
|
||||
export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
||||
dialog,
|
||||
canClose = true,
|
||||
}) => {
|
||||
const { closeOpenDiagramDialog } = useDialog();
|
||||
const { t } = useTranslation();
|
||||
@@ -122,14 +125,14 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
||||
<Dialog
|
||||
{...dialog}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
if (!open && canClose) {
|
||||
closeOpenDiagramDialog();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="flex h-[30rem] max-h-screen flex-col overflow-y-auto md:min-w-[80vw] xl:min-w-[55vw]"
|
||||
showClose
|
||||
showClose={canClose}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('open_diagram_dialog.title')}</DialogTitle>
|
||||
@@ -226,11 +229,15 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
||||
</DialogInternalContent>
|
||||
|
||||
<DialogFooter className="flex !justify-between gap-2">
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="secondary">
|
||||
{t('open_diagram_dialog.cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
{canClose ? (
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="secondary">
|
||||
{t('open_diagram_dialog.cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
type="submit"
|
||||
|
47
src/hooks/use-debounce-v2.ts
Normal file
47
src/hooks/use-debounce-v2.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { debounce as utilsDebounce } from '@/lib/utils';
|
||||
|
||||
interface DebouncedFunction {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(...args: any[]): void;
|
||||
cancel?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A hook that returns a debounced version of the provided function.
|
||||
* The debounced function will only be called after the specified delay
|
||||
* has passed without the function being called again.
|
||||
*
|
||||
* @param callback The function to debounce
|
||||
* @param delay The delay in milliseconds
|
||||
* @returns A debounced version of the callback
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function useDebounce<T extends (...args: any[]) => any>(
|
||||
callback: T,
|
||||
delay: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
// Use a ref to store the debounced function
|
||||
const debouncedFnRef = useRef<DebouncedFunction>();
|
||||
|
||||
// Update the debounced function when dependencies change
|
||||
useEffect(() => {
|
||||
// Create the debounced function
|
||||
debouncedFnRef.current = utilsDebounce(callback, delay);
|
||||
|
||||
// Clean up when component unmounts or dependencies change
|
||||
return () => {
|
||||
if (debouncedFnRef.current?.cancel) {
|
||||
debouncedFnRef.current.cancel();
|
||||
}
|
||||
};
|
||||
}, [callback, delay]);
|
||||
|
||||
// Create a stable callback that uses the ref
|
||||
const debouncedCallback = useCallback((...args: Parameters<T>) => {
|
||||
debouncedFnRef.current?.(...args);
|
||||
}, []);
|
||||
|
||||
return debouncedCallback;
|
||||
}
|
40
src/hooks/use-export-diagram.tsx
Normal file
40
src/hooks/use-export-diagram.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useDialog } from '@/hooks/use-dialog';
|
||||
import { diagramToJSONOutput } from '@/lib/export-import-utils';
|
||||
import { waitFor } from '@/lib/utils';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
|
||||
export const useExportDiagram = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { closeExportDiagramDialog } = useDialog();
|
||||
|
||||
const downloadOutput = useCallback((name: string, dataUrl: string) => {
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('download', `ChartDB(${name}).json`);
|
||||
a.setAttribute('href', dataUrl);
|
||||
a.click();
|
||||
}, []);
|
||||
|
||||
const handleExport = useCallback(
|
||||
async ({ diagram }: { diagram: Diagram }) => {
|
||||
setIsLoading(true);
|
||||
await waitFor(1000);
|
||||
try {
|
||||
const json = diagramToJSONOutput(diagram);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const dataUrl = URL.createObjectURL(blob);
|
||||
downloadOutput(diagram.name, dataUrl);
|
||||
setIsLoading(false);
|
||||
closeExportDiagramDialog();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[downloadOutput, closeExportDiagramDialog]
|
||||
);
|
||||
|
||||
return {
|
||||
exportDiagram: handleExport,
|
||||
isExporting: isLoading,
|
||||
};
|
||||
};
|
@@ -34,13 +34,14 @@ export const ar: LanguageTranslation = {
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: 'مشاركة',
|
||||
backup: {
|
||||
backup: 'النسخ الاحتياطي',
|
||||
export_diagram: 'تصدير المخطط',
|
||||
import_diagram: 'استيراد المخطط',
|
||||
restore_diagram: 'استعادة المخطط',
|
||||
},
|
||||
help: {
|
||||
help: 'مساعدة',
|
||||
docs_website: 'الوثائق',
|
||||
visit_website: 'ChartDB قم بزيارة',
|
||||
join_discord: 'Discord انضم إلينا على',
|
||||
schedule_a_call: '!تحدث معنا',
|
||||
@@ -150,6 +151,8 @@ export const ar: LanguageTranslation = {
|
||||
comments: 'تعليقات',
|
||||
no_comments: 'لا يوجد تعليقات',
|
||||
delete_field: 'حذف الحقل',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'خصائص الفهرس',
|
||||
@@ -378,9 +381,12 @@ export const ar: LanguageTranslation = {
|
||||
import_dbml_dialog: {
|
||||
// TODO: Translate
|
||||
title: 'Import DBML',
|
||||
example_title: 'Import Example DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -35,13 +35,14 @@ export const bn: LanguageTranslation = {
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
|
||||
share: {
|
||||
share: 'শেয়ার করুন',
|
||||
backup: {
|
||||
backup: 'ব্যাকআপ',
|
||||
export_diagram: 'ডায়াগ্রাম রপ্তানি করুন',
|
||||
import_diagram: 'ডায়াগ্রাম আমদানি করুন',
|
||||
restore_diagram: 'ডায়াগ্রাম পুনরুদ্ধার করুন',
|
||||
},
|
||||
help: {
|
||||
help: 'সাহায্য',
|
||||
docs_website: 'ডকুমেন্টেশন',
|
||||
visit_website: 'ChartDB ওয়েবসাইটে যান',
|
||||
join_discord: 'আমাদের Discord-এ যোগ দিন',
|
||||
schedule_a_call: 'আমাদের সাথে কথা বলুন!',
|
||||
@@ -151,6 +152,8 @@ export const bn: LanguageTranslation = {
|
||||
comments: 'মন্তব্য',
|
||||
no_comments: 'কোনো মন্তব্য নেই',
|
||||
delete_field: 'ফিল্ড মুছুন',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'ইনডেক্স কর্ম',
|
||||
@@ -381,10 +384,13 @@ export const bn: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -35,13 +35,14 @@ export const de: LanguageTranslation = {
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'Hilfe',
|
||||
docs_website: 'Dokumentation',
|
||||
visit_website: 'ChartDB Webseite',
|
||||
join_discord: 'Auf Discord beitreten',
|
||||
schedule_a_call: 'Gespräch vereinbaren',
|
||||
@@ -152,6 +153,8 @@ export const de: LanguageTranslation = {
|
||||
comments: 'Kommentare',
|
||||
no_comments: 'Keine Kommentare',
|
||||
delete_field: 'Feld löschen',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Indexattribute',
|
||||
@@ -384,10 +387,13 @@ export const de: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -33,13 +33,14 @@ export const en = {
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: 'Share',
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'Help',
|
||||
docs_website: 'Docs',
|
||||
visit_website: 'Visit ChartDB',
|
||||
join_discord: 'Join us on Discord',
|
||||
schedule_a_call: 'Talk with us!',
|
||||
@@ -144,6 +145,7 @@ export const en = {
|
||||
field_actions: {
|
||||
title: 'Field Attributes',
|
||||
unique: 'Unique',
|
||||
character_length: 'Max Length',
|
||||
comments: 'Comments',
|
||||
no_comments: 'No comments',
|
||||
delete_field: 'Delete Field',
|
||||
@@ -376,10 +378,13 @@ export const en = {
|
||||
},
|
||||
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error importing DBML',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -34,14 +34,14 @@ export const es: LanguageTranslation = {
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
backup: {
|
||||
backup: 'Respaldo',
|
||||
export_diagram: 'Exportar Diagrama',
|
||||
restore_diagram: 'Restaurar Diagrama',
|
||||
},
|
||||
help: {
|
||||
help: 'Ayuda',
|
||||
docs_website: 'Documentación',
|
||||
visit_website: 'Visitar ChartDB',
|
||||
join_discord: 'Únete a nosotros en Discord',
|
||||
schedule_a_call: '¡Habla con nosotros!',
|
||||
@@ -142,6 +142,8 @@ export const es: LanguageTranslation = {
|
||||
comments: 'Comentarios',
|
||||
no_comments: 'Sin comentarios',
|
||||
delete_field: 'Eliminar Campo',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Atributos del Índice',
|
||||
@@ -383,10 +385,13 @@ export const es: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -30,17 +30,17 @@ export const fr: LanguageTranslation = {
|
||||
theme: 'Thème',
|
||||
show_dependencies: 'Afficher les Dépendances',
|
||||
hide_dependencies: 'Masquer les Dépendances',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
show_minimap: 'Afficher la Mini Carte',
|
||||
hide_minimap: 'Masquer la Mini Carte',
|
||||
},
|
||||
share: {
|
||||
share: 'Partage',
|
||||
backup: {
|
||||
backup: 'Sauvegarde',
|
||||
export_diagram: 'Exporter le diagramme',
|
||||
import_diagram: 'Importer un diagramme',
|
||||
restore_diagram: 'Restaurer le diagramme',
|
||||
},
|
||||
help: {
|
||||
help: 'Aide',
|
||||
docs_website: 'Documentation',
|
||||
visit_website: 'Visitez ChartDB',
|
||||
join_discord: 'Rejoignez-nous sur Discord',
|
||||
schedule_a_call: 'Parlez avec nous !',
|
||||
@@ -101,9 +101,8 @@ export const fr: LanguageTranslation = {
|
||||
clear: 'Effacer',
|
||||
show_more: 'Afficher Plus',
|
||||
show_less: 'Afficher Moins',
|
||||
// TODO: Translate
|
||||
copy_to_clipboard: 'Copy to Clipboard',
|
||||
copied: 'Copied!',
|
||||
copy_to_clipboard: 'Copier dans le presse-papiers',
|
||||
copied: 'Copié !',
|
||||
|
||||
side_panel: {
|
||||
schema: 'Schéma:',
|
||||
@@ -116,12 +115,11 @@ export const fr: LanguageTranslation = {
|
||||
add_table: 'Ajouter une Table',
|
||||
filter: 'Filtrer',
|
||||
collapse: 'Réduire Tout',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
clear: 'Effacer le Filtre',
|
||||
no_results:
|
||||
'Aucune table trouvée correspondant à votre filtre.',
|
||||
show_list: 'Afficher la Liste des Tableaux',
|
||||
show_dbml: "Afficher l'éditeur DBML",
|
||||
|
||||
table: {
|
||||
fields: 'Champs',
|
||||
@@ -142,6 +140,8 @@ export const fr: LanguageTranslation = {
|
||||
comments: 'Commentaires',
|
||||
no_comments: 'Pas de commentaires',
|
||||
delete_field: 'Supprimer le Champ',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: "Attributs de l'Index",
|
||||
@@ -153,7 +153,7 @@ export const fr: LanguageTranslation = {
|
||||
title: 'Actions de la Table',
|
||||
add_field: 'Ajouter un Champ',
|
||||
add_index: 'Ajouter un Index',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
duplicate_table: 'Tableau dupliqué',
|
||||
delete_table: 'Supprimer la Table',
|
||||
change_schema: 'Changer le Schéma',
|
||||
},
|
||||
@@ -236,14 +236,12 @@ export const fr: LanguageTranslation = {
|
||||
step_2: 'Si vous utilisez "Résultats en Grille", changez le nombre maximum de caractères récupérés pour les données non-XML (définir à 9999999).',
|
||||
},
|
||||
instructions_link: "Besoin d'aide ? Regardez comment",
|
||||
// TODO: Translate
|
||||
check_script_result: 'Check Script Result',
|
||||
check_script_result: 'Vérifier le résultat du Script',
|
||||
},
|
||||
|
||||
cancel: 'Annuler',
|
||||
back: 'Retour',
|
||||
// TODO: Translate
|
||||
import_from_file: 'Import from File',
|
||||
import_from_file: "Importer à partir d'un fichier",
|
||||
empty_diagram: 'Diagramme vide',
|
||||
continue: 'Continuer',
|
||||
import: 'Importer',
|
||||
@@ -358,40 +356,42 @@ export const fr: LanguageTranslation = {
|
||||
cancel: 'Annuler',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
export_diagram_dialog: {
|
||||
title: 'Export Diagram',
|
||||
description: 'Choose the format for export:',
|
||||
title: 'Exporter le Diagramme',
|
||||
description: "Sélectionner le format d'exportation :",
|
||||
format_json: 'JSON',
|
||||
cancel: 'Cancel',
|
||||
export: 'Export',
|
||||
cancel: 'Annuler',
|
||||
export: 'Exporter',
|
||||
error: {
|
||||
title: 'Error exporting diagram',
|
||||
title: "Erreur lors de l'exportation du diagramme",
|
||||
description:
|
||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
||||
"Une erreur s'est produite. Besoin d'aide ? chartdb.io@gmail.com",
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_diagram_dialog: {
|
||||
title: 'Import Diagram',
|
||||
description: 'Paste the diagram JSON below:',
|
||||
cancel: 'Cancel',
|
||||
import: 'Import',
|
||||
title: 'Importer un diagramme',
|
||||
description: 'Coller le diagramme au format JSON ci-dessous :',
|
||||
cancel: 'Annuler',
|
||||
import: 'Exporter',
|
||||
error: {
|
||||
title: 'Error importing diagram',
|
||||
title: "Erreur lors de l'exportation du diagramme",
|
||||
description:
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
"Le diagramme JSON n'est pas valide. Veuillez vérifier le JSON et réessayer. Besoin d'aide ? chartdb.io@gmail.com",
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: "Exemple d'importation DBML",
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
description:
|
||||
'Importer un schéma de base de données à partir du format DBML.',
|
||||
import: 'Importer',
|
||||
cancel: 'Annuler',
|
||||
skip_and_empty: 'Passer et vider',
|
||||
show_example: 'Afficher un exemple',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
title: 'Erreur',
|
||||
description:
|
||||
"Erreur d'analyse du DBML. Veuillez vérifier la syntaxe.",
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
@@ -408,13 +408,13 @@ export const fr: LanguageTranslation = {
|
||||
|
||||
table_node_context_menu: {
|
||||
edit_table: 'Éditer la Table',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
duplicate_table: 'Tableau Dupliqué',
|
||||
delete_table: 'Supprimer la Table',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
add_relationship: 'Ajouter une Relation',
|
||||
},
|
||||
|
||||
// TODO: Add translations
|
||||
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
|
||||
snap_to_grid_tooltip:
|
||||
'Aligner sur la grille (maintenir la touche {{key}})',
|
||||
|
||||
tool_tips: {
|
||||
double_click_to_edit: 'Double-cliquez pour modifier',
|
||||
|
@@ -35,13 +35,14 @@ export const gu: LanguageTranslation = {
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
|
||||
share: {
|
||||
share: 'શેર કરો',
|
||||
backup: {
|
||||
backup: 'બેકઅપ',
|
||||
export_diagram: 'ડાયાગ્રામ નિકાસ કરો',
|
||||
import_diagram: 'ડાયાગ્રામ આયાત કરો',
|
||||
restore_diagram: 'ડાયાગ્રામ પુનઃસ્થાપિત કરો',
|
||||
},
|
||||
help: {
|
||||
help: 'મદદ',
|
||||
docs_website: 'દસ્તાવેજીકરણ',
|
||||
visit_website: 'ChartDB વેબસાઇટ પર જાઓ',
|
||||
join_discord: 'અમારા Discordમાં જોડાઓ',
|
||||
schedule_a_call: 'અમારી સાથે વાત કરો!',
|
||||
@@ -152,6 +153,8 @@ export const gu: LanguageTranslation = {
|
||||
comments: 'ટિપ્પણીઓ',
|
||||
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
|
||||
delete_field: 'ફીલ્ડ કાઢી નાખો',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'ઇન્ડેક્સ લક્ષણો',
|
||||
@@ -381,10 +384,13 @@ export const gu: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -34,14 +34,14 @@ export const hi: LanguageTranslation = {
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
backup: {
|
||||
backup: 'बैकअप',
|
||||
export_diagram: 'आरेख निर्यात करें',
|
||||
restore_diagram: 'आरेख पुनर्स्थापित करें',
|
||||
},
|
||||
help: {
|
||||
help: 'मदद',
|
||||
docs_website: 'દસ્તાવેજીકરણ',
|
||||
visit_website: 'ChartDB वेबसाइट पर जाएँ',
|
||||
join_discord: 'हमसे Discord पर जुड़ें',
|
||||
schedule_a_call: 'हमसे बात करें!',
|
||||
@@ -152,6 +152,8 @@ export const hi: LanguageTranslation = {
|
||||
comments: 'टिप्पणियाँ',
|
||||
no_comments: 'कोई टिप्पणी नहीं',
|
||||
delete_field: 'फ़ील्ड हटाएँ',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'सूचकांक विशेषताएँ',
|
||||
@@ -385,10 +387,13 @@ export const hi: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -34,13 +34,14 @@ export const id_ID: LanguageTranslation = {
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: 'Bagikan',
|
||||
backup: {
|
||||
backup: 'Cadangan',
|
||||
export_diagram: 'Ekspor Diagram',
|
||||
import_diagram: 'Impor Diagram',
|
||||
restore_diagram: 'Pulihkan Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'Bantuan',
|
||||
docs_website: 'દસ્તાવેજીકરણ',
|
||||
visit_website: 'Kunjungi ChartDB',
|
||||
join_discord: 'Bergabunglah di Discord kami',
|
||||
schedule_a_call: 'Berbicara dengan kami!',
|
||||
@@ -150,6 +151,8 @@ export const id_ID: LanguageTranslation = {
|
||||
comments: 'Komentar',
|
||||
no_comments: 'Tidak ada komentar',
|
||||
delete_field: 'Hapus Kolom',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Atribut Indeks',
|
||||
@@ -379,10 +382,13 @@ export const id_ID: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -36,13 +36,14 @@ export const ja: LanguageTranslation = {
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'ヘルプ',
|
||||
docs_website: 'ドキュメント',
|
||||
visit_website: 'ChartDBにアクセス',
|
||||
join_discord: 'Discordに参加',
|
||||
schedule_a_call: '話しかけてください!',
|
||||
@@ -154,6 +155,8 @@ export const ja: LanguageTranslation = {
|
||||
comments: 'コメント',
|
||||
no_comments: 'コメントがありません',
|
||||
delete_field: 'フィールドを削除',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'インデックス属性',
|
||||
@@ -388,10 +391,13 @@ export const ja: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -34,13 +34,14 @@ export const ko_KR: LanguageTranslation = {
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: '공유',
|
||||
backup: {
|
||||
backup: '백업',
|
||||
export_diagram: '다이어그램 내보내기',
|
||||
import_diagram: '다이어그램 가져오기',
|
||||
restore_diagram: '다이어그램 복구',
|
||||
},
|
||||
help: {
|
||||
help: '도움말',
|
||||
docs_website: '선적 서류 비치',
|
||||
visit_website: 'ChartDB 사이트 방문',
|
||||
join_discord: 'Discord 가입',
|
||||
schedule_a_call: 'Talk with us!',
|
||||
@@ -150,6 +151,8 @@ export const ko_KR: LanguageTranslation = {
|
||||
comments: '주석',
|
||||
no_comments: '주석 없음',
|
||||
delete_field: '필드 삭제',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: '인덱스 속성',
|
||||
@@ -377,10 +380,13 @@ export const ko_KR: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -34,14 +34,15 @@ export const mr: LanguageTranslation = {
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
backup: {
|
||||
// TODO: Add translations
|
||||
share: 'Share',
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'मदत',
|
||||
docs_website: 'दस्तऐवजीकरण',
|
||||
visit_website: 'ChartDB ला भेट द्या',
|
||||
join_discord: 'आमच्या डिस्कॉर्डमध्ये सामील व्हा',
|
||||
schedule_a_call: 'आमच्याशी बोला!',
|
||||
@@ -153,6 +154,8 @@ export const mr: LanguageTranslation = {
|
||||
comments: 'टिप्पण्या',
|
||||
no_comments: 'कोणत्याही टिप्पणी नाहीत',
|
||||
delete_field: 'फील्ड हटवा',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'इंडेक्स गुणधर्म',
|
||||
@@ -389,10 +392,13 @@ export const mr: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -34,13 +34,15 @@ export const ne: LanguageTranslation = {
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: 'शेयर गर्नुहोस्',
|
||||
export_diagram: 'डायाग्राम निर्यात गर्नुहोस्',
|
||||
import_diagram: 'डायाग्राम आयात गर्नुहोस्',
|
||||
// TODO: Translate
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'मद्दत',
|
||||
docs_website: 'कागजात',
|
||||
visit_website: 'वेबसाइटमा जानुहोस्',
|
||||
join_discord: 'डिस्कोर्डमा सामिल हुनुहोस्',
|
||||
schedule_a_call: 'कल अनुसूची गर्नुहोस्',
|
||||
@@ -150,6 +152,8 @@ export const ne: LanguageTranslation = {
|
||||
comments: 'टिप्पणीहरू',
|
||||
no_comments: 'कुनै टिप्पणीहरू छैनन्',
|
||||
delete_field: 'क्षेत्र हटाउनुहोस्',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'सूचक विशेषताहरू',
|
||||
@@ -382,10 +386,13 @@ export const ne: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -35,13 +35,14 @@ export const pt_BR: LanguageTranslation = {
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Exportar Diagrama',
|
||||
restore_diagram: 'Restaurar Diagrama',
|
||||
},
|
||||
help: {
|
||||
help: 'Ajuda',
|
||||
docs_website: 'Documentação',
|
||||
visit_website: 'Visitar ChartDB',
|
||||
join_discord: 'Junte-se a nós no Discord',
|
||||
schedule_a_call: 'Fale Conosco!',
|
||||
@@ -151,6 +152,8 @@ export const pt_BR: LanguageTranslation = {
|
||||
comments: 'Comentários',
|
||||
no_comments: 'Sem comentários',
|
||||
delete_field: 'Excluir Campo',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Atributos do Índice',
|
||||
@@ -382,10 +385,13 @@ export const pt_BR: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -34,13 +34,15 @@ export const ru: LanguageTranslation = {
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: 'Поделиться',
|
||||
export_diagram: 'Экспорт кода диаграммы',
|
||||
import_diagram: 'Импорт кода диаграммы',
|
||||
// TODO: Translate
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'Помощь',
|
||||
docs_website: 'Документация',
|
||||
visit_website: 'Перейти на сайт ChartDB',
|
||||
join_discord: 'Присоединиться к сообществу в Discord',
|
||||
schedule_a_call: 'Поговорите с нами!',
|
||||
@@ -149,6 +151,8 @@ export const ru: LanguageTranslation = {
|
||||
comments: 'Комментарии',
|
||||
no_comments: 'Нет комментария',
|
||||
delete_field: 'Удалить поле',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Атрибуты индекса',
|
||||
@@ -378,10 +382,13 @@ export const ru: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -35,13 +35,14 @@ export const te: LanguageTranslation = {
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'సహాయం',
|
||||
docs_website: 'డాక్యుమెంటేషన్',
|
||||
visit_website: 'ChartDB సందర్శించండి',
|
||||
join_discord: 'డిస్కార్డ్లో మా నుంచి చేరండి',
|
||||
schedule_a_call: 'మాతో మాట్లాడండి!',
|
||||
@@ -151,6 +152,8 @@ export const te: LanguageTranslation = {
|
||||
comments: 'వ్యాఖ్యలు',
|
||||
no_comments: 'వ్యాఖ్యలు లేవు',
|
||||
delete_field: 'ఫీల్డ్ తొలగించు',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'ఇండెక్స్ గుణాలు',
|
||||
@@ -385,10 +388,13 @@ export const te: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -35,13 +35,14 @@ export const tr: LanguageTranslation = {
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'Yardım',
|
||||
docs_website: 'Belgeleme',
|
||||
visit_website: "ChartDB'yi Ziyaret Et",
|
||||
join_discord: "Discord'a Katıl",
|
||||
schedule_a_call: 'Bize Ulaş!',
|
||||
@@ -150,6 +151,8 @@ export const tr: LanguageTranslation = {
|
||||
comments: 'Yorumlar',
|
||||
no_comments: 'Yorum yok',
|
||||
delete_field: 'Alanı Sil',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'İndeks Özellikleri',
|
||||
@@ -372,10 +375,13 @@ export const tr: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -33,13 +33,14 @@ export const uk: LanguageTranslation = {
|
||||
show_minimap: 'Показати мінімапу',
|
||||
hide_minimap: 'Приховати мінімапу',
|
||||
},
|
||||
share: {
|
||||
share: 'Поширити',
|
||||
backup: {
|
||||
backup: 'Резервне копіювання',
|
||||
export_diagram: 'Експорт діаграми',
|
||||
import_diagram: 'Імпорт діаграми',
|
||||
restore_diagram: 'Відновити діаграму',
|
||||
},
|
||||
help: {
|
||||
help: 'Довідка',
|
||||
docs_website: 'Документація',
|
||||
visit_website: 'Сайт ChartDB',
|
||||
join_discord: 'Приєднуйтесь до нас в Діскорд',
|
||||
schedule_a_call: 'Забронювати зустріч!',
|
||||
@@ -149,6 +150,8 @@ export const uk: LanguageTranslation = {
|
||||
comments: 'Коментарі',
|
||||
no_comments: 'Немає коментарів',
|
||||
delete_field: 'Видалити поле',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Атрибути індексу',
|
||||
@@ -377,10 +380,13 @@ export const uk: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -34,13 +34,14 @@ export const vi: LanguageTranslation = {
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: 'Chia sẻ',
|
||||
backup: {
|
||||
backup: 'Hỗ trợ',
|
||||
export_diagram: 'Xuất sơ đồ',
|
||||
import_diagram: 'Nhập sơ đồ',
|
||||
restore_diagram: 'Khôi phục sơ đồ',
|
||||
},
|
||||
help: {
|
||||
help: 'Trợ giúp',
|
||||
docs_website: 'Tài liệu',
|
||||
visit_website: 'Truy cập ChartDB',
|
||||
join_discord: 'Tham gia Discord',
|
||||
schedule_a_call: 'Trò chuyện cùng chúng tôi!',
|
||||
@@ -150,6 +151,8 @@ export const vi: LanguageTranslation = {
|
||||
comments: 'Bình luận',
|
||||
no_comments: 'Không có bình luận',
|
||||
delete_field: 'Xóa trường',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Thuộc tính chỉ mục',
|
||||
@@ -378,10 +381,13 @@ export const vi: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -34,13 +34,14 @@ export const zh_CN: LanguageTranslation = {
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: '分享',
|
||||
backup: {
|
||||
backup: '备份',
|
||||
export_diagram: '导出关系图',
|
||||
import_diagram: '导入关系图',
|
||||
restore_diagram: '还原图表',
|
||||
},
|
||||
help: {
|
||||
help: '帮助',
|
||||
docs_website: '文档',
|
||||
visit_website: '访问 ChartDB',
|
||||
join_discord: '在 Discord 上加入我们',
|
||||
schedule_a_call: '和我们交流!',
|
||||
@@ -147,6 +148,8 @@ export const zh_CN: LanguageTranslation = {
|
||||
comments: '注释',
|
||||
no_comments: '空',
|
||||
delete_field: '删除字段',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: '索引属性',
|
||||
@@ -374,10 +377,13 @@ export const zh_CN: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -34,13 +34,14 @@ export const zh_TW: LanguageTranslation = {
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: '分享',
|
||||
backup: {
|
||||
backup: '備份',
|
||||
export_diagram: '匯出圖表',
|
||||
import_diagram: '匯入圖表',
|
||||
restore_diagram: '恢復圖表',
|
||||
},
|
||||
help: {
|
||||
help: '幫助',
|
||||
docs_website: '文件',
|
||||
visit_website: '訪問 ChartDB 網站',
|
||||
join_discord: '加入 Discord',
|
||||
schedule_a_call: '與我們聯絡!',
|
||||
@@ -147,6 +148,8 @@ export const zh_TW: LanguageTranslation = {
|
||||
comments: '註解',
|
||||
no_comments: '無註解',
|
||||
delete_field: '刪除欄位',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: '索引屬性',
|
||||
@@ -373,10 +376,13 @@ export const zh_TW: LanguageTranslation = {
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const clickhouseDataTypes: readonly DataType[] = [
|
||||
export const clickhouseDataTypes: readonly DataTypeData[] = [
|
||||
// Numeric Types
|
||||
{ name: 'uint8', id: 'uint8' },
|
||||
{ name: 'uint16', id: 'uint16' },
|
||||
@@ -48,25 +48,41 @@ export const clickhouseDataTypes: readonly DataType[] = [
|
||||
{ name: 'mediumblob', id: 'mediumblob' },
|
||||
{ name: 'tinyblob', id: 'tinyblob' },
|
||||
{ name: 'blob', id: 'blob' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'char large object', id: 'char_large_object' },
|
||||
{ name: 'char varying', id: 'char_varying' },
|
||||
{ name: 'char varying', id: 'char_varying', hasCharMaxLength: true },
|
||||
{ name: 'character large object', id: 'character_large_object' },
|
||||
{ name: 'character varying', id: 'character_varying' },
|
||||
{
|
||||
name: 'character varying',
|
||||
id: 'character_varying',
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{ name: 'nchar large object', id: 'nchar_large_object' },
|
||||
{ name: 'nchar varying', id: 'nchar_varying' },
|
||||
{ name: 'nchar varying', id: 'nchar_varying', hasCharMaxLength: true },
|
||||
{
|
||||
name: 'national character large object',
|
||||
id: 'national_character_large_object',
|
||||
},
|
||||
{ name: 'national character varying', id: 'national_character_varying' },
|
||||
{ name: 'national char varying', id: 'national_char_varying' },
|
||||
{ name: 'national character', id: 'national_character' },
|
||||
{ name: 'national char', id: 'national_char' },
|
||||
{
|
||||
name: 'national character varying',
|
||||
id: 'national_character_varying',
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{
|
||||
name: 'national char varying',
|
||||
id: 'national_char_varying',
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{
|
||||
name: 'national character',
|
||||
id: 'national_character',
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{ name: 'national char', id: 'national_char', hasCharMaxLength: true },
|
||||
{ name: 'binary large object', id: 'binary_large_object' },
|
||||
{ name: 'binary varying', id: 'binary_varying' },
|
||||
{ name: 'fixedstring', id: 'fixedstring' },
|
||||
{ name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true },
|
||||
{ name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true },
|
||||
{ name: 'string', id: 'string' },
|
||||
|
||||
// Date Types
|
||||
|
@@ -13,12 +13,16 @@ export interface DataType {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DataTypeData extends DataType {
|
||||
hasCharMaxLength?: boolean;
|
||||
}
|
||||
|
||||
export const dataTypeSchema: z.ZodType<DataType> = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export const dataTypeMap: Record<DatabaseType, readonly DataType[]> = {
|
||||
export const dataTypeMap: Record<DatabaseType, readonly DataTypeData[]> = {
|
||||
[DatabaseType.GENERIC]: genericDataTypes,
|
||||
[DatabaseType.POSTGRESQL]: postgresDataTypes,
|
||||
[DatabaseType.MYSQL]: mysqlDataTypes,
|
||||
@@ -64,3 +68,21 @@ export function areFieldTypesCompatible(
|
||||
}
|
||||
|
||||
export const dataTypes = Object.values(dataTypeMap).flat();
|
||||
|
||||
export const dataTypeDataToDataType = (
|
||||
dataTypeData: DataTypeData
|
||||
): DataType => ({
|
||||
id: dataTypeData.id,
|
||||
name: dataTypeData.name,
|
||||
});
|
||||
|
||||
export const findDataTypeDataById = (
|
||||
id: string,
|
||||
databaseType?: DatabaseType
|
||||
): DataTypeData | undefined => {
|
||||
const dataTypesOptions = databaseType
|
||||
? dataTypeMap[databaseType]
|
||||
: dataTypes;
|
||||
|
||||
return dataTypesOptions.find((dataType) => dataType.id === id);
|
||||
};
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const genericDataTypes: readonly DataType[] = [
|
||||
export const genericDataTypes: readonly DataTypeData[] = [
|
||||
{ name: 'bigint', id: 'bigint' },
|
||||
{ name: 'binary', id: 'binary' },
|
||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||
{ name: 'blob', id: 'blob' },
|
||||
{ name: 'boolean', id: 'boolean' },
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'date', id: 'date' },
|
||||
{ name: 'datetime', id: 'datetime' },
|
||||
{ name: 'decimal', id: 'decimal' },
|
||||
@@ -22,6 +22,6 @@ export const genericDataTypes: readonly DataType[] = [
|
||||
{ name: 'time', id: 'time' },
|
||||
{ name: 'timestamp', id: 'timestamp' },
|
||||
{ name: 'uuid', id: 'uuid' },
|
||||
{ name: 'varbinary', id: 'varbinary' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
] as const;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const mariadbDataTypes: readonly DataType[] = [
|
||||
export const mariadbDataTypes: readonly DataTypeData[] = [
|
||||
// Numeric Types
|
||||
{ name: 'tinyint', id: 'tinyint' },
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
@@ -23,10 +23,10 @@ export const mariadbDataTypes: readonly DataType[] = [
|
||||
{ name: 'year', id: 'year' },
|
||||
|
||||
// String Types
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'binary', id: 'binary' },
|
||||
{ name: 'varbinary', id: 'varbinary' },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||
{ name: 'tinyblob', id: 'tinyblob' },
|
||||
{ name: 'blob', id: 'blob' },
|
||||
{ name: 'mediumblob', id: 'mediumblob' },
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const mysqlDataTypes: readonly DataType[] = [
|
||||
export const mysqlDataTypes: readonly DataTypeData[] = [
|
||||
// Numeric Types
|
||||
{ name: 'tinyint', id: 'tinyint' },
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
@@ -23,10 +23,10 @@ export const mysqlDataTypes: readonly DataType[] = [
|
||||
{ name: 'year', id: 'year' },
|
||||
|
||||
// String Types
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'binary', id: 'binary' },
|
||||
{ name: 'varbinary', id: 'varbinary' },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||
{ name: 'tinyblob', id: 'tinyblob' },
|
||||
{ name: 'blob', id: 'blob' },
|
||||
{ name: 'mediumblob', id: 'mediumblob' },
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const postgresDataTypes: readonly DataType[] = [
|
||||
export const postgresDataTypes: readonly DataTypeData[] = [
|
||||
// Numeric Types
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
{ name: 'integer', id: 'integer' },
|
||||
@@ -15,9 +15,13 @@ export const postgresDataTypes: readonly DataType[] = [
|
||||
{ name: 'money', id: 'money' },
|
||||
|
||||
// Character Types
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'character varying', id: 'character_varying' },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
{
|
||||
name: 'character varying',
|
||||
id: 'character_varying',
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{ name: 'text', id: 'text' },
|
||||
|
||||
// Binary Data Types
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const sqlServerDataTypes: readonly DataType[] = [
|
||||
export const sqlServerDataTypes: readonly DataTypeData[] = [
|
||||
// Exact Numerics
|
||||
{ name: 'bigint', id: 'bigint' },
|
||||
{ name: 'bit', id: 'bit' },
|
||||
@@ -25,18 +25,18 @@ export const sqlServerDataTypes: readonly DataType[] = [
|
||||
{ name: 'time', id: 'time' },
|
||||
|
||||
// Character Strings
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
{ name: 'text', id: 'text' },
|
||||
|
||||
// Unicode Character Strings
|
||||
{ name: 'nchar', id: 'nchar' },
|
||||
{ name: 'nvarchar', id: 'nvarchar' },
|
||||
{ name: 'nchar', id: 'nchar', hasCharMaxLength: true },
|
||||
{ name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true },
|
||||
{ name: 'ntext', id: 'ntext' },
|
||||
|
||||
// Binary Strings
|
||||
{ name: 'binary', id: 'binary' },
|
||||
{ name: 'varbinary', id: 'varbinary' },
|
||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||
{ name: 'image', id: 'image' },
|
||||
|
||||
// Other Data Types
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const sqliteDataTypes: readonly DataType[] = [
|
||||
export const sqliteDataTypes: readonly DataTypeData[] = [
|
||||
// Numeric Types
|
||||
{ name: 'integer', id: 'integer' },
|
||||
{ name: 'real', id: 'real' },
|
||||
@@ -12,6 +12,9 @@ export const sqliteDataTypes: readonly DataType[] = [
|
||||
// Blob Type
|
||||
{ name: 'blob', id: 'blob' },
|
||||
|
||||
// Blob Type
|
||||
{ name: 'json', id: 'json' },
|
||||
|
||||
// Date/Time Types (SQLite uses TEXT, REAL, or INTEGER types for dates and times)
|
||||
{ name: 'date', id: 'date' },
|
||||
{ name: 'datetime', id: 'datetime' },
|
||||
@@ -19,6 +22,6 @@ export const sqliteDataTypes: readonly DataType[] = [
|
||||
{ name: 'int', id: 'int' },
|
||||
{ name: 'float', id: 'float' },
|
||||
{ name: 'boolean', id: 'boolean' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
{ name: 'decimal', id: 'decimal' },
|
||||
] as const;
|
||||
|
82
src/lib/data/export-metadata/export-per-type/common.ts
Normal file
82
src/lib/data/export-metadata/export-per-type/common.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
|
||||
export function isFunction(value: string): boolean {
|
||||
// Common SQL functions
|
||||
const functionPatterns = [
|
||||
/^CURRENT_TIMESTAMP$/i,
|
||||
/^NOW\(\)$/i,
|
||||
/^GETDATE\(\)$/i,
|
||||
/^CURRENT_DATE$/i,
|
||||
/^CURRENT_TIME$/i,
|
||||
/^UUID\(\)$/i,
|
||||
/^NEWID\(\)$/i,
|
||||
/^NEXT VALUE FOR/i,
|
||||
/^IDENTITY\s*\(\d+,\s*\d+\)$/i,
|
||||
];
|
||||
return functionPatterns.some((pattern) => pattern.test(value.trim()));
|
||||
}
|
||||
|
||||
export function isKeyword(value: string): boolean {
|
||||
// Common SQL keywords that can be used as default values
|
||||
const keywords = [
|
||||
'NULL',
|
||||
'TRUE',
|
||||
'FALSE',
|
||||
'CURRENT_TIMESTAMP',
|
||||
'CURRENT_DATE',
|
||||
'CURRENT_TIME',
|
||||
'CURRENT_USER',
|
||||
'SESSION_USER',
|
||||
'SYSTEM_USER',
|
||||
];
|
||||
return keywords.includes(value.trim().toUpperCase());
|
||||
}
|
||||
|
||||
export function strHasQuotes(value: string): boolean {
|
||||
return /^['"].*['"]$/.test(value.trim());
|
||||
}
|
||||
|
||||
export function exportFieldComment(comment: string): string {
|
||||
if (!comment) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return comment
|
||||
.split('\n')
|
||||
.map((commentLine) => ` -- ${commentLine}\n`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
export function getInlineFK(table: DBTable, diagram: Diagram): string {
|
||||
if (!diagram.relationships) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const fks = diagram.relationships
|
||||
.filter((r) => r.sourceTableId === table.id)
|
||||
.map((r) => {
|
||||
const targetTable = diagram.tables?.find(
|
||||
(t) => t.id === r.targetTableId
|
||||
);
|
||||
const sourceField = table.fields.find(
|
||||
(f) => f.id === r.sourceFieldId
|
||||
);
|
||||
const targetField = targetTable?.fields.find(
|
||||
(f) => f.id === r.targetFieldId
|
||||
);
|
||||
|
||||
if (!targetTable || !sourceField || !targetField) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const targetTableName = targetTable.schema
|
||||
? `"${targetTable.schema}"."${targetTable.name}"`
|
||||
: `"${targetTable.name}"`;
|
||||
|
||||
return ` FOREIGN KEY ("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}")`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return fks.join(',\n');
|
||||
}
|
247
src/lib/data/export-metadata/export-per-type/mssql.ts
Normal file
247
src/lib/data/export-metadata/export-per-type/mssql.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import {
|
||||
exportFieldComment,
|
||||
isFunction,
|
||||
isKeyword,
|
||||
strHasQuotes,
|
||||
} from './common';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||
|
||||
function parseMSSQLDefault(field: DBField): string {
|
||||
if (!field.default) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let defaultValue = field.default.trim();
|
||||
|
||||
// Remove type casting for SQL Server
|
||||
defaultValue = defaultValue.split('::')[0];
|
||||
|
||||
// Handle nextval sequences for SQL Server
|
||||
if (defaultValue.includes('nextval')) {
|
||||
return 'IDENTITY(1,1)';
|
||||
}
|
||||
|
||||
// Special handling for SQL Server DEFAULT values
|
||||
if (defaultValue.match(/^\(\(.*\)\)$/)) {
|
||||
// Handle ((0)), ((0.00)) style defaults
|
||||
return defaultValue.replace(/^\(\(|\)\)$/g, '');
|
||||
} else if (defaultValue.match(/^\(N'.*'\)$/)) {
|
||||
// Handle (N'value') style defaults
|
||||
const innerValue = defaultValue.replace(/^\(N'|'\)$/g, '');
|
||||
return `N'${innerValue}'`;
|
||||
} else if (defaultValue.match(/^\(NULL\)$/i)) {
|
||||
// Handle (NULL) defaults
|
||||
return 'NULL';
|
||||
} else if (defaultValue.match(/^\(getdate\(\)\)$/i)) {
|
||||
// Handle (getdate()) defaults
|
||||
return 'getdate()';
|
||||
} else if (defaultValue.match(/^\('?\*'?\)$/i) || defaultValue === '*') {
|
||||
// Handle ('*') or (*) or * defaults - common for "all" values
|
||||
return "N'*'";
|
||||
} else if (defaultValue.match(/^\((['"])(.*)\1\)$/)) {
|
||||
// Handle ('value') or ("value") style defaults
|
||||
const matches = defaultValue.match(/^\((['"])(.*)\1\)$/);
|
||||
return matches ? `N'${matches[2]}'` : defaultValue;
|
||||
}
|
||||
|
||||
// Handle special characters that could be interpreted as operators
|
||||
const sqlServerSpecialChars = /[*+\-/%&|^!=<>~]/;
|
||||
if (sqlServerSpecialChars.test(defaultValue)) {
|
||||
// If the value contains special characters and isn't already properly quoted
|
||||
if (
|
||||
!strHasQuotes(defaultValue) &&
|
||||
!isFunction(defaultValue) &&
|
||||
!isKeyword(defaultValue)
|
||||
) {
|
||||
return `N'${defaultValue.replace(/'/g, "''")}'`;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
strHasQuotes(defaultValue) ||
|
||||
isFunction(defaultValue) ||
|
||||
isKeyword(defaultValue) ||
|
||||
/^-?\d+(\.\d+)?$/.test(defaultValue)
|
||||
) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return `'${defaultValue}'`;
|
||||
}
|
||||
|
||||
export function exportMSSQL(diagram: Diagram): string {
|
||||
if (!diagram.tables || !diagram.relationships) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const tables = diagram.tables;
|
||||
const relationships = diagram.relationships;
|
||||
|
||||
// Create CREATE SCHEMA statements for all schemas
|
||||
let sqlScript = '';
|
||||
const schemas = new Set<string>();
|
||||
|
||||
tables.forEach((table) => {
|
||||
if (table.schema) {
|
||||
schemas.add(table.schema);
|
||||
}
|
||||
});
|
||||
|
||||
// Add schema creation statements
|
||||
schemas.forEach((schema) => {
|
||||
sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n\n`;
|
||||
});
|
||||
|
||||
// Generate table creation SQL
|
||||
sqlScript += tables
|
||||
.map((table: DBTable) => {
|
||||
// Skip views
|
||||
if (table.isView) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const tableName = table.schema
|
||||
? `[${table.schema}].[${table.name}]`
|
||||
: `[${table.name}]`;
|
||||
|
||||
return `${
|
||||
table.comments ? `/**\n${table.comments}\n*/\n` : ''
|
||||
}CREATE TABLE ${tableName} (\n${table.fields
|
||||
.map((field: DBField) => {
|
||||
const fieldName = `[${field.name}]`;
|
||||
const typeName = field.type.name;
|
||||
|
||||
// Handle SQL Server specific type formatting
|
||||
let typeWithSize = typeName;
|
||||
if (field.characterMaximumLength) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'varchar' ||
|
||||
typeName.toLowerCase() === 'nvarchar' ||
|
||||
typeName.toLowerCase() === 'char' ||
|
||||
typeName.toLowerCase() === 'nchar'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
||||
}
|
||||
} else if (field.precision && field.scale) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'decimal' ||
|
||||
typeName.toLowerCase() === 'numeric'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
|
||||
}
|
||||
} else if (field.precision) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'decimal' ||
|
||||
typeName.toLowerCase() === 'numeric'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.precision})`;
|
||||
}
|
||||
}
|
||||
|
||||
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||
|
||||
// Check if identity column
|
||||
const identity = field.default
|
||||
?.toLowerCase()
|
||||
.includes('identity')
|
||||
? ' IDENTITY(1,1)'
|
||||
: '';
|
||||
|
||||
const unique =
|
||||
!field.primaryKey && field.unique ? ' UNIQUE' : '';
|
||||
|
||||
// Handle default value using SQL Server specific parser
|
||||
const defaultValue =
|
||||
field.default &&
|
||||
!field.default.toLowerCase().includes('identity')
|
||||
? ` DEFAULT ${parseMSSQLDefault(field)}`
|
||||
: '';
|
||||
|
||||
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
|
||||
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
|
||||
})
|
||||
.join(',\n')}${
|
||||
table.fields.filter((f) => f.primaryKey).length > 0
|
||||
? `,\n PRIMARY KEY (${table.fields
|
||||
.filter((f) => f.primaryKey)
|
||||
.map((f) => `[${f.name}]`)
|
||||
.join(', ')})`
|
||||
: ''
|
||||
}\n);\n\n${table.indexes
|
||||
.map((index) => {
|
||||
const indexName = table.schema
|
||||
? `[${table.schema}_${index.name}]`
|
||||
: `[${index.name}]`;
|
||||
const indexFields = index.fieldIds
|
||||
.map((fieldId) => {
|
||||
const field = table.fields.find(
|
||||
(f) => f.id === fieldId
|
||||
);
|
||||
return field ? `[${field.name}]` : '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
// SQL Server has a limit of 32 columns in an index
|
||||
if (indexFields.length > 32) {
|
||||
const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`;
|
||||
console.warn(
|
||||
`Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.`
|
||||
);
|
||||
indexFields.length = 32;
|
||||
return indexFields.length > 0
|
||||
? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
|
||||
: '';
|
||||
}
|
||||
|
||||
return indexFields.length > 0
|
||||
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
|
||||
: '';
|
||||
})
|
||||
.join('')}`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings (views)
|
||||
.join('\n');
|
||||
|
||||
// Generate foreign keys
|
||||
sqlScript += `\n${relationships
|
||||
.map((r: DBRelationship) => {
|
||||
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
|
||||
const targetTable = tables.find((t) => t.id === r.targetTableId);
|
||||
|
||||
if (
|
||||
!sourceTable ||
|
||||
!targetTable ||
|
||||
sourceTable.isView ||
|
||||
targetTable.isView
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const sourceField = sourceTable.fields.find(
|
||||
(f) => f.id === r.sourceFieldId
|
||||
);
|
||||
const targetField = targetTable.fields.find(
|
||||
(f) => f.id === r.targetFieldId
|
||||
);
|
||||
|
||||
if (!sourceField || !targetField) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const sourceTableName = sourceTable.schema
|
||||
? `[${sourceTable.schema}].[${sourceTable.name}]`
|
||||
: `[${sourceTable.name}]`;
|
||||
const targetTableName = targetTable.schema
|
||||
? `[${targetTable.schema}].[${targetTable.name}]`
|
||||
: `[${targetTable.name}]`;
|
||||
|
||||
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT [${r.name}] FOREIGN KEY([${sourceField.name}]) REFERENCES ${targetTableName}([${targetField.name}]);\n`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings
|
||||
.join('\n')}`;
|
||||
|
||||
return sqlScript;
|
||||
}
|
@@ -1,9 +1,10 @@
|
||||
import type { Diagram } from '../../domain/diagram';
|
||||
import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env';
|
||||
import type { DatabaseType } from '@/lib/domain/database-type';
|
||||
import { DatabaseType } from '@/lib/domain/database-type';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { DataType } from '../data-types/data-types';
|
||||
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
|
||||
import { exportMSSQL } from './export-per-type/mssql';
|
||||
|
||||
export const exportBaseSQL = (diagram: Diagram): string => {
|
||||
const { tables, relationships } = diagram;
|
||||
@@ -12,6 +13,10 @@ export const exportBaseSQL = (diagram: Diagram): string => {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (diagram.databaseType === DatabaseType.SQL_SERVER) {
|
||||
return exportMSSQL(diagram);
|
||||
}
|
||||
|
||||
// Filter out the tables that are views
|
||||
const nonViewTables = tables.filter((table) => !table.isView);
|
||||
|
||||
@@ -110,8 +115,22 @@ export const exportBaseSQL = (diagram: Diagram): string => {
|
||||
|
||||
// Remove the type cast part after :: if it exists
|
||||
if (fieldDefault.includes('::')) {
|
||||
const endedWithParentheses = fieldDefault.endsWith(')');
|
||||
fieldDefault = fieldDefault.split('::')[0];
|
||||
|
||||
if (
|
||||
(fieldDefault.startsWith('(') &&
|
||||
!fieldDefault.endsWith(')')) ||
|
||||
endedWithParentheses
|
||||
) {
|
||||
fieldDefault += ')';
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldDefault === `('now')`) {
|
||||
fieldDefault = `now()`;
|
||||
}
|
||||
|
||||
sqlScript += ` DEFAULT ${fieldDefault}`;
|
||||
}
|
||||
|
||||
@@ -226,6 +245,10 @@ export const exportSQL = async (
|
||||
}
|
||||
): Promise<string> => {
|
||||
const sqlScript = exportBaseSQL(diagram);
|
||||
if (databaseType === DatabaseType.SQL_SERVER) {
|
||||
return sqlScript;
|
||||
}
|
||||
|
||||
const cacheKey = await generateCacheKey(databaseType, sqlScript);
|
||||
|
||||
const cachedResult = getFromCache(cacheKey);
|
||||
@@ -243,13 +266,16 @@ export const exportSQL = async (
|
||||
|
||||
const apiKey = window?.env?.OPENAI_API_KEY ?? OPENAI_API_KEY;
|
||||
const baseUrl = window?.env?.OPENAI_API_ENDPOINT ?? OPENAI_API_ENDPOINT;
|
||||
const modelName = window?.env?.LLM_MODEL_NAME || 'gpt-4o-mini-2024-07-18';
|
||||
const modelName =
|
||||
window?.env?.LLM_MODEL_NAME ??
|
||||
LLM_MODEL_NAME ??
|
||||
'gpt-4o-mini-2024-07-18';
|
||||
|
||||
let config: { apiKey: string; baseUrl?: string };
|
||||
|
||||
if (useCustomEndpoint) {
|
||||
config = {
|
||||
apiKey: 'sk-xxx', // minimal valid API key format
|
||||
apiKey: apiKey,
|
||||
baseUrl: baseUrl,
|
||||
};
|
||||
} else {
|
||||
|
@@ -85,7 +85,7 @@ export const sqliteQuery = `WITH fk_info AS (
|
||||
ELSE LOWER(p.type)
|
||||
END,
|
||||
'ordinal_position', p.cid,
|
||||
'nullable', (CASE WHEN p."notnull" = 0 THEN 'true' ELSE 'false' END),
|
||||
'nullable', (CASE WHEN p."notnull" = 0 THEN true ELSE false END),
|
||||
'collation', '',
|
||||
'character_maximum_length',
|
||||
CASE
|
||||
|
@@ -117,7 +117,7 @@ indexes AS (
|
||||
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
||||
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
||||
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
||||
WHERE s.name LIKE '%' AND i.name IS NOT NULL
|
||||
WHERE s.name LIKE '%' AND i.name IS NOT NULL AND ic.is_included_column = 0
|
||||
),
|
||||
tbls AS (
|
||||
SELECT
|
||||
@@ -324,6 +324,7 @@ indexes AS (
|
||||
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
||||
WHERE s.name LIKE '%'
|
||||
AND i.name IS NOT NULL
|
||||
AND ic.is_included_column = 0
|
||||
FOR XML PATH('')
|
||||
), 1, 1, ''), '')
|
||||
+ N']' AS all_indexes_json
|
||||
|
@@ -95,7 +95,7 @@ export const createFieldsFromMetadata = ({
|
||||
nullable: col.nullable,
|
||||
...(col.character_maximum_length &&
|
||||
col.character_maximum_length !== 'null'
|
||||
? { character_maximum_length: col.character_maximum_length }
|
||||
? { characterMaximumLength: col.character_maximum_length }
|
||||
: {}),
|
||||
...(col.precision?.precision
|
||||
? { precision: col.precision.precision }
|
||||
|
@@ -103,10 +103,9 @@ const tableToTableNode = (
|
||||
|
||||
export interface CanvasProps {
|
||||
initialTables: DBTable[];
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
||||
export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||
const { getEdge, getInternalNode, getEdges, getNode } = useReactFlow();
|
||||
const [selectedTableIds, setSelectedTableIds] = useState<string[]>([]);
|
||||
const [selectedRelationshipIds, setSelectedRelationshipIds] = useState<
|
||||
@@ -127,6 +126,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
||||
filteredSchemas,
|
||||
events,
|
||||
dependencies,
|
||||
readonly,
|
||||
} = useChartDB();
|
||||
const { showSidePanel } = useLayout();
|
||||
const { effectiveTheme } = useTheme();
|
||||
@@ -682,7 +682,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
||||
|
||||
return (
|
||||
<CanvasContextMenu>
|
||||
<div className="relative flex h-full">
|
||||
<div className="relative flex h-full" id="canvas">
|
||||
<ReactFlow
|
||||
colorMode={effectiveTheme}
|
||||
className="canvas-cursor-default nodes-animated"
|
||||
|
86
src/pages/editor-page/canvas/hooks/use-is-lost-in-canvas.tsx
Normal file
86
src/pages/editor-page/canvas/hooks/use-is-lost-in-canvas.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { getTableDimensions } from '../canvas-utils';
|
||||
import type { TableNodeType } from '../table-node/table-node';
|
||||
import { useOnViewportChange, useReactFlow } from '@xyflow/react';
|
||||
import { useDebounce } from '@/hooks/use-debounce-v2';
|
||||
|
||||
export const useIsLostInCanvas = () => {
|
||||
const { getNodes, getViewport } = useReactFlow();
|
||||
const [noTablesVisible, setNoTablesVisible] = useState<boolean>(false);
|
||||
|
||||
// Check if any tables are visible in the current viewport
|
||||
const checkVisibleTables = useCallback(() => {
|
||||
const nodes = getNodes();
|
||||
const viewport = getViewport();
|
||||
|
||||
// If there are no nodes at all, don't highlight the button
|
||||
if (nodes.length === 0) {
|
||||
setNoTablesVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Count visible (not hidden) nodes
|
||||
const visibleNodes = nodes.filter((node) => !node.hidden);
|
||||
|
||||
// If there are no visible nodes at all, don't highlight the button
|
||||
if (visibleNodes.length === 0) {
|
||||
setNoTablesVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate viewport boundaries
|
||||
const viewportLeft = -viewport.x / viewport.zoom;
|
||||
const viewportTop = -viewport.y / viewport.zoom;
|
||||
|
||||
const width =
|
||||
document.getElementById('canvas')?.clientWidth || window.innerWidth;
|
||||
const height =
|
||||
document.getElementById('canvas')?.clientHeight ||
|
||||
window.innerHeight;
|
||||
|
||||
const viewportRight = viewportLeft + width / viewport.zoom;
|
||||
const viewportBottom = viewportTop + height / viewport.zoom;
|
||||
|
||||
// Check if any node is visible in the viewport
|
||||
const anyNodeVisible = visibleNodes.some((node) => {
|
||||
let nodeWidth = node.width || 0;
|
||||
let nodeHeight = node.height || 0;
|
||||
|
||||
if (node.type === 'table' && node.data?.table) {
|
||||
const tableNodeType = node as TableNodeType;
|
||||
const dimensions = getTableDimensions(tableNodeType.data.table);
|
||||
nodeWidth = dimensions.width;
|
||||
nodeHeight = dimensions.height;
|
||||
}
|
||||
|
||||
// Node boundaries
|
||||
const nodeLeft = node.position.x;
|
||||
const nodeTop = node.position.y;
|
||||
const nodeRight = nodeLeft + nodeWidth;
|
||||
const nodeBottom = nodeTop + nodeHeight;
|
||||
|
||||
return (
|
||||
nodeRight >= viewportLeft &&
|
||||
nodeLeft <= viewportRight &&
|
||||
nodeBottom >= viewportTop &&
|
||||
nodeTop <= viewportBottom
|
||||
);
|
||||
});
|
||||
|
||||
// Only set to true if there are tables but none are visible
|
||||
setNoTablesVisible(!anyNodeVisible);
|
||||
}, [getNodes, getViewport]);
|
||||
|
||||
// Create a debounced version of checkVisibleTables
|
||||
const debouncedCheckVisibleTables = useDebounce(checkVisibleTables, 1000);
|
||||
|
||||
useOnViewportChange({
|
||||
onEnd: () => {
|
||||
debouncedCheckVisibleTables();
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
isLostInCanvas: noTablesVisible,
|
||||
};
|
||||
};
|
@@ -7,6 +7,7 @@ import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { useLayout } from '@/hooks/use-layout';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { getCardinalityMarkerId } from './canvas-utils';
|
||||
import { useDiff } from '@/context/diff-context/use-diff';
|
||||
|
||||
export type RelationshipEdgeType = Edge<
|
||||
{
|
||||
@@ -29,6 +30,7 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
|
||||
}) => {
|
||||
const { getInternalNode, getEdge } = useReactFlow();
|
||||
const { openRelationshipFromSidebar, selectSidebarSection } = useLayout();
|
||||
const { checkIfRelationshipRemoved, checkIfNewRelationship } = useDiff();
|
||||
|
||||
const { relationships } = useChartDB();
|
||||
|
||||
@@ -149,6 +151,25 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
|
||||
}),
|
||||
[relationship?.targetCardinality, selected, targetSide]
|
||||
);
|
||||
|
||||
const isDiffNewRelationship = useMemo(
|
||||
() =>
|
||||
relationship?.id
|
||||
? checkIfNewRelationship({ relationshipId: relationship.id })
|
||||
: false,
|
||||
[checkIfNewRelationship, relationship?.id]
|
||||
);
|
||||
|
||||
const isDiffRelationshipRemoved = useMemo(
|
||||
() =>
|
||||
relationship?.id
|
||||
? checkIfRelationshipRemoved({
|
||||
relationshipId: relationship.id,
|
||||
})
|
||||
: false,
|
||||
[checkIfRelationshipRemoved, relationship?.id]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<path
|
||||
@@ -160,6 +181,10 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
|
||||
className={cn([
|
||||
'react-flow__edge-path',
|
||||
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
|
||||
{
|
||||
'!stroke-green-500': isDiffNewRelationship,
|
||||
'!stroke-red-500': isDiffRelationshipRemoved,
|
||||
},
|
||||
])}
|
||||
onClick={(e) => {
|
||||
if (e.detail === 2) {
|
||||
|
@@ -12,7 +12,15 @@ import {
|
||||
useUpdateNodeInternals,
|
||||
} from '@xyflow/react';
|
||||
import { Button } from '@/components/button/button';
|
||||
import { Check, KeyRound, MessageCircleMore, Trash2 } from 'lucide-react';
|
||||
import {
|
||||
Check,
|
||||
KeyRound,
|
||||
MessageCircleMore,
|
||||
SquareDot,
|
||||
SquareMinus,
|
||||
SquarePlus,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -23,6 +31,7 @@ import {
|
||||
} from '@/components/tooltip/tooltip';
|
||||
import { useClickAway, useKeyPressEvent } from 'react-use';
|
||||
import { Input } from '@/components/input/input';
|
||||
import { useDiff } from '@/context/diff-context/use-diff';
|
||||
|
||||
export const LEFT_HANDLE_ID_PREFIX = 'left_rel_';
|
||||
export const RIGHT_HANDLE_ID_PREFIX = 'right_rel_';
|
||||
@@ -95,6 +104,43 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
useKeyPressEvent('Enter', editFieldName);
|
||||
useKeyPressEvent('Escape', abortEdit);
|
||||
|
||||
const {
|
||||
checkIfFieldRemoved,
|
||||
checkIfNewField,
|
||||
getFieldNewName,
|
||||
getFieldNewType,
|
||||
checkIfFieldHasChange,
|
||||
} = useDiff();
|
||||
|
||||
const isDiffFieldRemoved = useMemo(
|
||||
() => checkIfFieldRemoved({ fieldId: field.id }),
|
||||
[checkIfFieldRemoved, field.id]
|
||||
);
|
||||
|
||||
const isDiffNewField = useMemo(
|
||||
() => checkIfNewField({ fieldId: field.id }),
|
||||
[checkIfNewField, field.id]
|
||||
);
|
||||
|
||||
const fieldDiffChangedName = useMemo(
|
||||
() => getFieldNewName({ fieldId: field.id }),
|
||||
[getFieldNewName, field.id]
|
||||
);
|
||||
|
||||
const fieldDiffChangedType = useMemo(
|
||||
() => getFieldNewType({ fieldId: field.id }),
|
||||
[getFieldNewType, field.id]
|
||||
);
|
||||
|
||||
const isDiffFieldChanged = useMemo(
|
||||
() =>
|
||||
checkIfFieldHasChange({
|
||||
fieldId: field.id,
|
||||
tableId: tableNodeId,
|
||||
}),
|
||||
[checkIfFieldHasChange, field.id, tableNodeId]
|
||||
);
|
||||
|
||||
const enterEditMode = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setEditMode(true);
|
||||
@@ -102,13 +148,23 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800 ${
|
||||
highlighted ? 'bg-pink-100 dark:bg-pink-900' : ''
|
||||
} transition-all duration-200 ease-in-out ${
|
||||
visible
|
||||
? 'max-h-8 opacity-100'
|
||||
: 'z-0 max-h-0 overflow-hidden opacity-0'
|
||||
}`}
|
||||
className={cn(
|
||||
'group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
{
|
||||
'bg-pink-100 dark:bg-pink-900': highlighted,
|
||||
'max-h-8 opacity-100': visible,
|
||||
'z-0 max-h-0 overflow-hidden opacity-0': !visible,
|
||||
'bg-sky-200 dark:bg-sky-800 hover:bg-sky-100 dark:hover:bg-sky-900 border-sky-300 dark:border-sky-700':
|
||||
isDiffFieldChanged &&
|
||||
!isDiffFieldRemoved &&
|
||||
!isDiffNewField,
|
||||
'bg-red-200 dark:bg-red-800 hover:bg-red-100 dark:hover:bg-red-900 border-red-300 dark:border-red-700':
|
||||
isDiffFieldRemoved,
|
||||
'bg-green-200 dark:bg-green-800 hover:bg-green-100 dark:hover:bg-green-900 border-green-300 dark:border-green-700':
|
||||
isDiffNewField,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{isConnectable ? (
|
||||
<>
|
||||
@@ -161,7 +217,14 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
}
|
||||
)}
|
||||
>
|
||||
{editMode ? (
|
||||
{isDiffFieldRemoved ? (
|
||||
<SquareMinus className="size-3.5 text-red-800 dark:text-red-200" />
|
||||
) : isDiffNewField ? (
|
||||
<SquarePlus className="size-3.5 text-green-800 dark:text-green-200" />
|
||||
) : isDiffFieldChanged ? (
|
||||
<SquareDot className="size-3.5 shrink-0 text-sky-800 dark:text-sky-200" />
|
||||
) : null}
|
||||
{editMode && !readonly ? (
|
||||
<>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
@@ -190,10 +253,27 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
// {field.name}
|
||||
// </span>
|
||||
<span
|
||||
className="truncate"
|
||||
className={cn('truncate', {
|
||||
'text-red-800 font-normal dark:text-red-200':
|
||||
isDiffFieldRemoved,
|
||||
'text-green-800 font-normal dark:text-green-200':
|
||||
isDiffNewField,
|
||||
'text-sky-800 font-normal dark:text-sky-200':
|
||||
isDiffFieldChanged &&
|
||||
!isDiffFieldRemoved &&
|
||||
!isDiffNewField,
|
||||
})}
|
||||
onDoubleClick={enterEditMode}
|
||||
>
|
||||
{field.name}
|
||||
{fieldDiffChangedName ? (
|
||||
<>
|
||||
{field.name}{' '}
|
||||
<span className="font-medium">→</span>{' '}
|
||||
{fieldDiffChangedName}
|
||||
</>
|
||||
) : (
|
||||
field.name
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{/* <span className="truncate">{field.name}</span> */}
|
||||
@@ -214,7 +294,18 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
<div
|
||||
className={cn(
|
||||
'text-muted-foreground',
|
||||
!readonly ? 'group-hover:hidden' : ''
|
||||
!readonly ? 'group-hover:hidden' : '',
|
||||
isDiffFieldRemoved
|
||||
? 'text-red-800 dark:text-red-200'
|
||||
: '',
|
||||
isDiffNewField
|
||||
? 'text-green-800 dark:text-green-200'
|
||||
: '',
|
||||
isDiffFieldChanged &&
|
||||
!isDiffFieldRemoved &&
|
||||
!isDiffNewField
|
||||
? 'text-sky-800 dark:text-sky-200'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
<KeyRound size={14} />
|
||||
@@ -223,11 +314,31 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'content-center truncate text-right text-xs text-muted-foreground shrink-0',
|
||||
!readonly ? 'group-hover:hidden' : ''
|
||||
'content-center truncate text-right text-xs text-muted-foreground',
|
||||
!readonly ? 'group-hover:hidden' : '',
|
||||
isDiffFieldRemoved
|
||||
? 'text-red-800 dark:text-red-200'
|
||||
: '',
|
||||
isDiffNewField
|
||||
? 'text-green-800 dark:text-green-200'
|
||||
: '',
|
||||
isDiffFieldChanged &&
|
||||
!isDiffFieldRemoved &&
|
||||
!isDiffNewField
|
||||
? 'text-sky-800 dark:text-sky-200'
|
||||
: ''
|
||||
)}
|
||||
>
|
||||
{field.type.name}
|
||||
{fieldDiffChangedType ? (
|
||||
<>
|
||||
<span className="line-through">
|
||||
{field.type.name.split(' ')[0]}
|
||||
</span>{' '}
|
||||
{fieldDiffChangedType.name.split(' ')[0]}
|
||||
</>
|
||||
) : (
|
||||
field.type.name.split(' ')[0]
|
||||
)}
|
||||
{field.nullable ? '?' : ''}
|
||||
</div>
|
||||
{readonly ? null : (
|
||||
|
@@ -0,0 +1,35 @@
|
||||
import { cn } from '@/lib/utils';
|
||||
import React from 'react';
|
||||
|
||||
export interface TableNodeStatusProps {
|
||||
status: 'new' | 'changed' | 'removed' | 'none';
|
||||
}
|
||||
|
||||
export const TableNodeStatus: React.FC<TableNodeStatusProps> = ({ status }) => {
|
||||
if (status === 'none') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="absolute left-1/2 top-0 z-10 -translate-x-1/2 -translate-y-1/2">
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium text-white',
|
||||
{
|
||||
'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100':
|
||||
status === 'new',
|
||||
'bg-sky-100 text-sky-800 dark:bg-sky-800 dark:text-sky-100':
|
||||
status === 'changed',
|
||||
'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100':
|
||||
status === 'removed',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{status === 'new'
|
||||
? 'New'
|
||||
: status === 'changed'
|
||||
? 'Modified'
|
||||
: 'Deleted'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -10,6 +10,9 @@ import {
|
||||
ChevronUp,
|
||||
Check,
|
||||
CircleDotDashed,
|
||||
SquareDot,
|
||||
SquarePlus,
|
||||
SquareMinus,
|
||||
} from 'lucide-react';
|
||||
import { Label } from '@/components/label/label';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
@@ -30,6 +33,8 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/tooltip/tooltip';
|
||||
import { useDiff } from '@/context/diff-context/use-diff';
|
||||
import { TableNodeStatus } from './table-node-status/table-node-status';
|
||||
|
||||
export type TableNodeType = Node<
|
||||
{
|
||||
@@ -61,6 +66,35 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
const [tableName, setTableName] = useState(table.name);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const {
|
||||
getTableNewName,
|
||||
checkIfTableHasChange,
|
||||
checkIfNewTable,
|
||||
checkIfTableRemoved,
|
||||
} = useDiff();
|
||||
|
||||
const fields = useMemo(() => table.fields, [table.fields]);
|
||||
|
||||
const tableChangedName = useMemo(
|
||||
() => getTableNewName({ tableId: table.id }),
|
||||
[getTableNewName, table.id]
|
||||
);
|
||||
|
||||
const isDiffTableChanged = useMemo(
|
||||
() => checkIfTableHasChange({ tableId: table.id }),
|
||||
[checkIfTableHasChange, table.id]
|
||||
);
|
||||
|
||||
const isDiffNewTable = useMemo(
|
||||
() => checkIfNewTable({ tableId: table.id }),
|
||||
[checkIfNewTable, table.id]
|
||||
);
|
||||
|
||||
const isDiffTableRemoved = useMemo(
|
||||
() => checkIfTableRemoved({ tableId: table.id }),
|
||||
[checkIfTableRemoved, table.id]
|
||||
);
|
||||
|
||||
const selectedRelEdges = edges.filter(
|
||||
(edge) =>
|
||||
(edge.source === id || edge.target === id) &&
|
||||
@@ -109,13 +143,13 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
|
||||
const visibleFields = useMemo(() => {
|
||||
if (expanded) {
|
||||
return table.fields;
|
||||
return fields;
|
||||
}
|
||||
|
||||
const mustDisplayedFields = table.fields.filter((field: DBField) =>
|
||||
const mustDisplayedFields = fields.filter((field: DBField) =>
|
||||
isMustDisplayedField(field)
|
||||
);
|
||||
const nonMustDisplayedFields = table.fields.filter(
|
||||
const nonMustDisplayedFields = fields.filter(
|
||||
(field: DBField) => !isMustDisplayedField(field)
|
||||
);
|
||||
|
||||
@@ -133,8 +167,8 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
return [
|
||||
...visibleMustDisplayedFields,
|
||||
...visibleNonMustDisplayedFields,
|
||||
].sort((a, b) => table.fields.indexOf(a) - table.fields.indexOf(b));
|
||||
}, [expanded, table.fields, isMustDisplayedField]);
|
||||
].sort((a, b) => fields.indexOf(a) - fields.indexOf(b));
|
||||
}, [expanded, fields, isMustDisplayedField]);
|
||||
|
||||
const editTableName = useCallback(() => {
|
||||
if (!editMode) return;
|
||||
@@ -174,6 +208,17 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
: '',
|
||||
highlightOverlappingTables && isOverlapping
|
||||
? 'animate-scale-2'
|
||||
: '',
|
||||
isDiffTableChanged &&
|
||||
!isDiffNewTable &&
|
||||
!isDiffTableRemoved
|
||||
? 'outline outline-[3px] outline-sky-500 dark:outline-sky-900 outline-offset-[5px]'
|
||||
: '',
|
||||
isDiffNewTable
|
||||
? 'outline outline-[3px] outline-green-500 dark:outline-green-900 outline-offset-[5px]'
|
||||
: '',
|
||||
isDiffTableRemoved
|
||||
? 'outline outline-[3px] outline-red-500 dark:outline-red-900 outline-offset-[5px]'
|
||||
: ''
|
||||
)}
|
||||
onClick={(e) => {
|
||||
@@ -194,14 +239,87 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
table={table}
|
||||
focused={focused}
|
||||
/>
|
||||
{/* Badge added here */}
|
||||
<TableNodeStatus
|
||||
status={
|
||||
isDiffNewTable
|
||||
? 'new'
|
||||
: isDiffTableRemoved
|
||||
? 'removed'
|
||||
: isDiffTableChanged
|
||||
? 'changed'
|
||||
: 'none'
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="h-2 rounded-t-[6px]"
|
||||
style={{ backgroundColor: table.color }}
|
||||
></div>
|
||||
<div className="group flex h-9 items-center justify-between bg-slate-200 px-2 dark:bg-slate-900">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
|
||||
{editMode ? (
|
||||
{isDiffNewTable ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SquarePlus
|
||||
className="size-3.5 shrink-0 text-green-600"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>New Table</TooltipContent>
|
||||
</Tooltip>
|
||||
) : isDiffTableRemoved ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SquareMinus
|
||||
className="size-3.5 shrink-0 text-red-600"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Table Removed
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : isDiffTableChanged ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<SquareDot
|
||||
className="size-3.5 shrink-0 text-sky-600"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Table Changed
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
|
||||
)}
|
||||
|
||||
{tableChangedName ? (
|
||||
<Label className="flex h-5 items-center justify-center truncate rounded-sm bg-sky-200 px-2 py-0.5 text-sm font-normal text-sky-900 dark:bg-sky-800 dark:text-sky-200">
|
||||
<span className="truncate">
|
||||
{table.name}
|
||||
</span>
|
||||
<span className="mx-1 font-semibold">
|
||||
→
|
||||
</span>
|
||||
<span className="truncate">
|
||||
{tableChangedName}
|
||||
</span>
|
||||
</Label>
|
||||
) : isDiffNewTable ? (
|
||||
<Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-green-200 px-2 py-0.5 text-sm font-normal text-green-900 dark:bg-green-800 dark:text-green-200">
|
||||
{table.name}
|
||||
</Label>
|
||||
) : isDiffTableRemoved ? (
|
||||
<Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-red-200 px-2 py-0.5 text-sm font-normal text-red-900 dark:bg-red-800 dark:text-red-200">
|
||||
{table.name}
|
||||
</Label>
|
||||
) : isDiffTableChanged ? (
|
||||
<Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-sky-200 px-2 py-0.5 text-sm font-normal text-sky-900 dark:bg-sky-800 dark:text-sky-200">
|
||||
{table.name}
|
||||
</Label>
|
||||
) : editMode && !readonly ? (
|
||||
<>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
@@ -273,11 +391,11 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
className="transition-[max-height] duration-200 ease-in-out"
|
||||
style={{
|
||||
maxHeight: expanded
|
||||
? `${table.fields.length * 2}rem` // h-8 per field
|
||||
? `${fields.length * 2}rem` // h-8 per field
|
||||
: `${TABLE_MINIMIZED_FIELDS * 2}rem`, // h-8 per field
|
||||
}}
|
||||
>
|
||||
{table.fields.map((field: DBField) => (
|
||||
{fields.map((field: DBField) => (
|
||||
<TableNodeField
|
||||
key={field.id}
|
||||
focused={focused}
|
||||
@@ -295,7 +413,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{table.fields.length > TABLE_MINIMIZED_FIELDS && (
|
||||
{fields.length > TABLE_MINIMIZED_FIELDS && (
|
||||
<div
|
||||
className="z-10 flex h-8 cursor-pointer items-center justify-center rounded-b-md border-t text-xs text-muted-foreground transition-colors duration-200 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
onClick={toggleExpand}
|
||||
|
@@ -1,17 +1,22 @@
|
||||
import React from 'react';
|
||||
import type { ButtonProps } from '@/components/button/button';
|
||||
import { Button } from '@/components/button/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export const ToolbarButton = React.forwardRef<
|
||||
React.ElementRef<typeof Button>,
|
||||
ButtonProps
|
||||
>((props, ref) => {
|
||||
const { className, ...rest } = props;
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
className={'w-[36px] p-2 hover:bg-primary-foreground'}
|
||||
{...props}
|
||||
className={cn(
|
||||
'w-[36px] p-2 hover:bg-primary-foreground',
|
||||
className
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@@ -15,6 +15,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@/components/button/button';
|
||||
import { keyboardShortcutsForOS } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
|
||||
import { KeyboardShortcutAction } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
|
||||
import { useIsLostInCanvas } from '../hooks/use-is-lost-in-canvas';
|
||||
|
||||
const convertToPercentage = (value: number) => `${Math.round(value * 100)}%`;
|
||||
|
||||
@@ -28,6 +29,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
|
||||
const { redo, undo, hasRedo, hasUndo } = useHistory();
|
||||
const { getZoom, zoomIn, zoomOut, fitView } = useReactFlow();
|
||||
const [zoom, setZoom] = useState<string>(convertToPercentage(getZoom()));
|
||||
const { isLostInCanvas } = useIsLostInCanvas();
|
||||
|
||||
useOnViewportChange({
|
||||
onChange: ({ zoom }) => {
|
||||
setZoom(convertToPercentage(zoom));
|
||||
@@ -93,7 +96,14 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<ToolbarButton onClick={showAll}>
|
||||
<ToolbarButton
|
||||
onClick={showAll}
|
||||
className={
|
||||
isLostInCanvas
|
||||
? 'bg-pink-500 text-white hover:bg-pink-600 hover:text-white'
|
||||
: ''
|
||||
}
|
||||
>
|
||||
<Scan />
|
||||
</ToolbarButton>
|
||||
</span>
|
||||
|
@@ -1,22 +1,12 @@
|
||||
import React, {
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { Suspense, useCallback, useEffect, useRef } from 'react';
|
||||
import { TopNavbar } from './top-navbar/top-navbar';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { useDialog } from '@/hooks/use-dialog';
|
||||
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
|
||||
import { Toaster } from '@/components/toast/toaster';
|
||||
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
|
||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
||||
import { useLayout } from '@/hooks/use-layout';
|
||||
import { useToast } from '@/components/toast/use-toast';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import { ToastAction } from '@/components/toast/toast';
|
||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -35,18 +25,15 @@ 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';
|
||||
import { AlertProvider } from '@/context/alert-context/alert-provider';
|
||||
import { CanvasProvider } from '@/context/canvas-context/canvas-provider';
|
||||
import { HIDE_BUCKLE_DOT_DEV } from '@/lib/env';
|
||||
import { useDiagramLoader } from './use-diagram-loader';
|
||||
import { DiffProvider } from '@/context/diff-context/diff-provider';
|
||||
|
||||
const OPEN_STAR_US_AFTER_SECONDS = 30;
|
||||
const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1;
|
||||
|
||||
const OPEN_BUCKLE_AFTER_SECONDS = 60;
|
||||
const SHOW_BUCKLE_AGAIN_AFTER_DAYS = 1;
|
||||
const SHOW_BUCKLE_AGAIN_OPENED_AFTER_DAYS = 7;
|
||||
|
||||
export const EditorDesktopLayoutLazy = React.lazy(
|
||||
() => import('./editor-desktop-layout')
|
||||
);
|
||||
@@ -56,102 +43,22 @@ export const EditorMobileLayoutLazy = React.lazy(
|
||||
);
|
||||
|
||||
const EditorPageComponent: React.FC = () => {
|
||||
const {
|
||||
loadDiagram,
|
||||
diagramName,
|
||||
currentDiagram,
|
||||
schemas,
|
||||
filteredSchemas,
|
||||
} = useChartDB();
|
||||
const { diagramName, currentDiagram, schemas, filteredSchemas } =
|
||||
useChartDB();
|
||||
const { openSelectSchema, showSidePanel } = useLayout();
|
||||
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
|
||||
const { showLoader, hideLoader } = useFullScreenLoader();
|
||||
const { openCreateDiagramDialog, openStarUsDialog, openBuckleDialog } =
|
||||
useDialog();
|
||||
const { openStarUsDialog } = useDialog();
|
||||
const { diagramId } = useParams<{ diagramId: string }>();
|
||||
const { config, updateConfig } = useConfig();
|
||||
const navigate = useNavigate();
|
||||
const { isMd: isDesktop } = useBreakpoint('md');
|
||||
const [initialDiagram, setInitialDiagram] = useState<Diagram | undefined>();
|
||||
const {
|
||||
hideMultiSchemaNotification,
|
||||
setHideMultiSchemaNotification,
|
||||
starUsDialogLastOpen,
|
||||
setStarUsDialogLastOpen,
|
||||
githubRepoOpened,
|
||||
setBuckleDialogLastOpen,
|
||||
buckleDialogLastOpen,
|
||||
buckleWaitlistOpened,
|
||||
} = useLocalConfig();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { listDiagrams } = useStorage();
|
||||
|
||||
useEffect(() => {
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentDiagram?.id === diagramId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadDefaultDiagram = async () => {
|
||||
if (diagramId) {
|
||||
setInitialDiagram(undefined);
|
||||
showLoader();
|
||||
resetRedoStack();
|
||||
resetUndoStack();
|
||||
const diagram = await loadDiagram(diagramId);
|
||||
if (!diagram) {
|
||||
if (currentDiagram?.id) {
|
||||
await updateConfig({
|
||||
defaultDiagramId: currentDiagram.id,
|
||||
});
|
||||
navigate(`/diagrams/${currentDiagram.id}`);
|
||||
} else {
|
||||
navigate('/');
|
||||
}
|
||||
}
|
||||
setInitialDiagram(diagram);
|
||||
hideLoader();
|
||||
} else if (!diagramId && config.defaultDiagramId) {
|
||||
const diagram = await loadDiagram(config.defaultDiagramId);
|
||||
if (!diagram) {
|
||||
await updateConfig({
|
||||
defaultDiagramId: '',
|
||||
});
|
||||
navigate('/');
|
||||
} else {
|
||||
navigate(`/diagrams/${config.defaultDiagramId}`);
|
||||
}
|
||||
} else {
|
||||
const diagrams = await listDiagrams();
|
||||
|
||||
if (diagrams.length > 0) {
|
||||
const defaultDiagramId = diagrams[0].id;
|
||||
await updateConfig({ defaultDiagramId });
|
||||
navigate(`/diagrams/${defaultDiagramId}`);
|
||||
} else {
|
||||
openCreateDiagramDialog();
|
||||
}
|
||||
}
|
||||
};
|
||||
loadDefaultDiagram();
|
||||
}, [
|
||||
diagramId,
|
||||
openCreateDiagramDialog,
|
||||
config,
|
||||
navigate,
|
||||
listDiagrams,
|
||||
loadDiagram,
|
||||
resetRedoStack,
|
||||
resetUndoStack,
|
||||
hideLoader,
|
||||
showLoader,
|
||||
currentDiagram?.id,
|
||||
updateConfig,
|
||||
]);
|
||||
const { initialDiagram } = useDiagramLoader();
|
||||
|
||||
useEffect(() => {
|
||||
if (HIDE_BUCKLE_DOT_DEV) {
|
||||
@@ -178,37 +85,6 @@ const EditorPageComponent: React.FC = () => {
|
||||
starUsDialogLastOpen,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (HIDE_BUCKLE_DOT_DEV) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentDiagram?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
new Date().getTime() - buckleDialogLastOpen >
|
||||
1000 *
|
||||
60 *
|
||||
60 *
|
||||
24 *
|
||||
(buckleWaitlistOpened
|
||||
? SHOW_BUCKLE_AGAIN_OPENED_AFTER_DAYS
|
||||
: SHOW_BUCKLE_AGAIN_AFTER_DAYS)
|
||||
) {
|
||||
const lastOpen = new Date().getTime();
|
||||
setBuckleDialogLastOpen(lastOpen);
|
||||
setTimeout(openBuckleDialog, OPEN_BUCKLE_AFTER_SECONDS * 1000);
|
||||
}
|
||||
}, [
|
||||
currentDiagram?.id,
|
||||
buckleWaitlistOpened,
|
||||
openBuckleDialog,
|
||||
setBuckleDialogLastOpen,
|
||||
buckleDialogLastOpen,
|
||||
]);
|
||||
|
||||
const lastDiagramId = useRef<string>('');
|
||||
|
||||
const handleChangeSchema = useCallback(async () => {
|
||||
@@ -322,23 +198,25 @@ export const EditorPage: React.FC = () => (
|
||||
<StorageProvider>
|
||||
<ConfigProvider>
|
||||
<RedoUndoStackProvider>
|
||||
<ChartDBProvider>
|
||||
<HistoryProvider>
|
||||
<ReactFlowProvider>
|
||||
<CanvasProvider>
|
||||
<ExportImageProvider>
|
||||
<AlertProvider>
|
||||
<DialogProvider>
|
||||
<KeyboardShortcutsProvider>
|
||||
<EditorPageComponent />
|
||||
</KeyboardShortcutsProvider>
|
||||
</DialogProvider>
|
||||
</AlertProvider>
|
||||
</ExportImageProvider>
|
||||
</CanvasProvider>
|
||||
</ReactFlowProvider>
|
||||
</HistoryProvider>
|
||||
</ChartDBProvider>
|
||||
<DiffProvider>
|
||||
<ChartDBProvider>
|
||||
<HistoryProvider>
|
||||
<ReactFlowProvider>
|
||||
<CanvasProvider>
|
||||
<ExportImageProvider>
|
||||
<AlertProvider>
|
||||
<DialogProvider>
|
||||
<KeyboardShortcutsProvider>
|
||||
<EditorPageComponent />
|
||||
</KeyboardShortcutsProvider>
|
||||
</DialogProvider>
|
||||
</AlertProvider>
|
||||
</ExportImageProvider>
|
||||
</CanvasProvider>
|
||||
</ReactFlowProvider>
|
||||
</HistoryProvider>
|
||||
</ChartDBProvider>
|
||||
</DiffProvider>
|
||||
</RedoUndoStackProvider>
|
||||
</ConfigProvider>
|
||||
</StorageProvider>
|
||||
|
@@ -0,0 +1,162 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Ellipsis, Trash2 } from 'lucide-react';
|
||||
import { Input } from '@/components/input/input';
|
||||
import { Button } from '@/components/button/button';
|
||||
import { Separator } from '@/components/separator/separator';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/popover/popover';
|
||||
import { Label } from '@/components/label/label';
|
||||
import { Checkbox } from '@/components/checkbox/checkbox';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Textarea } from '@/components/textarea/textarea';
|
||||
import { debounce } from '@/lib/utils';
|
||||
|
||||
export interface TableFieldPopoverProps {
|
||||
field: DBField;
|
||||
updateField: (attrs: Partial<DBField>) => void;
|
||||
removeField: () => void;
|
||||
}
|
||||
|
||||
export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
|
||||
field,
|
||||
updateField,
|
||||
removeField,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [localField, setLocalField] = React.useState<DBField>(field);
|
||||
|
||||
const debouncedUpdateFieldRef = useRef<((value?: DBField) => void) | null>(
|
||||
null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
debouncedUpdateFieldRef.current = debounce((value?: DBField) => {
|
||||
updateField({
|
||||
comments: value?.comments,
|
||||
characterMaximumLength: value?.characterMaximumLength,
|
||||
unique: value?.unique,
|
||||
});
|
||||
}, 200);
|
||||
|
||||
return () => {
|
||||
debouncedUpdateFieldRef.current = null;
|
||||
};
|
||||
}, [updateField]);
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedUpdateFieldRef.current) {
|
||||
debouncedUpdateFieldRef.current(localField);
|
||||
}
|
||||
}, [localField]);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
onOpenChange={(isOpen) => {
|
||||
if (isOpen) {
|
||||
setLocalField(field);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-[32px] p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
|
||||
>
|
||||
<Ellipsis className="size-3.5" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-52">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-sm font-semibold">
|
||||
{t(
|
||||
'side_panel.tables_section.table.field_actions.title'
|
||||
)}
|
||||
</div>
|
||||
<Separator orientation="horizontal" />
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="width" className="text-subtitle">
|
||||
{t(
|
||||
'side_panel.tables_section.table.field_actions.unique'
|
||||
)}
|
||||
</Label>
|
||||
<Checkbox
|
||||
checked={localField.unique}
|
||||
disabled={field.primaryKey}
|
||||
onCheckedChange={(value) =>
|
||||
setLocalField((current) => ({
|
||||
...current,
|
||||
unique: !!value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{findDataTypeDataById(field.type.id)
|
||||
?.hasCharMaxLength ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label
|
||||
htmlFor="width"
|
||||
className="text-subtitle"
|
||||
>
|
||||
{t(
|
||||
'side_panel.tables_section.table.field_actions.character_length'
|
||||
)}
|
||||
</Label>
|
||||
<Input
|
||||
value={
|
||||
localField.characterMaximumLength ?? ''
|
||||
}
|
||||
type="number"
|
||||
onChange={(e) =>
|
||||
setLocalField((current) => ({
|
||||
...current,
|
||||
characterMaximumLength:
|
||||
e.target.value,
|
||||
}))
|
||||
}
|
||||
className="w-full rounded-md bg-muted text-sm"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="width" className="text-subtitle">
|
||||
{t(
|
||||
'side_panel.tables_section.table.field_actions.comments'
|
||||
)}
|
||||
</Label>
|
||||
<Textarea
|
||||
value={localField.comments}
|
||||
onChange={(e) =>
|
||||
setLocalField((current) => ({
|
||||
...current,
|
||||
comments: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder={t(
|
||||
'side_panel.tables_section.table.field_actions.no_comments'
|
||||
)}
|
||||
className="w-full rounded-md bg-muted text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Separator orientation="horizontal" />
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex gap-2 !text-red-700"
|
||||
onClick={removeField}
|
||||
>
|
||||
<Trash2 className="size-3.5 text-red-700" />
|
||||
{t(
|
||||
'side_panel.tables_section.table.field_actions.delete_field'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
@@ -1,30 +1,27 @@
|
||||
import React from 'react';
|
||||
import { Ellipsis, GripVertical, Trash2, KeyRound } from 'lucide-react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { GripVertical, KeyRound } from 'lucide-react';
|
||||
import { Input } from '@/components/input/input';
|
||||
import { Button } from '@/components/button/button';
|
||||
import { Separator } from '@/components/separator/separator';
|
||||
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { dataTypeMap } from '@/lib/data/data-types/data-types';
|
||||
import {
|
||||
dataTypeDataToDataType,
|
||||
dataTypeMap,
|
||||
} from '@/lib/data/data-types/data-types';
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/tooltip/tooltip';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/popover/popover';
|
||||
import { Label } from '@/components/label/label';
|
||||
import { Checkbox } from '@/components/checkbox/checkbox';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Textarea } from '@/components/textarea/textarea';
|
||||
import { TableFieldToggle } from './table-field-toggle';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import type {
|
||||
SelectBoxOption,
|
||||
SelectBoxProps,
|
||||
} from '@/components/select-box/select-box';
|
||||
import { SelectBox } from '@/components/select-box/select-box';
|
||||
import { TableFieldPopover } from './table-field-modal/table-field-modal';
|
||||
|
||||
export interface TableFieldProps {
|
||||
field: DBField;
|
||||
@@ -39,13 +36,55 @@ export const TableField: React.FC<TableFieldProps> = ({
|
||||
}) => {
|
||||
const { databaseType } = useChartDB();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({ id: field.id });
|
||||
|
||||
const dataFieldOptions = dataTypeMap[databaseType].map((type) => ({
|
||||
label: type.name,
|
||||
value: type.id,
|
||||
}));
|
||||
const dataFieldOptions: SelectBoxOption[] = dataTypeMap[databaseType].map(
|
||||
(type) => ({
|
||||
label: type.name,
|
||||
value: type.id,
|
||||
regex: type.hasCharMaxLength
|
||||
? `^${type.name}\\(\\d+\\)$`
|
||||
: undefined,
|
||||
extractRegex: type.hasCharMaxLength ? /\((\d+)\)/ : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
const onChangeDataType = useCallback<
|
||||
NonNullable<SelectBoxProps['onChange']>
|
||||
>(
|
||||
(value, regexMatches) => {
|
||||
const dataType = dataTypeMap[databaseType].find(
|
||||
(v) => v.id === value
|
||||
) ?? {
|
||||
id: value as string,
|
||||
name: value as string,
|
||||
};
|
||||
|
||||
let characterMaximumLength: string | undefined = undefined;
|
||||
|
||||
if (regexMatches?.length && dataType?.hasCharMaxLength) {
|
||||
characterMaximumLength = regexMatches[1];
|
||||
} else if (
|
||||
field.characterMaximumLength &&
|
||||
dataType?.hasCharMaxLength
|
||||
) {
|
||||
characterMaximumLength = field.characterMaximumLength;
|
||||
}
|
||||
|
||||
updateField({
|
||||
characterMaximumLength,
|
||||
type: dataTypeDataToDataType(
|
||||
dataType ?? {
|
||||
id: value as string,
|
||||
name: value as string,
|
||||
}
|
||||
),
|
||||
});
|
||||
},
|
||||
[updateField, databaseType, field.characterMaximumLength]
|
||||
);
|
||||
|
||||
const style = {
|
||||
transform: CSS.Translate.toString(transform),
|
||||
@@ -96,20 +135,39 @@ export const TableField: React.FC<TableFieldProps> = ({
|
||||
'side_panel.tables_section.table.field_type'
|
||||
)}
|
||||
value={field.type.id}
|
||||
onChange={(value) =>
|
||||
updateField({
|
||||
type: dataTypeMap[databaseType].find(
|
||||
(v) => v.id === value
|
||||
),
|
||||
})
|
||||
valueSuffix={
|
||||
field.characterMaximumLength
|
||||
? `(${field.characterMaximumLength})`
|
||||
: ''
|
||||
}
|
||||
optionSuffix={(option) => {
|
||||
const type = dataTypeMap[databaseType].find(
|
||||
(v) => v.id === option.value
|
||||
);
|
||||
|
||||
if (!type) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (type.hasCharMaxLength) {
|
||||
return `(${!field.characterMaximumLength ? 'n' : field.characterMaximumLength})`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}}
|
||||
onChange={onChangeDataType}
|
||||
emptyPlaceholder={t(
|
||||
'side_panel.tables_section.table.no_types_found'
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{field.type.name}</TooltipContent>
|
||||
<TooltipContent>
|
||||
{field.type.name}
|
||||
{field.characterMaximumLength
|
||||
? `(${field.characterMaximumLength})`
|
||||
: ''}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex w-4/12 justify-end gap-1 overflow-hidden">
|
||||
@@ -152,80 +210,11 @@ export const TableField: React.FC<TableFieldProps> = ({
|
||||
{t('side_panel.tables_section.table.primary_key')}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="h-8 w-[32px] p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
|
||||
>
|
||||
<Ellipsis className="size-3.5" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-52">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-sm font-semibold">
|
||||
{t(
|
||||
'side_panel.tables_section.table.field_actions.title'
|
||||
)}
|
||||
</div>
|
||||
<Separator orientation="horizontal" />
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label
|
||||
htmlFor="width"
|
||||
className="text-subtitle"
|
||||
>
|
||||
{t(
|
||||
'side_panel.tables_section.table.field_actions.unique'
|
||||
)}
|
||||
</Label>
|
||||
<Checkbox
|
||||
checked={field.unique}
|
||||
disabled={field.primaryKey}
|
||||
onCheckedChange={(value) =>
|
||||
updateField({
|
||||
unique: !!value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label
|
||||
htmlFor="width"
|
||||
className="text-subtitle"
|
||||
>
|
||||
{t(
|
||||
'side_panel.tables_section.table.field_actions.comments'
|
||||
)}
|
||||
</Label>
|
||||
<Textarea
|
||||
value={field.comments}
|
||||
onChange={(e) =>
|
||||
updateField({
|
||||
comments: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder={t(
|
||||
'side_panel.tables_section.table.field_actions.no_comments'
|
||||
)}
|
||||
className="w-full rounded-md bg-muted text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Separator orientation="horizontal" />
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex gap-2 !text-red-700"
|
||||
onClick={removeField}
|
||||
>
|
||||
<Trash2 className="size-3.5 text-red-700" />
|
||||
{t(
|
||||
'side_panel.tables_section.table.field_actions.delete_field'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<TableFieldPopover
|
||||
field={field}
|
||||
updateField={updateField}
|
||||
removeField={removeField}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { Plus, FileType2, FileKey2, MessageCircleMore } from 'lucide-react';
|
||||
import { Button } from '@/components/button/button';
|
||||
import {
|
||||
@@ -70,17 +70,29 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const createIndexHandler = () => {
|
||||
setSelectedItems((prev) => {
|
||||
if (prev.includes('indexes')) {
|
||||
return prev;
|
||||
}
|
||||
const createIndexHandler = useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
setSelectedItems((prev) => {
|
||||
if (prev.includes('indexes')) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return [...prev, 'indexes'];
|
||||
});
|
||||
return [...prev, 'indexes'];
|
||||
});
|
||||
|
||||
createIndex(table.id);
|
||||
};
|
||||
createIndex(table.id);
|
||||
},
|
||||
[createIndex, table.id, setSelectedItems]
|
||||
);
|
||||
|
||||
const createFieldHandler = useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
createField(table.id);
|
||||
},
|
||||
[createField, table.id]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -113,10 +125,7 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="size-4 p-0 text-xs hover:bg-primary-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
createField(table.id);
|
||||
}}
|
||||
onClick={createFieldHandler}
|
||||
>
|
||||
<Plus className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||
</Button>
|
||||
@@ -153,6 +162,18 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
<div className="flex justify-start p-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex h-7 items-center gap-1 px-2 text-xs"
|
||||
onClick={createFieldHandler}
|
||||
>
|
||||
<Plus className="size-4 text-muted-foreground" />
|
||||
{t(
|
||||
'side_panel.tables_section.table.add_field'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</DndContext>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
@@ -173,10 +194,7 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="size-4 p-0 text-xs hover:bg-primary-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
createIndexHandler();
|
||||
}}
|
||||
onClick={createIndexHandler}
|
||||
>
|
||||
<Plus className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||
</Button>
|
||||
@@ -198,6 +216,16 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
|
||||
fields={table.fields}
|
||||
/>
|
||||
))}
|
||||
<div className="flex justify-start p-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex h-7 items-center gap-1 px-2 text-xs"
|
||||
onClick={createIndexHandler}
|
||||
>
|
||||
<Plus className="size-4 text-muted-foreground" />
|
||||
{t('side_panel.tables_section.table.add_index')}
|
||||
</Button>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
@@ -248,7 +276,7 @@ export const TableListItemContent: React.FC<TableListItemContentProps> = ({
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 p-2 text-xs"
|
||||
onClick={() => createField(table.id)}
|
||||
onClick={createFieldHandler}
|
||||
>
|
||||
<FileType2 className="h-4" />
|
||||
{t('side_panel.tables_section.table.add_field')}
|
||||
|
@@ -102,6 +102,10 @@ export const Menu: React.FC<MenuProps> = () => {
|
||||
window.location.href = 'https://chartdb.io';
|
||||
}, []);
|
||||
|
||||
const openChartDBDocs = useCallback(() => {
|
||||
window.open('https://docs.chartdb.io', '_blank');
|
||||
}, []);
|
||||
|
||||
const openJoinDiscord = useCallback(() => {
|
||||
window.open('https://discord.gg/QeFwyWSKwC', '_blank');
|
||||
}, []);
|
||||
@@ -225,6 +229,9 @@ export const Menu: React.FC<MenuProps> = () => {
|
||||
{t('menu.file.import')}
|
||||
</MenubarSubTrigger>
|
||||
<MenubarSubContent>
|
||||
<MenubarItem onClick={openImportDiagramDialog}>
|
||||
.json
|
||||
</MenubarItem>
|
||||
<MenubarItem onClick={() => openImportDBMLDialog()}>
|
||||
.dbml
|
||||
</MenubarItem>
|
||||
@@ -341,6 +348,10 @@ export const Menu: React.FC<MenuProps> = () => {
|
||||
<MenubarItem onClick={exportPNG}>PNG</MenubarItem>
|
||||
<MenubarItem onClick={exportJPG}>JPG</MenubarItem>
|
||||
<MenubarItem onClick={exportSVG}>SVG</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem onClick={openExportDiagramDialog}>
|
||||
JSON
|
||||
</MenubarItem>
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
<MenubarSeparator />
|
||||
@@ -459,8 +470,16 @@ export const Menu: React.FC<MenuProps> = () => {
|
||||
</MenubarSub>
|
||||
<MenubarSeparator />
|
||||
<MenubarSub>
|
||||
<MenubarSubTrigger>
|
||||
{t('menu.view.theme')}
|
||||
<MenubarSubTrigger className="flex items-center gap-1">
|
||||
<span>{t('menu.view.theme')}</span>
|
||||
<div className="flex-1" />
|
||||
<MenubarShortcut>
|
||||
{
|
||||
keyboardShortcutsForOS[
|
||||
KeyboardShortcutAction.TOGGLE_THEME
|
||||
].keyCombinationLabel
|
||||
}
|
||||
</MenubarShortcut>
|
||||
</MenubarSubTrigger>
|
||||
<MenubarSubContent>
|
||||
<MenubarCheckboxItem
|
||||
@@ -487,13 +506,13 @@ export const Menu: React.FC<MenuProps> = () => {
|
||||
</MenubarMenu>
|
||||
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>{t('menu.share.share')}</MenubarTrigger>
|
||||
<MenubarTrigger>{t('menu.backup.backup')}</MenubarTrigger>
|
||||
<MenubarContent>
|
||||
<MenubarItem onClick={openExportDiagramDialog}>
|
||||
{t('menu.share.export_diagram')}
|
||||
{t('menu.backup.export_diagram')}
|
||||
</MenubarItem>
|
||||
<MenubarItem onClick={openImportDiagramDialog}>
|
||||
{t('menu.share.import_diagram')}
|
||||
{t('menu.backup.restore_diagram')}
|
||||
</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
@@ -501,6 +520,9 @@ export const Menu: React.FC<MenuProps> = () => {
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>{t('menu.help.help')}</MenubarTrigger>
|
||||
<MenubarContent>
|
||||
<MenubarItem onClick={openChartDBDocs}>
|
||||
{t('menu.help.docs_website')}
|
||||
</MenubarItem>
|
||||
<MenubarItem onClick={openChartDBIO}>
|
||||
{t('menu.help.visit_website')}
|
||||
</MenubarItem>
|
||||
|
@@ -7,7 +7,6 @@ import { DiagramName } from './diagram-name';
|
||||
import { LastSaved } from './last-saved';
|
||||
import { LanguageNav } from './language-nav/language-nav';
|
||||
import { Menu } from './menu/menu';
|
||||
import { HIDE_BUCKLE_DOT_DEV } from '@/lib/env';
|
||||
|
||||
export interface TopNavbarProps {}
|
||||
|
||||
@@ -26,27 +25,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
||||
);
|
||||
}, [isDesktop]);
|
||||
|
||||
const openBuckleWaitlist = useCallback(() => {
|
||||
window.open('https://waitlist.buckle.dev', '_blank');
|
||||
}, []);
|
||||
|
||||
const renderGetBuckleButton = useCallback(() => {
|
||||
if (HIDE_BUCKLE_DOT_DEV) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className="gradient-background relative inline-flex items-center justify-center overflow-hidden rounded-lg p-0.5 text-base text-gray-700 focus:outline-none focus:ring-0"
|
||||
onClick={openBuckleWaitlist}
|
||||
>
|
||||
<span className="relative inline-flex items-center justify-center whitespace-nowrap rounded-md bg-background px-2 py-0.5 font-primary text-xs font-semibold text-foreground md:text-sm">
|
||||
ChartDB v2.0 🔥
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}, [openBuckleWaitlist]);
|
||||
|
||||
return (
|
||||
<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-1 md:flex-row md:justify-normal">
|
||||
@@ -68,7 +46,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
||||
</a>
|
||||
{!isDesktop ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{renderGetBuckleButton()}
|
||||
{renderStars()}
|
||||
<LanguageNav />
|
||||
</div>
|
||||
@@ -80,7 +57,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
||||
<>
|
||||
<DiagramName />
|
||||
<div className="hidden flex-1 items-center justify-end gap-2 sm:flex">
|
||||
{renderGetBuckleButton()}
|
||||
<LastSaved />
|
||||
{renderStars()}
|
||||
<LanguageNav />
|
||||
|
92
src/pages/editor-page/use-diagram-loader.tsx
Normal file
92
src/pages/editor-page/use-diagram-loader.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { useConfig } from '@/hooks/use-config';
|
||||
import { useDialog } from '@/hooks/use-dialog';
|
||||
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
|
||||
import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
|
||||
import { useStorage } from '@/hooks/use-storage';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
export const useDiagramLoader = () => {
|
||||
const [initialDiagram, setInitialDiagram] = useState<Diagram | undefined>();
|
||||
const { diagramId } = useParams<{ diagramId: string }>();
|
||||
const { config } = useConfig();
|
||||
const { loadDiagram, currentDiagram } = useChartDB();
|
||||
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
|
||||
const { showLoader, hideLoader } = useFullScreenLoader();
|
||||
const { openCreateDiagramDialog, openOpenDiagramDialog } = useDialog();
|
||||
const navigate = useNavigate();
|
||||
const { listDiagrams } = useStorage();
|
||||
|
||||
const currentDiagramLoadingRef = useRef<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentDiagram?.id === diagramId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadDefaultDiagram = async () => {
|
||||
if (diagramId) {
|
||||
setInitialDiagram(undefined);
|
||||
showLoader();
|
||||
resetRedoStack();
|
||||
resetUndoStack();
|
||||
const diagram = await loadDiagram(diagramId);
|
||||
if (!diagram) {
|
||||
openOpenDiagramDialog({ canClose: false });
|
||||
hideLoader();
|
||||
return;
|
||||
}
|
||||
|
||||
setInitialDiagram(diagram);
|
||||
hideLoader();
|
||||
|
||||
return;
|
||||
} else if (!diagramId && config.defaultDiagramId) {
|
||||
const diagram = await loadDiagram(config.defaultDiagramId);
|
||||
if (diagram) {
|
||||
navigate(`/diagrams/${config.defaultDiagramId}`);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
const diagrams = await listDiagrams();
|
||||
|
||||
if (diagrams.length > 0) {
|
||||
openOpenDiagramDialog({ canClose: false });
|
||||
} else {
|
||||
openCreateDiagramDialog();
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
currentDiagramLoadingRef.current === (diagramId ?? '') &&
|
||||
currentDiagramLoadingRef.current !== undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
currentDiagramLoadingRef.current = diagramId ?? '';
|
||||
|
||||
loadDefaultDiagram();
|
||||
}, [
|
||||
diagramId,
|
||||
openCreateDiagramDialog,
|
||||
config,
|
||||
navigate,
|
||||
listDiagrams,
|
||||
loadDiagram,
|
||||
resetRedoStack,
|
||||
resetUndoStack,
|
||||
hideLoader,
|
||||
showLoader,
|
||||
currentDiagram?.id,
|
||||
openOpenDiagramDialog,
|
||||
]);
|
||||
|
||||
return { initialDiagram };
|
||||
};
|
@@ -289,7 +289,6 @@ const TemplatePageComponent: React.FC = () => {
|
||||
readonly
|
||||
>
|
||||
<Canvas
|
||||
readonly
|
||||
initialTables={
|
||||
template.diagram.tables ?? []
|
||||
}
|
||||
|
Reference in New Issue
Block a user