mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-07 15:33:41 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a52bf02e6 | ||
|
|
08b627cb8c | ||
|
|
73f542adad | ||
|
|
0d11b0c55a | ||
|
|
5b9d2bd1e3 | ||
|
|
cf1e141837 | ||
|
|
3894a22174 | ||
|
|
cad155e655 | ||
|
|
4477b1ca1f | ||
|
|
cd443466c7 | ||
|
|
18012ddab1 | ||
|
|
beb015194f | ||
|
|
c3904d9fdd | ||
|
|
aee5779983 | ||
|
|
765a1c4354 |
23
CHANGELOG.md
23
CHANGELOG.md
@@ -1,5 +1,28 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.13.0](https://github.com/chartdb/chartdb/compare/v1.12.0...v1.13.0) (2025-05-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **custom-types:** add enums and composite types for Postgres ([#714](https://github.com/chartdb/chartdb/issues/714)) ([c3904d9](https://github.com/chartdb/chartdb/commit/c3904d9fdd63ef5b76a44e73582d592f2c418687))
|
||||||
|
* **export-sql:** add custom types to export sql script ([#720](https://github.com/chartdb/chartdb/issues/720)) ([cad155e](https://github.com/chartdb/chartdb/commit/cad155e6550f171b8faecbfdff27032798ecea43))
|
||||||
|
* **oracle:** support oracle in ChartDB ([#709](https://github.com/chartdb/chartdb/issues/709)) ([765a1c4](https://github.com/chartdb/chartdb/commit/765a1c43547a29bd3428c942c7afb56f63aaf046))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **canvas:** prevent canvas blink and lag on field edit ([#723](https://github.com/chartdb/chartdb/issues/723)) ([cd44346](https://github.com/chartdb/chartdb/commit/cd443466c7952f1cdc3739645c12130b9231e3a1))
|
||||||
|
* **canvas:** prevent canvas blink and lag on primary field edit ([#725](https://github.com/chartdb/chartdb/issues/725)) ([4477b1c](https://github.com/chartdb/chartdb/commit/4477b1ca1fe6b282b604739a23e31181acd4d7bc))
|
||||||
|
* **custom_types:** fix custom types on storage provider ([#721](https://github.com/chartdb/chartdb/issues/721)) ([beb0151](https://github.com/chartdb/chartdb/commit/beb015194f917c0ba644458410162d2b7599918c))
|
||||||
|
* **custom_types:** fix custom types on storage provider ([#722](https://github.com/chartdb/chartdb/issues/722)) ([18012dd](https://github.com/chartdb/chartdb/commit/18012ddab1718bcce3432aea626adf6fc9be25d9))
|
||||||
|
* **custom-types:** fetch directly via the smart-query the custom types ([#729](https://github.com/chartdb/chartdb/issues/729)) ([cf1e141](https://github.com/chartdb/chartdb/commit/cf1e141837eda77d717ad87489ce9946b688e226))
|
||||||
|
* **dbml-editor:** export comments with schema if existsed ([#728](https://github.com/chartdb/chartdb/issues/728)) ([73f542a](https://github.com/chartdb/chartdb/commit/73f542adad2d66a1e84fc656a0c34d9b1f39f33c))
|
||||||
|
* **dbml-editor:** fix export dbml - to show enums ([#724](https://github.com/chartdb/chartdb/issues/724)) ([3894a22](https://github.com/chartdb/chartdb/commit/3894a221745d32c13160bedcb1bcf53d89897698))
|
||||||
|
* **import-database:** remove the default fetch from import database ([#718](https://github.com/chartdb/chartdb/issues/718)) ([0d11b0c](https://github.com/chartdb/chartdb/commit/0d11b0c55a94a12a764785cfdcf2ba10437241d6))
|
||||||
|
* **menu:** add oracle to import menu ([#713](https://github.com/chartdb/chartdb/issues/713)) ([aee5779](https://github.com/chartdb/chartdb/commit/aee577998342eb4a2b05b3e03181992a435712d8))
|
||||||
|
* **relationship:** fix creating of relationships ([#732](https://github.com/chartdb/chartdb/issues/732)) ([08b627c](https://github.com/chartdb/chartdb/commit/08b627cb8ca8fdf08d8ed2ff7e89104887deffb7))
|
||||||
|
|
||||||
## [1.12.0](https://github.com/chartdb/chartdb/compare/v1.11.0...v1.12.0) (2025-05-20)
|
## [1.12.0](https://github.com/chartdb/chartdb/compare/v1.11.0...v1.12.0) (2025-05-20)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "chartdb",
|
"name": "chartdb",
|
||||||
"version": "1.12.0",
|
"version": "1.13.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "chartdb",
|
"name": "chartdb",
|
||||||
"version": "1.12.0",
|
"version": "1.13.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^0.0.51",
|
"@ai-sdk/openai": "^0.0.51",
|
||||||
"@dbml/core": "^3.9.5",
|
"@dbml/core": "^3.9.5",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "chartdb",
|
"name": "chartdb",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.12.0",
|
"version": "1.13.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
BIN
src/assets/oracle_logo.png
Normal file
BIN
src/assets/oracle_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
BIN
src/assets/oracle_logo_2.png
Normal file
BIN
src/assets/oracle_logo_2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
BIN
src/assets/oracle_logo_dark.png
Normal file
BIN
src/assets/oracle_logo_dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -28,7 +28,7 @@ export const DiagramIcon = React.forwardRef<
|
|||||||
<TooltipTrigger className={cn('mr-1', className)} ref={ref} asChild>
|
<TooltipTrigger className={cn('mr-1', className)} ref={ref} asChild>
|
||||||
<img
|
<img
|
||||||
src={databaseEditionToImageMap[databaseEdition]}
|
src={databaseEditionToImageMap[databaseEdition]}
|
||||||
className={cn('h-5 max-w-fit rounded-full', imgClassName)}
|
className={cn('max-h-5 max-w-5 rounded-full', imgClassName)}
|
||||||
alt="database"
|
alt="database"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
@@ -42,7 +42,7 @@ export const DiagramIcon = React.forwardRef<
|
|||||||
<TooltipTrigger className={cn('mr-2', className)} ref={ref} asChild>
|
<TooltipTrigger className={cn('mr-2', className)} ref={ref} asChild>
|
||||||
<img
|
<img
|
||||||
src={databaseSecondaryLogoMap[databaseType]}
|
src={databaseSecondaryLogoMap[databaseType]}
|
||||||
className={cn('h-5 max-w-fit', imgClassName)}
|
className={cn('max-h-5 max-w-5', imgClassName)}
|
||||||
alt="database"
|
alt="database"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface SelectBoxOption {
|
|||||||
description?: string;
|
description?: string;
|
||||||
regex?: string;
|
regex?: string;
|
||||||
extractRegex?: RegExp;
|
extractRegex?: RegExp;
|
||||||
|
group?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectBoxProps {
|
export interface SelectBoxProps {
|
||||||
@@ -51,6 +52,7 @@ export interface SelectBoxProps {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
onOpenChange?: (open: boolean) => void;
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
popoverClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||||
@@ -75,6 +77,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
disabled,
|
disabled,
|
||||||
open,
|
open,
|
||||||
onOpenChange: setOpen,
|
onOpenChange: setOpen,
|
||||||
|
popoverClassName,
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
@@ -175,6 +178,101 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
[isOpen, onOpenChange]
|
[isOpen, onOpenChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const groups = React.useMemo(
|
||||||
|
() =>
|
||||||
|
options.reduce(
|
||||||
|
(acc, option) => {
|
||||||
|
if (option.group) {
|
||||||
|
if (!acc[option.group]) {
|
||||||
|
acc[option.group] = [];
|
||||||
|
}
|
||||||
|
acc[option.group].push(option);
|
||||||
|
} else {
|
||||||
|
if (!acc['default']) {
|
||||||
|
acc['default'] = [];
|
||||||
|
}
|
||||||
|
acc['default'].push(option);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, SelectBoxOption[]>
|
||||||
|
),
|
||||||
|
[options]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasGroups = React.useMemo(
|
||||||
|
() =>
|
||||||
|
Object.keys(groups).filter((group) => group !== 'default')
|
||||||
|
.length > 0,
|
||||||
|
[groups]
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderOption = React.useCallback(
|
||||||
|
(option: SelectBoxOption) => {
|
||||||
|
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}
|
||||||
|
onSelect={() =>
|
||||||
|
handleSelect(
|
||||||
|
option.value,
|
||||||
|
matches?.map((match) => match.toString())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{multiple && (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||||
|
isSelected
|
||||||
|
? 'bg-primary text-primary-foreground'
|
||||||
|
: 'opacity-50 [&_svg]:invisible'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CheckIcon />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-1 items-center truncate">
|
||||||
|
<span>
|
||||||
|
{isRegexMatch ? searchTerm : option.label}
|
||||||
|
{!isRegexMatch && optionSuffix
|
||||||
|
? optionSuffix(option)
|
||||||
|
: ''}
|
||||||
|
</span>
|
||||||
|
{option.description && (
|
||||||
|
<span className="ml-1 w-0 flex-1 truncate text-xs text-muted-foreground">
|
||||||
|
{option.description}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{((!multiple && option.value === value) ||
|
||||||
|
isRegexMatch) && (
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
'ml-auto',
|
||||||
|
option.value === value
|
||||||
|
? 'opacity-100'
|
||||||
|
: 'opacity-0'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[value, multiple, searchTerm, handleSelect, optionSuffix]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
|
<Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
|
||||||
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
|
<PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
|
||||||
@@ -245,7 +343,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
</div>
|
</div>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent
|
<PopoverContent
|
||||||
className="w-fit min-w-[var(--radix-popover-trigger-width)] p-0"
|
className={cn(
|
||||||
|
'w-fit min-w-[var(--radix-popover-trigger-width)] p-0',
|
||||||
|
popoverClassName
|
||||||
|
)}
|
||||||
align="center"
|
align="center"
|
||||||
>
|
>
|
||||||
<Command
|
<Command
|
||||||
@@ -319,91 +420,23 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
<div className="max-h-64 w-full">
|
<div className="max-h-64 w-full">
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
<CommandList className="max-h-fit w-full">
|
<CommandList className="max-h-fit w-full">
|
||||||
{options.map((option) => {
|
{hasGroups
|
||||||
const isSelected =
|
? Object.entries(groups).map(
|
||||||
Array.isArray(value) &&
|
([
|
||||||
value.includes(option.value);
|
groupName,
|
||||||
|
groupOptions,
|
||||||
const isRegexMatch =
|
]) => (
|
||||||
option.regex &&
|
<CommandGroup
|
||||||
new RegExp(option.regex)?.test(
|
key={groupName}
|
||||||
searchTerm
|
heading={groupName}
|
||||||
);
|
>
|
||||||
|
{groupOptions.map(
|
||||||
const matches = option.extractRegex
|
renderOption
|
||||||
? searchTerm.match(
|
)}
|
||||||
option.extractRegex
|
</CommandGroup>
|
||||||
)
|
)
|
||||||
: undefined;
|
)
|
||||||
|
: options.map(renderOption)}
|
||||||
return (
|
|
||||||
<CommandItem
|
|
||||||
className="flex items-center"
|
|
||||||
key={option.value}
|
|
||||||
keywords={
|
|
||||||
option.regex
|
|
||||||
? [option.regex]
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onSelect={() =>
|
|
||||||
handleSelect(
|
|
||||||
option.value,
|
|
||||||
matches?.map(
|
|
||||||
(match) =>
|
|
||||||
match.toString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{multiple && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
|
||||||
isSelected
|
|
||||||
? 'bg-primary text-primary-foreground'
|
|
||||||
: 'opacity-50 [&_svg]:invisible'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CheckIcon />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-center truncate">
|
|
||||||
<span>
|
|
||||||
{isRegexMatch
|
|
||||||
? searchTerm
|
|
||||||
: option.label}
|
|
||||||
{!isRegexMatch &&
|
|
||||||
optionSuffix
|
|
||||||
? optionSuffix(
|
|
||||||
option
|
|
||||||
)
|
|
||||||
: ''}
|
|
||||||
</span>
|
|
||||||
{option.description && (
|
|
||||||
<span className="ml-1 text-xs text-muted-foreground">
|
|
||||||
{
|
|
||||||
option.description
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{((!multiple &&
|
|
||||||
option.value ===
|
|
||||||
value) ||
|
|
||||||
isRegexMatch) && (
|
|
||||||
<CheckIcon
|
|
||||||
className={cn(
|
|
||||||
'ml-auto',
|
|
||||||
option.value ===
|
|
||||||
value
|
|
||||||
? 'opacity-100'
|
|
||||||
: 'opacity-0'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</CommandItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</CommandList>
|
</CommandList>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type { DBSchema } from '@/lib/domain/db-schema';
|
|||||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||||
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
|
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
|
||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
|
|
||||||
export type ChartDBEventType =
|
export type ChartDBEventType =
|
||||||
| 'add_tables'
|
| 'add_tables'
|
||||||
@@ -72,6 +73,7 @@ export interface ChartDBContext {
|
|||||||
relationships: DBRelationship[];
|
relationships: DBRelationship[];
|
||||||
dependencies: DBDependency[];
|
dependencies: DBDependency[];
|
||||||
areas: Area[];
|
areas: Area[];
|
||||||
|
customTypes: DBCustomType[];
|
||||||
currentDiagram: Diagram;
|
currentDiagram: Diagram;
|
||||||
events: EventEmitter<ChartDBEvent>;
|
events: EventEmitter<ChartDBEvent>;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
@@ -248,6 +250,33 @@ export interface ChartDBContext {
|
|||||||
area: Partial<Area>,
|
area: Partial<Area>,
|
||||||
options?: { updateHistory: boolean }
|
options?: { updateHistory: boolean }
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
|
|
||||||
|
// Custom type operations
|
||||||
|
createCustomType: (
|
||||||
|
attributes?: Partial<Omit<DBCustomType, 'id'>>
|
||||||
|
) => Promise<DBCustomType>;
|
||||||
|
addCustomType: (
|
||||||
|
customType: DBCustomType,
|
||||||
|
options?: { updateHistory: boolean }
|
||||||
|
) => Promise<void>;
|
||||||
|
addCustomTypes: (
|
||||||
|
customTypes: DBCustomType[],
|
||||||
|
options?: { updateHistory: boolean }
|
||||||
|
) => Promise<void>;
|
||||||
|
getCustomType: (id: string) => DBCustomType | null;
|
||||||
|
removeCustomType: (
|
||||||
|
id: string,
|
||||||
|
options?: { updateHistory: boolean }
|
||||||
|
) => Promise<void>;
|
||||||
|
removeCustomTypes: (
|
||||||
|
ids: string[],
|
||||||
|
options?: { updateHistory: boolean }
|
||||||
|
) => Promise<void>;
|
||||||
|
updateCustomType: (
|
||||||
|
id: string,
|
||||||
|
customType: Partial<DBCustomType>,
|
||||||
|
options?: { updateHistory: boolean }
|
||||||
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const chartDBContext = createContext<ChartDBContext>({
|
export const chartDBContext = createContext<ChartDBContext>({
|
||||||
@@ -258,6 +287,7 @@ export const chartDBContext = createContext<ChartDBContext>({
|
|||||||
relationships: [],
|
relationships: [],
|
||||||
dependencies: [],
|
dependencies: [],
|
||||||
areas: [],
|
areas: [],
|
||||||
|
customTypes: [],
|
||||||
schemas: [],
|
schemas: [],
|
||||||
filteredSchemas: [],
|
filteredSchemas: [],
|
||||||
filterSchemas: emptyFn,
|
filterSchemas: emptyFn,
|
||||||
@@ -333,4 +363,13 @@ export const chartDBContext = createContext<ChartDBContext>({
|
|||||||
removeArea: emptyFn,
|
removeArea: emptyFn,
|
||||||
removeAreas: emptyFn,
|
removeAreas: emptyFn,
|
||||||
updateArea: emptyFn,
|
updateArea: emptyFn,
|
||||||
|
|
||||||
|
// Custom type operations
|
||||||
|
createCustomType: emptyFn,
|
||||||
|
addCustomType: emptyFn,
|
||||||
|
addCustomTypes: emptyFn,
|
||||||
|
getCustomType: emptyFn,
|
||||||
|
removeCustomType: emptyFn,
|
||||||
|
removeCustomTypes: emptyFn,
|
||||||
|
updateCustomType: emptyFn,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ import type { Area } from '@/lib/domain/area';
|
|||||||
import { storageInitialValue } from '../storage-context/storage-context';
|
import { storageInitialValue } from '../storage-context/storage-context';
|
||||||
import { useDiff } from '../diff-context/use-diff';
|
import { useDiff } from '../diff-context/use-diff';
|
||||||
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
|
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
|
||||||
|
import {
|
||||||
|
DBCustomTypeKind,
|
||||||
|
type DBCustomType,
|
||||||
|
} from '@/lib/domain/db-custom-type';
|
||||||
|
|
||||||
export interface ChartDBProviderProps {
|
export interface ChartDBProviderProps {
|
||||||
diagram?: Diagram;
|
diagram?: Diagram;
|
||||||
@@ -58,6 +62,9 @@ export const ChartDBProvider: React.FC<
|
|||||||
diagram?.dependencies ?? []
|
diagram?.dependencies ?? []
|
||||||
);
|
);
|
||||||
const [areas, setAreas] = useState<Area[]>(diagram?.areas ?? []);
|
const [areas, setAreas] = useState<Area[]>(diagram?.areas ?? []);
|
||||||
|
const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
|
||||||
|
diagram?.customTypes ?? []
|
||||||
|
);
|
||||||
const { events: diffEvents } = useDiff();
|
const { events: diffEvents } = useDiff();
|
||||||
|
|
||||||
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
|
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
|
||||||
@@ -155,6 +162,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
relationships,
|
relationships,
|
||||||
dependencies,
|
dependencies,
|
||||||
areas,
|
areas,
|
||||||
|
customTypes,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
diagramId,
|
diagramId,
|
||||||
@@ -165,6 +173,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
relationships,
|
relationships,
|
||||||
dependencies,
|
dependencies,
|
||||||
areas,
|
areas,
|
||||||
|
customTypes,
|
||||||
diagramCreatedAt,
|
diagramCreatedAt,
|
||||||
diagramUpdatedAt,
|
diagramUpdatedAt,
|
||||||
]
|
]
|
||||||
@@ -177,6 +186,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
setRelationships([]);
|
setRelationships([]);
|
||||||
setDependencies([]);
|
setDependencies([]);
|
||||||
setAreas([]);
|
setAreas([]);
|
||||||
|
setCustomTypes([]);
|
||||||
setDiagramUpdatedAt(updatedAt);
|
setDiagramUpdatedAt(updatedAt);
|
||||||
|
|
||||||
resetRedoStack();
|
resetRedoStack();
|
||||||
@@ -188,6 +198,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
db.deleteDiagramRelationships(diagramId),
|
db.deleteDiagramRelationships(diagramId),
|
||||||
db.deleteDiagramDependencies(diagramId),
|
db.deleteDiagramDependencies(diagramId),
|
||||||
db.deleteDiagramAreas(diagramId),
|
db.deleteDiagramAreas(diagramId),
|
||||||
|
db.deleteDiagramCustomTypes(diagramId),
|
||||||
]);
|
]);
|
||||||
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
||||||
|
|
||||||
@@ -201,6 +212,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
setRelationships([]);
|
setRelationships([]);
|
||||||
setDependencies([]);
|
setDependencies([]);
|
||||||
setAreas([]);
|
setAreas([]);
|
||||||
|
setCustomTypes([]);
|
||||||
resetRedoStack();
|
resetRedoStack();
|
||||||
resetUndoStack();
|
resetUndoStack();
|
||||||
|
|
||||||
@@ -210,6 +222,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
db.deleteDiagram(diagramId),
|
db.deleteDiagram(diagramId),
|
||||||
db.deleteDiagramDependencies(diagramId),
|
db.deleteDiagramDependencies(diagramId),
|
||||||
db.deleteDiagramAreas(diagramId),
|
db.deleteDiagramAreas(diagramId),
|
||||||
|
db.deleteDiagramCustomTypes(diagramId),
|
||||||
]);
|
]);
|
||||||
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
}, [db, diagramId, resetRedoStack, resetUndoStack]);
|
||||||
|
|
||||||
@@ -1506,6 +1519,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
setRelationships(diagram?.relationships ?? []);
|
setRelationships(diagram?.relationships ?? []);
|
||||||
setDependencies(diagram?.dependencies ?? []);
|
setDependencies(diagram?.dependencies ?? []);
|
||||||
setAreas(diagram?.areas ?? []);
|
setAreas(diagram?.areas ?? []);
|
||||||
|
setCustomTypes(diagram?.customTypes ?? []);
|
||||||
setDiagramCreatedAt(diagram.createdAt);
|
setDiagramCreatedAt(diagram.createdAt);
|
||||||
setDiagramUpdatedAt(diagram.updatedAt);
|
setDiagramUpdatedAt(diagram.updatedAt);
|
||||||
|
|
||||||
@@ -1520,6 +1534,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
setRelationships,
|
setRelationships,
|
||||||
setDependencies,
|
setDependencies,
|
||||||
setAreas,
|
setAreas,
|
||||||
|
setCustomTypes,
|
||||||
setDiagramCreatedAt,
|
setDiagramCreatedAt,
|
||||||
setDiagramUpdatedAt,
|
setDiagramUpdatedAt,
|
||||||
events,
|
events,
|
||||||
@@ -1533,6 +1548,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
includeTables: true,
|
includeTables: true,
|
||||||
includeDependencies: true,
|
includeDependencies: true,
|
||||||
includeAreas: true,
|
includeAreas: true,
|
||||||
|
includeCustomTypes: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (diagram) {
|
if (diagram) {
|
||||||
@@ -1544,6 +1560,150 @@ export const ChartDBProvider: React.FC<
|
|||||||
[db, loadDiagramFromData]
|
[db, loadDiagramFromData]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Custom type operations
|
||||||
|
const getCustomType: ChartDBContext['getCustomType'] = useCallback(
|
||||||
|
(id: string) => customTypes.find((type) => type.id === id) ?? null,
|
||||||
|
[customTypes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addCustomTypes: ChartDBContext['addCustomTypes'] = useCallback(
|
||||||
|
async (
|
||||||
|
customTypes: DBCustomType[],
|
||||||
|
options = { updateHistory: true }
|
||||||
|
) => {
|
||||||
|
setCustomTypes((currentTypes) => [...currentTypes, ...customTypes]);
|
||||||
|
const updatedAt = new Date();
|
||||||
|
setDiagramUpdatedAt(updatedAt);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||||
|
...customTypes.map((customType) =>
|
||||||
|
db.addCustomType({ diagramId, customType })
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (options.updateHistory) {
|
||||||
|
addUndoAction({
|
||||||
|
action: 'addCustomTypes',
|
||||||
|
redoData: { customTypes },
|
||||||
|
undoData: { customTypeIds: customTypes.map((t) => t.id) },
|
||||||
|
});
|
||||||
|
resetRedoStack();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[db, diagramId, setCustomTypes, addUndoAction, resetRedoStack]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addCustomType: ChartDBContext['addCustomType'] = useCallback(
|
||||||
|
async (customType: DBCustomType, options = { updateHistory: true }) => {
|
||||||
|
return addCustomTypes([customType], options);
|
||||||
|
},
|
||||||
|
[addCustomTypes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const createCustomType: ChartDBContext['createCustomType'] = useCallback(
|
||||||
|
async (attributes) => {
|
||||||
|
const customType: DBCustomType = {
|
||||||
|
id: generateId(),
|
||||||
|
name: `type_${customTypes.length + 1}`,
|
||||||
|
kind: DBCustomTypeKind.enum,
|
||||||
|
values: [],
|
||||||
|
fields: [],
|
||||||
|
...attributes,
|
||||||
|
};
|
||||||
|
|
||||||
|
await addCustomType(customType);
|
||||||
|
return customType;
|
||||||
|
},
|
||||||
|
[addCustomType, customTypes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeCustomTypes: ChartDBContext['removeCustomTypes'] = useCallback(
|
||||||
|
async (ids, options = { updateHistory: true }) => {
|
||||||
|
const typesToRemove = ids
|
||||||
|
.map((id) => getCustomType(id))
|
||||||
|
.filter(Boolean) as DBCustomType[];
|
||||||
|
|
||||||
|
setCustomTypes((types) =>
|
||||||
|
types.filter((type) => !ids.includes(type.id))
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedAt = new Date();
|
||||||
|
setDiagramUpdatedAt(updatedAt);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||||
|
...ids.map((id) => db.deleteCustomType({ diagramId, id })),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (typesToRemove.length > 0 && options.updateHistory) {
|
||||||
|
addUndoAction({
|
||||||
|
action: 'removeCustomTypes',
|
||||||
|
redoData: {
|
||||||
|
customTypeIds: ids,
|
||||||
|
},
|
||||||
|
undoData: {
|
||||||
|
customTypes: typesToRemove,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
resetRedoStack();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
db,
|
||||||
|
diagramId,
|
||||||
|
setCustomTypes,
|
||||||
|
addUndoAction,
|
||||||
|
resetRedoStack,
|
||||||
|
getCustomType,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeCustomType: ChartDBContext['removeCustomType'] = useCallback(
|
||||||
|
async (id: string, options = { updateHistory: true }) => {
|
||||||
|
return removeCustomTypes([id], options);
|
||||||
|
},
|
||||||
|
[removeCustomTypes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateCustomType: ChartDBContext['updateCustomType'] = useCallback(
|
||||||
|
async (
|
||||||
|
id: string,
|
||||||
|
customType: Partial<DBCustomType>,
|
||||||
|
options = { updateHistory: true }
|
||||||
|
) => {
|
||||||
|
const prevCustomType = getCustomType(id);
|
||||||
|
setCustomTypes((types) =>
|
||||||
|
types.map((t) => (t.id === id ? { ...t, ...customType } : t))
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedAt = new Date();
|
||||||
|
setDiagramUpdatedAt(updatedAt);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
db.updateDiagram({ id: diagramId, attributes: { updatedAt } }),
|
||||||
|
db.updateCustomType({ id, attributes: customType }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!!prevCustomType && options.updateHistory) {
|
||||||
|
addUndoAction({
|
||||||
|
action: 'updateCustomType',
|
||||||
|
redoData: { customTypeId: id, customType },
|
||||||
|
undoData: { customTypeId: id, customType: prevCustomType },
|
||||||
|
});
|
||||||
|
resetRedoStack();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
db,
|
||||||
|
setCustomTypes,
|
||||||
|
addUndoAction,
|
||||||
|
resetRedoStack,
|
||||||
|
getCustomType,
|
||||||
|
diagramId,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<chartDBContext.Provider
|
<chartDBContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -1608,6 +1768,14 @@ export const ChartDBProvider: React.FC<
|
|||||||
removeArea,
|
removeArea,
|
||||||
removeAreas,
|
removeAreas,
|
||||||
updateArea,
|
updateArea,
|
||||||
|
customTypes,
|
||||||
|
createCustomType,
|
||||||
|
addCustomType,
|
||||||
|
addCustomTypes,
|
||||||
|
getCustomType,
|
||||||
|
removeCustomType,
|
||||||
|
removeCustomTypes,
|
||||||
|
updateCustomType,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
addAreas,
|
addAreas,
|
||||||
removeAreas,
|
removeAreas,
|
||||||
updateArea,
|
updateArea,
|
||||||
|
addCustomTypes,
|
||||||
|
removeCustomTypes,
|
||||||
|
updateCustomType,
|
||||||
} = useChartDB();
|
} = useChartDB();
|
||||||
|
|
||||||
const redoActionHandlers = useMemo(
|
const redoActionHandlers = useMemo(
|
||||||
@@ -119,6 +122,19 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
updateArea: ({ redoData: { areaId, area } }) => {
|
updateArea: ({ redoData: { areaId, area } }) => {
|
||||||
return updateArea(areaId, area, { updateHistory: false });
|
return updateArea(areaId, area, { updateHistory: false });
|
||||||
},
|
},
|
||||||
|
addCustomTypes: ({ redoData: { customTypes } }) => {
|
||||||
|
return addCustomTypes(customTypes, { updateHistory: false });
|
||||||
|
},
|
||||||
|
removeCustomTypes: ({ redoData: { customTypeIds } }) => {
|
||||||
|
return removeCustomTypes(customTypeIds, {
|
||||||
|
updateHistory: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
updateCustomType: ({ redoData: { customTypeId, customType } }) => {
|
||||||
|
return updateCustomType(customTypeId, customType, {
|
||||||
|
updateHistory: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
addTables,
|
addTables,
|
||||||
@@ -141,6 +157,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
addAreas,
|
addAreas,
|
||||||
removeAreas,
|
removeAreas,
|
||||||
updateArea,
|
updateArea,
|
||||||
|
addCustomTypes,
|
||||||
|
removeCustomTypes,
|
||||||
|
updateCustomType,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -239,6 +258,19 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
updateArea: ({ undoData: { areaId, area } }) => {
|
updateArea: ({ undoData: { areaId, area } }) => {
|
||||||
return updateArea(areaId, area, { updateHistory: false });
|
return updateArea(areaId, area, { updateHistory: false });
|
||||||
},
|
},
|
||||||
|
addCustomTypes: ({ undoData: { customTypeIds } }) => {
|
||||||
|
return removeCustomTypes(customTypeIds, {
|
||||||
|
updateHistory: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
removeCustomTypes: ({ undoData: { customTypes } }) => {
|
||||||
|
return addCustomTypes(customTypes, { updateHistory: false });
|
||||||
|
},
|
||||||
|
updateCustomType: ({ undoData: { customTypeId, customType } }) => {
|
||||||
|
return updateCustomType(customTypeId, customType, {
|
||||||
|
updateHistory: false,
|
||||||
|
});
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
addTables,
|
addTables,
|
||||||
@@ -261,6 +293,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
addAreas,
|
addAreas,
|
||||||
removeAreas,
|
removeAreas,
|
||||||
updateArea,
|
updateArea,
|
||||||
|
addCustomTypes,
|
||||||
|
removeCustomTypes,
|
||||||
|
updateCustomType,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { DBIndex } from '@/lib/domain/db-index';
|
|||||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
|
|
||||||
type Action = keyof ChartDBContext;
|
type Action = keyof ChartDBContext;
|
||||||
|
|
||||||
@@ -142,6 +143,24 @@ type RedoUndoActionRemoveAreas = RedoUndoActionBase<
|
|||||||
{ areas: Area[] }
|
{ areas: Area[] }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
type RedoUndoActionAddCustomTypes = RedoUndoActionBase<
|
||||||
|
'addCustomTypes',
|
||||||
|
{ customTypes: DBCustomType[] },
|
||||||
|
{ customTypeIds: string[] }
|
||||||
|
>;
|
||||||
|
|
||||||
|
type RedoUndoActionUpdateCustomType = RedoUndoActionBase<
|
||||||
|
'updateCustomType',
|
||||||
|
{ customTypeId: string; customType: Partial<DBCustomType> },
|
||||||
|
{ customTypeId: string; customType: Partial<DBCustomType> }
|
||||||
|
>;
|
||||||
|
|
||||||
|
type RedoUndoActionRemoveCustomTypes = RedoUndoActionBase<
|
||||||
|
'removeCustomTypes',
|
||||||
|
{ customTypeIds: string[] },
|
||||||
|
{ customTypes: DBCustomType[] }
|
||||||
|
>;
|
||||||
|
|
||||||
export type RedoUndoAction =
|
export type RedoUndoAction =
|
||||||
| RedoUndoActionAddTables
|
| RedoUndoActionAddTables
|
||||||
| RedoUndoActionRemoveTables
|
| RedoUndoActionRemoveTables
|
||||||
@@ -162,7 +181,10 @@ export type RedoUndoAction =
|
|||||||
| RedoUndoActionRemoveDependencies
|
| RedoUndoActionRemoveDependencies
|
||||||
| RedoUndoActionAddAreas
|
| RedoUndoActionAddAreas
|
||||||
| RedoUndoActionUpdateArea
|
| RedoUndoActionUpdateArea
|
||||||
| RedoUndoActionRemoveAreas;
|
| RedoUndoActionRemoveAreas
|
||||||
|
| RedoUndoActionAddCustomTypes
|
||||||
|
| RedoUndoActionUpdateCustomType
|
||||||
|
| RedoUndoActionRemoveCustomTypes;
|
||||||
|
|
||||||
export type RedoActionData<T extends Action> = Extract<
|
export type RedoActionData<T extends Action> = Extract<
|
||||||
RedoUndoAction,
|
RedoUndoAction,
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ export type SidebarSection =
|
|||||||
| 'tables'
|
| 'tables'
|
||||||
| 'relationships'
|
| 'relationships'
|
||||||
| 'dependencies'
|
| 'dependencies'
|
||||||
| 'areas';
|
| 'areas'
|
||||||
|
| 'customTypes';
|
||||||
|
|
||||||
export interface LayoutContext {
|
export interface LayoutContext {
|
||||||
openedTableInSidebar: string | undefined;
|
openedTableInSidebar: string | undefined;
|
||||||
@@ -24,6 +25,10 @@ export interface LayoutContext {
|
|||||||
openAreaFromSidebar: (areaId: string) => void;
|
openAreaFromSidebar: (areaId: string) => void;
|
||||||
closeAllAreasInSidebar: () => void;
|
closeAllAreasInSidebar: () => void;
|
||||||
|
|
||||||
|
openedCustomTypeInSidebar: string | undefined;
|
||||||
|
openCustomTypeFromSidebar: (customTypeId: string) => void;
|
||||||
|
closeAllCustomTypesInSidebar: () => void;
|
||||||
|
|
||||||
selectedSidebarSection: SidebarSection;
|
selectedSidebarSection: SidebarSection;
|
||||||
selectSidebarSection: (section: SidebarSection) => void;
|
selectSidebarSection: (section: SidebarSection) => void;
|
||||||
|
|
||||||
@@ -53,6 +58,10 @@ export const layoutContext = createContext<LayoutContext>({
|
|||||||
openAreaFromSidebar: emptyFn,
|
openAreaFromSidebar: emptyFn,
|
||||||
closeAllAreasInSidebar: emptyFn,
|
closeAllAreasInSidebar: emptyFn,
|
||||||
|
|
||||||
|
openedCustomTypeInSidebar: undefined,
|
||||||
|
openCustomTypeFromSidebar: emptyFn,
|
||||||
|
closeAllCustomTypesInSidebar: emptyFn,
|
||||||
|
|
||||||
selectSidebarSection: emptyFn,
|
selectSidebarSection: emptyFn,
|
||||||
openTableFromSidebar: emptyFn,
|
openTableFromSidebar: emptyFn,
|
||||||
closeAllTablesInSidebar: emptyFn,
|
closeAllTablesInSidebar: emptyFn,
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
|
const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
>();
|
>();
|
||||||
|
const [openedCustomTypeInSidebar, setOpenedCustomTypeInSidebar] =
|
||||||
|
React.useState<string | undefined>();
|
||||||
const [selectedSidebarSection, setSelectedSidebarSection] =
|
const [selectedSidebarSection, setSelectedSidebarSection] =
|
||||||
React.useState<SidebarSection>('tables');
|
React.useState<SidebarSection>('tables');
|
||||||
const [isSidePanelShowed, setIsSidePanelShowed] =
|
const [isSidePanelShowed, setIsSidePanelShowed] =
|
||||||
@@ -36,6 +38,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
|
const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
|
||||||
() => setOpenedAreaInSidebar('');
|
() => setOpenedAreaInSidebar('');
|
||||||
|
|
||||||
|
const closeAllCustomTypesInSidebar: LayoutContext['closeAllCustomTypesInSidebar'] =
|
||||||
|
() => setOpenedCustomTypeInSidebar('');
|
||||||
|
|
||||||
const hideSidePanel: LayoutContext['hideSidePanel'] = () =>
|
const hideSidePanel: LayoutContext['hideSidePanel'] = () =>
|
||||||
setIsSidePanelShowed(false);
|
setIsSidePanelShowed(false);
|
||||||
|
|
||||||
@@ -76,6 +81,13 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setOpenedAreaInSidebar(areaId);
|
setOpenedAreaInSidebar(areaId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openCustomTypeFromSidebar: LayoutContext['openCustomTypeFromSidebar'] =
|
||||||
|
(customTypeId) => {
|
||||||
|
showSidePanel();
|
||||||
|
setSelectedSidebarSection('customTypes');
|
||||||
|
setOpenedTableInSidebar(customTypeId);
|
||||||
|
};
|
||||||
|
|
||||||
const openSelectSchema: LayoutContext['openSelectSchema'] = () =>
|
const openSelectSchema: LayoutContext['openSelectSchema'] = () =>
|
||||||
setIsSelectSchemaOpen(true);
|
setIsSelectSchemaOpen(true);
|
||||||
|
|
||||||
@@ -105,6 +117,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
openedAreaInSidebar,
|
openedAreaInSidebar,
|
||||||
openAreaFromSidebar,
|
openAreaFromSidebar,
|
||||||
closeAllAreasInSidebar,
|
closeAllAreasInSidebar,
|
||||||
|
openedCustomTypeInSidebar,
|
||||||
|
openCustomTypeFromSidebar,
|
||||||
|
closeAllCustomTypesInSidebar,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { DBTable } from '@/lib/domain/db-table';
|
|||||||
import type { ChartDBConfig } from '@/lib/domain/config';
|
import type { ChartDBConfig } from '@/lib/domain/config';
|
||||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
|
|
||||||
export interface StorageContext {
|
export interface StorageContext {
|
||||||
// Config operations
|
// Config operations
|
||||||
@@ -19,6 +20,7 @@ export interface StorageContext {
|
|||||||
includeRelationships?: boolean;
|
includeRelationships?: boolean;
|
||||||
includeDependencies?: boolean;
|
includeDependencies?: boolean;
|
||||||
includeAreas?: boolean;
|
includeAreas?: boolean;
|
||||||
|
includeCustomTypes?: boolean;
|
||||||
}) => Promise<Diagram[]>;
|
}) => Promise<Diagram[]>;
|
||||||
getDiagram: (
|
getDiagram: (
|
||||||
id: string,
|
id: string,
|
||||||
@@ -27,6 +29,7 @@ export interface StorageContext {
|
|||||||
includeRelationships?: boolean;
|
includeRelationships?: boolean;
|
||||||
includeDependencies?: boolean;
|
includeDependencies?: boolean;
|
||||||
includeAreas?: boolean;
|
includeAreas?: boolean;
|
||||||
|
includeCustomTypes?: boolean;
|
||||||
}
|
}
|
||||||
) => Promise<Diagram | undefined>;
|
) => Promise<Diagram | undefined>;
|
||||||
updateDiagram: (params: {
|
updateDiagram: (params: {
|
||||||
@@ -103,6 +106,26 @@ export interface StorageContext {
|
|||||||
deleteArea: (params: { diagramId: string; id: string }) => Promise<void>;
|
deleteArea: (params: { diagramId: string; id: string }) => Promise<void>;
|
||||||
listAreas: (diagramId: string) => Promise<Area[]>;
|
listAreas: (diagramId: string) => Promise<Area[]>;
|
||||||
deleteDiagramAreas: (diagramId: string) => Promise<void>;
|
deleteDiagramAreas: (diagramId: string) => Promise<void>;
|
||||||
|
|
||||||
|
// Custom type operations
|
||||||
|
addCustomType: (params: {
|
||||||
|
diagramId: string;
|
||||||
|
customType: DBCustomType;
|
||||||
|
}) => Promise<void>;
|
||||||
|
getCustomType: (params: {
|
||||||
|
diagramId: string;
|
||||||
|
id: string;
|
||||||
|
}) => Promise<DBCustomType | undefined>;
|
||||||
|
updateCustomType: (params: {
|
||||||
|
id: string;
|
||||||
|
attributes: Partial<DBCustomType>;
|
||||||
|
}) => Promise<void>;
|
||||||
|
deleteCustomType: (params: {
|
||||||
|
diagramId: string;
|
||||||
|
id: string;
|
||||||
|
}) => Promise<void>;
|
||||||
|
listCustomTypes: (diagramId: string) => Promise<DBCustomType[]>;
|
||||||
|
deleteDiagramCustomTypes: (diagramId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const storageInitialValue: StorageContext = {
|
export const storageInitialValue: StorageContext = {
|
||||||
@@ -143,6 +166,14 @@ export const storageInitialValue: StorageContext = {
|
|||||||
deleteArea: emptyFn,
|
deleteArea: emptyFn,
|
||||||
listAreas: emptyFn,
|
listAreas: emptyFn,
|
||||||
deleteDiagramAreas: emptyFn,
|
deleteDiagramAreas: emptyFn,
|
||||||
|
|
||||||
|
// Custom type operations
|
||||||
|
addCustomType: emptyFn,
|
||||||
|
getCustomType: emptyFn,
|
||||||
|
updateCustomType: emptyFn,
|
||||||
|
deleteCustomType: emptyFn,
|
||||||
|
listCustomTypes: emptyFn,
|
||||||
|
deleteDiagramCustomTypes: emptyFn,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const storageContext =
|
export const storageContext =
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { determineCardinalities } from '@/lib/domain/db-relationship';
|
|||||||
import type { ChartDBConfig } from '@/lib/domain/config';
|
import type { ChartDBConfig } from '@/lib/domain/config';
|
||||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||||
import type { Area } from '@/lib/domain/area';
|
import type { Area } from '@/lib/domain/area';
|
||||||
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
|
|
||||||
export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -34,6 +35,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
Area & { diagramId: string },
|
Area & { diagramId: string },
|
||||||
'id' // primary key "id" (for the typings only)
|
'id' // primary key "id" (for the typings only)
|
||||||
>;
|
>;
|
||||||
|
db_custom_types: EntityTable<
|
||||||
|
DBCustomType & { diagramId: string },
|
||||||
|
'id' // primary key "id" (for the typings only)
|
||||||
|
>;
|
||||||
config: EntityTable<
|
config: EntityTable<
|
||||||
ChartDBConfig & { id: number },
|
ChartDBConfig & { id: number },
|
||||||
'id' // primary key "id" (for the typings only)
|
'id' // primary key "id" (for the typings only)
|
||||||
@@ -166,6 +171,20 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
config: '++id, defaultDiagramId',
|
config: '++id, defaultDiagramId',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
db.version(11).stores({
|
||||||
|
diagrams:
|
||||||
|
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
||||||
|
db_tables:
|
||||||
|
'++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment, isView, isMaterializedView, order',
|
||||||
|
db_relationships:
|
||||||
|
'++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
|
||||||
|
db_dependencies:
|
||||||
|
'++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt',
|
||||||
|
areas: '++id, diagramId, name, x, y, width, height, color',
|
||||||
|
db_custom_types: '++id, diagramId, schema, type, kind, values, fields',
|
||||||
|
config: '++id, defaultDiagramId',
|
||||||
|
});
|
||||||
|
|
||||||
db.on('ready', async () => {
|
db.on('ready', async () => {
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
|
|
||||||
@@ -232,6 +251,13 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
...areas.map((area) => addArea({ diagramId: diagram.id, area }))
|
...areas.map((area) => addArea({ diagramId: diagram.id, area }))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const customTypes = diagram.customTypes ?? [];
|
||||||
|
promises.push(
|
||||||
|
...customTypes.map((customType) =>
|
||||||
|
addCustomType({ diagramId: diagram.id, customType })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -241,11 +267,13 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
includeRelationships?: boolean;
|
includeRelationships?: boolean;
|
||||||
includeDependencies?: boolean;
|
includeDependencies?: boolean;
|
||||||
includeAreas?: boolean;
|
includeAreas?: boolean;
|
||||||
|
includeCustomTypes?: boolean;
|
||||||
} = {
|
} = {
|
||||||
includeRelationships: false,
|
includeRelationships: false,
|
||||||
includeTables: false,
|
includeTables: false,
|
||||||
includeDependencies: false,
|
includeDependencies: false,
|
||||||
includeAreas: false,
|
includeAreas: false,
|
||||||
|
includeCustomTypes: false,
|
||||||
}
|
}
|
||||||
): Promise<Diagram[]> => {
|
): Promise<Diagram[]> => {
|
||||||
let diagrams = await db.diagrams.toArray();
|
let diagrams = await db.diagrams.toArray();
|
||||||
@@ -286,6 +314,15 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.includeCustomTypes) {
|
||||||
|
diagrams = await Promise.all(
|
||||||
|
diagrams.map(async (diagram) => {
|
||||||
|
diagram.customTypes = await listCustomTypes(diagram.id);
|
||||||
|
return diagram;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return diagrams;
|
return diagrams;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -296,11 +333,13 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
includeRelationships?: boolean;
|
includeRelationships?: boolean;
|
||||||
includeDependencies?: boolean;
|
includeDependencies?: boolean;
|
||||||
includeAreas?: boolean;
|
includeAreas?: boolean;
|
||||||
|
includeCustomTypes?: boolean;
|
||||||
} = {
|
} = {
|
||||||
includeRelationships: false,
|
includeRelationships: false,
|
||||||
includeTables: false,
|
includeTables: false,
|
||||||
includeDependencies: false,
|
includeDependencies: false,
|
||||||
includeAreas: false,
|
includeAreas: false,
|
||||||
|
includeCustomTypes: false,
|
||||||
}
|
}
|
||||||
): Promise<Diagram | undefined> => {
|
): Promise<Diagram | undefined> => {
|
||||||
const diagram = await db.diagrams.get(id);
|
const diagram = await db.diagrams.get(id);
|
||||||
@@ -325,6 +364,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
diagram.areas = await listAreas(id);
|
diagram.areas = await listAreas(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.includeCustomTypes) {
|
||||||
|
diagram.customTypes = await listCustomTypes(id);
|
||||||
|
}
|
||||||
|
|
||||||
return diagram;
|
return diagram;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -351,6 +394,13 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
.where('diagramId')
|
.where('diagramId')
|
||||||
.equals(id)
|
.equals(id)
|
||||||
.modify({ diagramId: attributes.id }),
|
.modify({ diagramId: attributes.id }),
|
||||||
|
db.areas.where('diagramId').equals(id).modify({
|
||||||
|
diagramId: attributes.id,
|
||||||
|
}),
|
||||||
|
db.db_custom_types
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(id)
|
||||||
|
.modify({ diagramId: attributes.id }),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -364,6 +414,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
db.db_relationships.where('diagramId').equals(id).delete(),
|
db.db_relationships.where('diagramId').equals(id).delete(),
|
||||||
db.db_dependencies.where('diagramId').equals(id).delete(),
|
db.db_dependencies.where('diagramId').equals(id).delete(),
|
||||||
db.areas.where('diagramId').equals(id).delete(),
|
db.areas.where('diagramId').equals(id).delete(),
|
||||||
|
db.db_custom_types.where('diagramId').equals(id).delete(),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -580,6 +631,71 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
await db.areas.where('diagramId').equals(diagramId).delete();
|
await db.areas.where('diagramId').equals(diagramId).delete();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Custom type operations
|
||||||
|
const addCustomType: StorageContext['addCustomType'] = async ({
|
||||||
|
diagramId,
|
||||||
|
customType,
|
||||||
|
}: {
|
||||||
|
diagramId: string;
|
||||||
|
customType: DBCustomType;
|
||||||
|
}) => {
|
||||||
|
await db.db_custom_types.add({
|
||||||
|
...customType,
|
||||||
|
diagramId,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCustomType: StorageContext['getCustomType'] = async ({
|
||||||
|
diagramId,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
diagramId: string;
|
||||||
|
id: string;
|
||||||
|
}): Promise<DBCustomType | undefined> => {
|
||||||
|
return await db.db_custom_types.get({ id, diagramId });
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateCustomType: StorageContext['updateCustomType'] = async ({
|
||||||
|
id,
|
||||||
|
attributes,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
attributes: Partial<DBCustomType>;
|
||||||
|
}) => {
|
||||||
|
await db.db_custom_types.update(id, attributes);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteCustomType: StorageContext['deleteCustomType'] = async ({
|
||||||
|
diagramId,
|
||||||
|
id,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
diagramId: string;
|
||||||
|
}) => {
|
||||||
|
await db.db_custom_types.where({ id, diagramId }).delete();
|
||||||
|
};
|
||||||
|
|
||||||
|
const listCustomTypes: StorageContext['listCustomTypes'] = async (
|
||||||
|
diagramId: string
|
||||||
|
): Promise<DBCustomType[]> => {
|
||||||
|
return (
|
||||||
|
await db.db_custom_types
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(diagramId)
|
||||||
|
.toArray()
|
||||||
|
).sort((a, b) => {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteDiagramCustomTypes: StorageContext['deleteDiagramCustomTypes'] =
|
||||||
|
async (diagramId: string) => {
|
||||||
|
await db.db_custom_types
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(diagramId)
|
||||||
|
.delete();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<storageContext.Provider
|
<storageContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -615,6 +731,12 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
deleteArea,
|
deleteArea,
|
||||||
listAreas,
|
listAreas,
|
||||||
deleteDiagramAreas,
|
deleteDiagramAreas,
|
||||||
|
addCustomType,
|
||||||
|
getCustomType,
|
||||||
|
updateCustomType,
|
||||||
|
deleteCustomType,
|
||||||
|
listCustomTypes,
|
||||||
|
deleteDiagramCustomTypes,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -366,7 +366,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
{isDesktop ? (
|
{isDesktop ? (
|
||||||
<ResizablePanelGroup
|
<ResizablePanelGroup
|
||||||
direction={isDesktop ? 'horizontal' : 'vertical'}
|
direction={isDesktop ? 'horizontal' : 'vertical'}
|
||||||
className="min-h-[500px] md:min-h-fit"
|
className="min-h-[500px]"
|
||||||
>
|
>
|
||||||
<ResizablePanel
|
<ResizablePanel
|
||||||
defaultSize={25}
|
defaultSize={25}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { DDLInstructions } from './instructions/ddl-instructions';
|
|||||||
|
|
||||||
const DatabasesWithoutDDLInstructions: DatabaseType[] = [
|
const DatabasesWithoutDDLInstructions: DatabaseType[] = [
|
||||||
DatabaseType.CLICKHOUSE,
|
DatabaseType.CLICKHOUSE,
|
||||||
|
DatabaseType.ORACLE,
|
||||||
];
|
];
|
||||||
|
|
||||||
export interface InstructionsSectionProps {
|
export interface InstructionsSectionProps {
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ const DDLInstructionsMap: Record<DatabaseType, DDLInstruction[]> = {
|
|||||||
text: 'Open the exported SQL file, copy its contents, and paste them here.',
|
text: 'Open the exported SQL file, copy its contents, and paste them here.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[DatabaseType.ORACLE]: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface DDLInstructionsProps {
|
export interface DDLInstructionsProps {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const SUPPORTED_DB_TYPES: DatabaseType[] = [
|
|||||||
DatabaseType.MARIADB,
|
DatabaseType.MARIADB,
|
||||||
DatabaseType.SQLITE,
|
DatabaseType.SQLITE,
|
||||||
DatabaseType.SQL_SERVER,
|
DatabaseType.SQL_SERVER,
|
||||||
|
DatabaseType.ORACLE,
|
||||||
DatabaseType.COCKROACHDB,
|
DatabaseType.COCKROACHDB,
|
||||||
DatabaseType.CLICKHOUSE,
|
DatabaseType.CLICKHOUSE,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -231,6 +231,33 @@ export const ar: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -232,6 +232,32 @@ export const bn: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -234,6 +234,32 @@ export const de: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -226,6 +226,32 @@ export const en = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -222,6 +222,32 @@ export const es: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -220,6 +220,32 @@ export const fr: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -233,6 +233,32 @@ export const gu: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -233,6 +233,32 @@ export const hi: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -231,6 +231,32 @@ export const id_ID: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -237,6 +237,32 @@ export const ja: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -231,6 +231,32 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -236,6 +236,32 @@ export const mr: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -233,6 +233,32 @@ export const ne: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -232,6 +232,32 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -229,6 +229,32 @@ export const ru: LanguageTranslation = {
|
|||||||
description: 'Создайте область, чтобы начать',
|
description: 'Создайте область, чтобы начать',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -233,6 +233,32 @@ export const te: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -232,6 +232,32 @@ export const tr: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
toolbar: {
|
toolbar: {
|
||||||
zoom_in: 'Yakınlaştır',
|
zoom_in: 'Yakınlaştır',
|
||||||
|
|||||||
@@ -230,6 +230,32 @@ export const uk: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -231,6 +231,32 @@ export const vi: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -228,6 +228,32 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -228,6 +228,32 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
description: 'Create an area to get started',
|
description: 'Create an area to get started',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: Translate
|
||||||
|
custom_types_section: {
|
||||||
|
custom_types: 'Custom Types',
|
||||||
|
filter: 'Filter',
|
||||||
|
clear: 'Clear Filter',
|
||||||
|
no_results: 'No custom types found matching your filter.',
|
||||||
|
empty_state: {
|
||||||
|
title: 'No custom types',
|
||||||
|
description:
|
||||||
|
'Custom types will appear here when they are available in your database',
|
||||||
|
},
|
||||||
|
custom_type: {
|
||||||
|
kind: 'Kind',
|
||||||
|
enum_values: 'Enum Values',
|
||||||
|
composite_fields: 'Fields',
|
||||||
|
no_fields: 'No fields defined',
|
||||||
|
field_name_placeholder: 'Field name',
|
||||||
|
field_type_placeholder: 'Select type',
|
||||||
|
add_field: 'Add Field',
|
||||||
|
custom_type_actions: {
|
||||||
|
title: 'Actions',
|
||||||
|
delete_custom_type: 'Delete',
|
||||||
|
},
|
||||||
|
delete_custom_type: 'Delete Type',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
toolbar: {
|
toolbar: {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { mysqlDataTypes } from './mysql-data-types';
|
|||||||
import { postgresDataTypes } from './postgres-data-types';
|
import { postgresDataTypes } from './postgres-data-types';
|
||||||
import { sqlServerDataTypes } from './sql-server-data-types';
|
import { sqlServerDataTypes } from './sql-server-data-types';
|
||||||
import { sqliteDataTypes } from './sqlite-data-types';
|
import { sqliteDataTypes } from './sqlite-data-types';
|
||||||
|
import { oracleDataTypes } from './oracle-data-types';
|
||||||
|
|
||||||
export interface DataType {
|
export interface DataType {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -32,6 +33,7 @@ export const dataTypeMap: Record<DatabaseType, readonly DataTypeData[]> = {
|
|||||||
[DatabaseType.SQLITE]: sqliteDataTypes,
|
[DatabaseType.SQLITE]: sqliteDataTypes,
|
||||||
[DatabaseType.CLICKHOUSE]: clickhouseDataTypes,
|
[DatabaseType.CLICKHOUSE]: clickhouseDataTypes,
|
||||||
[DatabaseType.COCKROACHDB]: postgresDataTypes,
|
[DatabaseType.COCKROACHDB]: postgresDataTypes,
|
||||||
|
[DatabaseType.ORACLE]: oracleDataTypes,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const sortDataTypes = (dataTypes: DataTypeData[]): DataTypeData[] => {
|
export const sortDataTypes = (dataTypes: DataTypeData[]): DataTypeData[] => {
|
||||||
@@ -71,6 +73,9 @@ export const sortedDataTypeMap: Record<DatabaseType, readonly DataTypeData[]> =
|
|||||||
[DatabaseType.COCKROACHDB]: sortDataTypes([
|
[DatabaseType.COCKROACHDB]: sortDataTypes([
|
||||||
...dataTypeMap[DatabaseType.COCKROACHDB],
|
...dataTypeMap[DatabaseType.COCKROACHDB],
|
||||||
]),
|
]),
|
||||||
|
[DatabaseType.ORACLE]: sortDataTypes([
|
||||||
|
...dataTypeMap[DatabaseType.ORACLE],
|
||||||
|
]),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const compatibleTypes: Record<DatabaseType, Record<string, string[]>> = {
|
const compatibleTypes: Record<DatabaseType, Record<string, string[]>> = {
|
||||||
@@ -88,6 +93,7 @@ const compatibleTypes: Record<DatabaseType, Record<string, string[]>> = {
|
|||||||
[DatabaseType.SQLITE]: {},
|
[DatabaseType.SQLITE]: {},
|
||||||
[DatabaseType.CLICKHOUSE]: {},
|
[DatabaseType.CLICKHOUSE]: {},
|
||||||
[DatabaseType.COCKROACHDB]: {},
|
[DatabaseType.COCKROACHDB]: {},
|
||||||
|
[DatabaseType.ORACLE]: {},
|
||||||
[DatabaseType.GENERIC]: {},
|
[DatabaseType.GENERIC]: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
57
src/lib/data/data-types/oracle-data-types.ts
Normal file
57
src/lib/data/data-types/oracle-data-types.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
|
export const oracleDataTypes: readonly DataTypeData[] = [
|
||||||
|
// Character types
|
||||||
|
{ name: 'VARCHAR2', id: 'varchar2', usageLevel: 1, hasCharMaxLength: true },
|
||||||
|
{
|
||||||
|
name: 'NVARCHAR2',
|
||||||
|
id: 'nvarchar2',
|
||||||
|
usageLevel: 1,
|
||||||
|
hasCharMaxLength: true,
|
||||||
|
},
|
||||||
|
{ name: 'CHAR', id: 'char', usageLevel: 2, hasCharMaxLength: true },
|
||||||
|
{ name: 'NCHAR', id: 'nchar', usageLevel: 2, hasCharMaxLength: true },
|
||||||
|
{ name: 'CLOB', id: 'clob', usageLevel: 2 },
|
||||||
|
{ name: 'NCLOB', id: 'nclob', usageLevel: 2 },
|
||||||
|
|
||||||
|
// Numeric types
|
||||||
|
{ name: 'NUMBER', id: 'number', usageLevel: 1 },
|
||||||
|
{ name: 'FLOAT', id: 'float', usageLevel: 2 },
|
||||||
|
{ name: 'BINARY_FLOAT', id: 'binary_float', usageLevel: 2 },
|
||||||
|
{ name: 'BINARY_DOUBLE', id: 'binary_double', usageLevel: 2 },
|
||||||
|
|
||||||
|
// Date/Time types
|
||||||
|
{ name: 'DATE', id: 'date', usageLevel: 1 },
|
||||||
|
{ name: 'TIMESTAMP', id: 'timestamp', usageLevel: 1 },
|
||||||
|
{
|
||||||
|
name: 'TIMESTAMP WITH TIME ZONE',
|
||||||
|
id: 'timestamp_with_time_zone',
|
||||||
|
usageLevel: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'TIMESTAMP WITH LOCAL TIME ZONE',
|
||||||
|
id: 'timestamp_with_local_time_zone',
|
||||||
|
usageLevel: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'INTERVAL YEAR TO MONTH',
|
||||||
|
id: 'interval_year_to_month',
|
||||||
|
usageLevel: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'INTERVAL DAY TO SECOND',
|
||||||
|
id: 'interval_day_to_second',
|
||||||
|
usageLevel: 2,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Large Object types
|
||||||
|
{ name: 'BLOB', id: 'blob', usageLevel: 2 },
|
||||||
|
{ name: 'BFILE', id: 'bfile', usageLevel: 2 },
|
||||||
|
|
||||||
|
// Other types
|
||||||
|
{ name: 'RAW', id: 'raw', usageLevel: 2, hasCharMaxLength: true },
|
||||||
|
{ name: 'LONG RAW', id: 'long_raw', usageLevel: 2 },
|
||||||
|
{ name: 'ROWID', id: 'rowid', usageLevel: 2 },
|
||||||
|
{ name: 'UROWID', id: 'urowid', usageLevel: 2 },
|
||||||
|
{ name: 'XMLType', id: 'xmltype', usageLevel: 2 },
|
||||||
|
] as const;
|
||||||
@@ -8,6 +8,8 @@ import type { Diagram } from '@/lib/domain/diagram';
|
|||||||
import type { DBTable } from '@/lib/domain/db-table';
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
import type { DBField } from '@/lib/domain/db-field';
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
|
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
|
||||||
|
|
||||||
function parsePostgresDefault(field: DBField): string {
|
function parsePostgresDefault(field: DBField): string {
|
||||||
if (!field.default || typeof field.default !== 'string') {
|
if (!field.default || typeof field.default !== 'string') {
|
||||||
@@ -90,6 +92,57 @@ function mapPostgresType(typeName: string, fieldName: string): string {
|
|||||||
return typeName;
|
return typeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function exportCustomTypes(customTypes: DBCustomType[]): string {
|
||||||
|
if (!customTypes || customTypes.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let typesSql = '';
|
||||||
|
|
||||||
|
// Sort custom types to ensure enums are created before composite types that might use them
|
||||||
|
const sortedTypes = [...customTypes].sort((a, b) => {
|
||||||
|
if (
|
||||||
|
a.kind === DBCustomTypeKind.enum &&
|
||||||
|
b.kind === DBCustomTypeKind.composite
|
||||||
|
) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
a.kind === DBCustomTypeKind.composite &&
|
||||||
|
b.kind === DBCustomTypeKind.enum
|
||||||
|
) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
sortedTypes.forEach((customType) => {
|
||||||
|
const typeName = customType.schema
|
||||||
|
? `"${customType.schema}"."${customType.name}"`
|
||||||
|
: `"${customType.name}"`;
|
||||||
|
|
||||||
|
if (customType.kind === DBCustomTypeKind.enum) {
|
||||||
|
// Export enum type
|
||||||
|
if (customType.values && customType.values.length > 0) {
|
||||||
|
const enumValues = customType.values
|
||||||
|
.map((value) => `'${value.replace(/'/g, "''")}'`)
|
||||||
|
.join(', ');
|
||||||
|
typesSql += `CREATE TYPE ${typeName} AS ENUM (${enumValues});\n`;
|
||||||
|
}
|
||||||
|
} else if (customType.kind === DBCustomTypeKind.composite) {
|
||||||
|
// Export composite type
|
||||||
|
if (customType.fields && customType.fields.length > 0) {
|
||||||
|
const compositeFields = customType.fields
|
||||||
|
.map((field) => `"${field.field}" ${field.type}`)
|
||||||
|
.join(', ');
|
||||||
|
typesSql += `CREATE TYPE ${typeName} AS (${compositeFields});\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return typesSql + '\n';
|
||||||
|
}
|
||||||
|
|
||||||
export function exportPostgreSQL(diagram: Diagram): string {
|
export function exportPostgreSQL(diagram: Diagram): string {
|
||||||
if (!diagram.tables || !diagram.relationships) {
|
if (!diagram.tables || !diagram.relationships) {
|
||||||
return '';
|
return '';
|
||||||
@@ -97,6 +150,7 @@ export function exportPostgreSQL(diagram: Diagram): string {
|
|||||||
|
|
||||||
const tables = diagram.tables;
|
const tables = diagram.tables;
|
||||||
const relationships = diagram.relationships;
|
const relationships = diagram.relationships;
|
||||||
|
const customTypes = diagram.customTypes || [];
|
||||||
|
|
||||||
// Create CREATE SCHEMA statements for all schemas
|
// Create CREATE SCHEMA statements for all schemas
|
||||||
let sqlScript = '';
|
let sqlScript = '';
|
||||||
@@ -108,12 +162,22 @@ export function exportPostgreSQL(diagram: Diagram): string {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Also collect schemas from custom types
|
||||||
|
customTypes.forEach((customType) => {
|
||||||
|
if (customType.schema) {
|
||||||
|
schemas.add(customType.schema);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Add schema creation statements
|
// Add schema creation statements
|
||||||
schemas.forEach((schema) => {
|
schemas.forEach((schema) => {
|
||||||
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
|
sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
|
||||||
});
|
});
|
||||||
sqlScript += '\n';
|
sqlScript += '\n';
|
||||||
|
|
||||||
|
// Add custom types (enums and composite types)
|
||||||
|
sqlScript += exportCustomTypes(customTypes);
|
||||||
|
|
||||||
// Add sequence creation statements
|
// Add sequence creation statements
|
||||||
const sequences = new Set<string>();
|
const sequences = new Set<string>();
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,55 @@ export const exportBaseSQL = ({
|
|||||||
schemas.forEach((schema) => {
|
schemas.forEach((schema) => {
|
||||||
sqlScript += `CREATE SCHEMA IF NOT EXISTS ${schema};\n`;
|
sqlScript += `CREATE SCHEMA IF NOT EXISTS ${schema};\n`;
|
||||||
});
|
});
|
||||||
sqlScript += '\n';
|
if (schemas.size > 0) sqlScript += '\n'; // Add newline only if schemas were added
|
||||||
|
|
||||||
|
// Add CREATE TYPE statements for ENUMs and COMPOSITE types from diagram.customTypes
|
||||||
|
if (diagram.customTypes && diagram.customTypes.length > 0) {
|
||||||
|
diagram.customTypes.forEach((customType) => {
|
||||||
|
const typeNameWithSchema = customType.schema
|
||||||
|
? `${customType.schema}.${customType.name}`
|
||||||
|
: customType.name;
|
||||||
|
|
||||||
|
if (
|
||||||
|
customType.kind === 'enum' &&
|
||||||
|
customType.values &&
|
||||||
|
customType.values.length > 0
|
||||||
|
) {
|
||||||
|
// For PostgreSQL, generate CREATE TYPE ... AS ENUM
|
||||||
|
// For other DBs, this might need adjustment or be omitted if not supported directly
|
||||||
|
// or if we rely on the DBML generator to create Enums separately (as currently done)
|
||||||
|
// For now, let's assume PostgreSQL-style for demonstration if isDBMLFlow is false.
|
||||||
|
// If isDBMLFlow is true, we let TableDBML.tsx handle Enum syntax directly.
|
||||||
|
if (
|
||||||
|
targetDatabaseType === DatabaseType.POSTGRESQL &&
|
||||||
|
!isDBMLFlow
|
||||||
|
) {
|
||||||
|
const enumValues = customType.values
|
||||||
|
.map((v) => `'${v.replace(/'/g, "''")}'`)
|
||||||
|
.join(', ');
|
||||||
|
sqlScript += `CREATE TYPE ${typeNameWithSchema} AS ENUM (${enumValues});\n`;
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
customType.kind === 'composite' &&
|
||||||
|
customType.fields &&
|
||||||
|
customType.fields.length > 0
|
||||||
|
) {
|
||||||
|
// For PostgreSQL, generate CREATE TYPE ... AS (...)
|
||||||
|
// This is crucial for composite types to be recognized by the DBML importer
|
||||||
|
if (
|
||||||
|
targetDatabaseType === DatabaseType.POSTGRESQL ||
|
||||||
|
isDBMLFlow
|
||||||
|
) {
|
||||||
|
// Assume other DBs might not support this or DBML flow needs it
|
||||||
|
const compositeFields = customType.fields
|
||||||
|
.map((f) => `${f.field} ${simplifyDataType(f.type)}`)
|
||||||
|
.join(',\n ');
|
||||||
|
sqlScript += `CREATE TYPE ${typeNameWithSchema} AS (\n ${compositeFields}\n);\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sqlScript += '\n'; // Add a newline if custom types were processed
|
||||||
|
}
|
||||||
|
|
||||||
// Add CREATE SEQUENCE statements
|
// Add CREATE SEQUENCE statements
|
||||||
const sequences = new Set<string>();
|
const sequences = new Set<string>();
|
||||||
@@ -119,8 +167,45 @@ export const exportBaseSQL = ({
|
|||||||
let typeName = simplifyDataType(field.type.name);
|
let typeName = simplifyDataType(field.type.name);
|
||||||
|
|
||||||
// Handle ENUM type
|
// Handle ENUM type
|
||||||
if (typeName.toLowerCase() === 'enum') {
|
// If we are generating SQL for DBML flow, and we ALREADY generated CREATE TYPE for enums (e.g., for PG),
|
||||||
// Map enum to TEXT for broader compatibility, especially with DBML importer
|
// then we should use the enum type name. Otherwise, map to text.
|
||||||
|
// However, the current TableDBML.tsx generates its own Enum blocks, so for DBML flow,
|
||||||
|
// converting to TEXT here might still be the safest bet to avoid conflicts if SQL enums aren't perfectly parsed.
|
||||||
|
// Let's adjust: if it's a known custom enum type, use its name for PG, otherwise TEXT.
|
||||||
|
const customEnumType = diagram.customTypes?.find(
|
||||||
|
(ct) =>
|
||||||
|
ct.name === field.type.name &&
|
||||||
|
ct.kind === 'enum' &&
|
||||||
|
(ct.schema ? ct.schema === table.schema : true)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
customEnumType &&
|
||||||
|
targetDatabaseType === DatabaseType.POSTGRESQL &&
|
||||||
|
!isDBMLFlow
|
||||||
|
) {
|
||||||
|
typeName = customEnumType.schema
|
||||||
|
? `${customEnumType.schema}.${customEnumType.name}`
|
||||||
|
: customEnumType.name;
|
||||||
|
} else if (typeName.toLowerCase() === 'enum') {
|
||||||
|
// Fallback for non-PG or if custom type not found, or for DBML flow if not handled by CREATE TYPE above
|
||||||
|
typeName = 'text';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the field type is a known composite custom type
|
||||||
|
const customCompositeType = diagram.customTypes?.find(
|
||||||
|
(ct) =>
|
||||||
|
ct.name === field.type.name &&
|
||||||
|
ct.kind === 'composite' &&
|
||||||
|
(ct.schema ? ct.schema === table.schema : true)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (customCompositeType) {
|
||||||
|
typeName = customCompositeType.schema
|
||||||
|
? `${customCompositeType.schema}.${customCompositeType.name}`
|
||||||
|
: customCompositeType.name;
|
||||||
|
} else if (typeName.toLowerCase() === 'user-defined') {
|
||||||
|
// If it's 'user-defined' but not a known composite, fallback to TEXT
|
||||||
typeName = 'text';
|
typeName = 'text';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,11 +214,6 @@ export const exportBaseSQL = ({
|
|||||||
typeName = 'text[]';
|
typeName = 'text[]';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temp fix for 'user-defined' to be text
|
|
||||||
if (typeName.toLowerCase() === 'user-defined') {
|
|
||||||
typeName = 'text';
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlScript += ` ${field.name} ${typeName}`;
|
sqlScript += ` ${field.name} ${typeName}`;
|
||||||
|
|
||||||
// Add size for character types
|
// Add size for character types
|
||||||
@@ -522,6 +602,7 @@ const generateSQLPrompt = (databaseType: DatabaseType, sqlScript: string) => {
|
|||||||
- **Serial and Identity Columns**: For auto-increment columns, use \`SERIAL\` or \`GENERATED BY DEFAULT AS IDENTITY\`.
|
- **Serial and Identity Columns**: For auto-increment columns, use \`SERIAL\` or \`GENERATED BY DEFAULT AS IDENTITY\`.
|
||||||
- **Conditional Statements**: Utilize PostgreSQL's support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
|
- **Conditional Statements**: Utilize PostgreSQL's support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
|
||||||
`,
|
`,
|
||||||
|
oracle: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const dialectInstruction = dialectInstructionMap[databaseType] ?? '';
|
const dialectInstruction = dialectInstructionMap[databaseType] ?? '';
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export interface DBCustomTypeFieldInfo {
|
||||||
|
field: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DBCustomTypeFieldInfoSchema = z.object({
|
||||||
|
field: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface DBCustomTypeInfo {
|
||||||
|
schema: string;
|
||||||
|
type: string;
|
||||||
|
kind: 'enum' | 'composite';
|
||||||
|
values?: string[];
|
||||||
|
fields?: DBCustomTypeFieldInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DBCustomTypeInfoSchema: z.ZodType<DBCustomTypeInfo> = z.object({
|
||||||
|
schema: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
kind: z.enum(['enum', 'composite']),
|
||||||
|
values: z.array(z.string()).optional(),
|
||||||
|
fields: z.array(DBCustomTypeFieldInfoSchema).optional(),
|
||||||
|
});
|
||||||
@@ -5,6 +5,10 @@ import { ColumnInfoSchema, type ColumnInfo } from './column-info';
|
|||||||
import { IndexInfoSchema, type IndexInfo } from './index-info';
|
import { IndexInfoSchema, type IndexInfo } from './index-info';
|
||||||
import { TableInfoSchema, type TableInfo } from './table-info';
|
import { TableInfoSchema, type TableInfo } from './table-info';
|
||||||
import { ViewInfoSchema, type ViewInfo } from './view-info';
|
import { ViewInfoSchema, type ViewInfo } from './view-info';
|
||||||
|
import {
|
||||||
|
DBCustomTypeInfoSchema,
|
||||||
|
type DBCustomTypeInfo,
|
||||||
|
} from './custom-type-info';
|
||||||
|
|
||||||
export interface DatabaseMetadata {
|
export interface DatabaseMetadata {
|
||||||
fk_info: ForeignKeyInfo[];
|
fk_info: ForeignKeyInfo[];
|
||||||
@@ -13,6 +17,7 @@ export interface DatabaseMetadata {
|
|||||||
indexes: IndexInfo[];
|
indexes: IndexInfo[];
|
||||||
tables: TableInfo[];
|
tables: TableInfo[];
|
||||||
views: ViewInfo[];
|
views: ViewInfo[];
|
||||||
|
custom_types?: DBCustomTypeInfo[];
|
||||||
database_name: string;
|
database_name: string;
|
||||||
version: string;
|
version: string;
|
||||||
}
|
}
|
||||||
@@ -24,6 +29,7 @@ export const DatabaseMetadataSchema: z.ZodType<DatabaseMetadata> = z.object({
|
|||||||
indexes: z.array(IndexInfoSchema),
|
indexes: z.array(IndexInfoSchema),
|
||||||
tables: z.array(TableInfoSchema),
|
tables: z.array(TableInfoSchema),
|
||||||
views: z.array(ViewInfoSchema),
|
views: z.array(ViewInfoSchema),
|
||||||
|
custom_types: z.array(DBCustomTypeInfoSchema).optional(),
|
||||||
database_name: z.string(),
|
database_name: z.string(),
|
||||||
version: z.string(),
|
version: z.string(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ cols AS (
|
|||||||
',"ordinal_position":', toString(col_tuple.4),
|
',"ordinal_position":', toString(col_tuple.4),
|
||||||
',"type":"', col_tuple.5, '"',
|
',"type":"', col_tuple.5, '"',
|
||||||
',"nullable":"', if(col_tuple.6 = 'NULLABLE', 'true', 'false'), '"',
|
',"nullable":"', if(col_tuple.6 = 'NULLABLE', 'true', 'false'), '"',
|
||||||
',"default":"', if(col_tuple.7 = '', 'null', col_tuple.7), '"',
|
',"default":"', col_tuple.7, '"',
|
||||||
',"comment":', if(col_tuple.8 = '', '""', toString(toJSONString(col_tuple.8))), '}'),
|
',"comment":', if(col_tuple.8 = '', '""', toString(toJSONString(col_tuple.8))), '}'),
|
||||||
groupArray((
|
groupArray((
|
||||||
col.database,
|
col.database,
|
||||||
@@ -15,7 +15,7 @@ cols AS (
|
|||||||
col.name,
|
col.name,
|
||||||
col.position,
|
col.position,
|
||||||
col.type,
|
col.type,
|
||||||
col.default_kind,
|
null as default_kind,
|
||||||
col.default_expression,
|
col.default_expression,
|
||||||
col.comment
|
col.comment
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ cols AS (
|
|||||||
ELSE 'null'
|
ELSE 'null'
|
||||||
END,
|
END,
|
||||||
',"nullable":', CASE WHEN (cols.IS_NULLABLE = 'YES') THEN true ELSE false END::TEXT,
|
',"nullable":', CASE WHEN (cols.IS_NULLABLE = 'YES') THEN true ELSE false END::TEXT,
|
||||||
',"default":"', COALESCE(replace(replace(cols.column_default::TEXT, '"', '\\"'), '\\x', '\\\\x'), ''),
|
',"default":"', null,
|
||||||
'","collation":"', COALESCE(cols.COLLATION_NAME::TEXT, ''),
|
'","collation":"', COALESCE(cols.COLLATION_NAME::TEXT, ''),
|
||||||
'","comment":"', COALESCE(replace(replace(dsc.description::TEXT, '"', '\\"'), '\\x', '\\\\x'), ''),
|
'","comment":"', COALESCE(replace(replace(dsc.description::TEXT, '"', '\\"'), '\\x', '\\\\x'), ''),
|
||||||
'"}')), ',') AS cols_metadata
|
'"}')), ',') AS cols_metadata
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
const withExtras = false;
|
||||||
|
const withDefault = `IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', 'ֿֿֿ\\"'), '')`;
|
||||||
|
const withoutDefault = `""`;
|
||||||
|
|
||||||
export const mariaDBQuery = `WITH fk_info as (
|
export const mariaDBQuery = `WITH fk_info as (
|
||||||
(SELECT (@fk_info:=NULL),
|
(SELECT (@fk_info:=NULL),
|
||||||
(SELECT (0)
|
(SELECT (0)
|
||||||
@@ -76,7 +80,7 @@ export const mariaDBQuery = `WITH fk_info as (
|
|||||||
END,
|
END,
|
||||||
',"ordinal_position":', cols.ordinal_position,
|
',"ordinal_position":', cols.ordinal_position,
|
||||||
',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
|
',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
|
||||||
',"default":"', IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', '\\"'), ''),
|
',"default":"', ${withExtras ? withDefault : withoutDefault},
|
||||||
'","collation":"', IFNULL(cols.collation_name, ''), '"}'
|
'","collation":"', IFNULL(cols.collation_name, ''), '"}'
|
||||||
)))))
|
)))))
|
||||||
), indexes as (
|
), indexes as (
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ export const getMySQLQuery = (
|
|||||||
const databaseEdition: DatabaseEdition | undefined =
|
const databaseEdition: DatabaseEdition | undefined =
|
||||||
options.databaseEdition;
|
options.databaseEdition;
|
||||||
|
|
||||||
|
const withExtras = false;
|
||||||
|
|
||||||
|
const withDefault = `IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', 'ֿֿֿ\\"'), '')`;
|
||||||
|
const withoutDefault = `""`;
|
||||||
|
|
||||||
const newMySQLQuery = `WITH fk_info as (
|
const newMySQLQuery = `WITH fk_info as (
|
||||||
(SELECT (@fk_info:=NULL),
|
(SELECT (@fk_info:=NULL),
|
||||||
(SELECT (0)
|
(SELECT (0)
|
||||||
@@ -86,7 +91,7 @@ export const getMySQLQuery = (
|
|||||||
END,
|
END,
|
||||||
',"ordinal_position":', cols.ordinal_position,
|
',"ordinal_position":', cols.ordinal_position,
|
||||||
',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
|
',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
|
||||||
',"default":"', IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', 'ֿֿֿ\\"'), ''),
|
',"default":"', ${withExtras ? withDefault : withoutDefault},
|
||||||
'","collation":"', IFNULL(cols.collation_name, ''), '"}'
|
'","collation":"', IFNULL(cols.collation_name, ''), '"}'
|
||||||
)))))
|
)))))
|
||||||
), indexes as (
|
), indexes as (
|
||||||
@@ -211,7 +216,7 @@ export const getMySQLQuery = (
|
|||||||
',"scale":', IFNULL(cols.numeric_scale, 'null'), '}'), 'null'),
|
',"scale":', IFNULL(cols.numeric_scale, 'null'), '}'), 'null'),
|
||||||
',"ordinal_position":', cols.ordinal_position,
|
',"ordinal_position":', cols.ordinal_position,
|
||||||
',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
|
',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
|
||||||
',"default":"', IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', '\\"'), ''),
|
',"default":"', ${withExtras ? withDefault : withoutDefault},
|
||||||
'","collation":"', IFNULL(cols.collation_name, ''), '"}')
|
'","collation":"', IFNULL(cols.collation_name, ''), '"}')
|
||||||
) FROM (
|
) FROM (
|
||||||
SELECT cols.table_schema,
|
SELECT cols.table_schema,
|
||||||
|
|||||||
160
src/lib/data/import-metadata/scripts/oracle-script.ts
Normal file
160
src/lib/data/import-metadata/scripts/oracle-script.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
export const oracleDBQuery = `----------------------------------------------------------------------------
|
||||||
|
-- 1. FOREIGN-KEY METADATA
|
||||||
|
----------------------------------------------------------------------------
|
||||||
|
WITH fk_info AS (
|
||||||
|
SELECT JSON_OBJECT(
|
||||||
|
KEY 'schema' VALUE a.owner,
|
||||||
|
KEY 'table' VALUE a.table_name,
|
||||||
|
KEY 'column' VALUE b.column_name,
|
||||||
|
KEY 'foreign_key_name' VALUE a.constraint_name,
|
||||||
|
KEY 'reference_schema' VALUE c.owner,
|
||||||
|
KEY 'reference_table' VALUE c.table_name,
|
||||||
|
KEY 'reference_column' VALUE d.column_name,
|
||||||
|
KEY 'fk_def' VALUE
|
||||||
|
'FOREIGN KEY ('||b.column_name||') REFERENCES '||
|
||||||
|
c.table_name||'('||d.column_name||') ON DELETE '||
|
||||||
|
DECODE(a.delete_rule,
|
||||||
|
'CASCADE' , 'CASCADE' ,
|
||||||
|
'SET NULL', 'SET NULL',
|
||||||
|
'RESTRICT', 'RESTRICT',
|
||||||
|
'NO ACTION')
|
||||||
|
RETURNING CLOB
|
||||||
|
) AS json_data
|
||||||
|
FROM all_constraints a
|
||||||
|
JOIN all_cons_columns b
|
||||||
|
ON b.owner = a.owner
|
||||||
|
AND b.constraint_name = a.constraint_name
|
||||||
|
JOIN all_constraints c
|
||||||
|
ON c.owner = a.r_owner
|
||||||
|
AND c.constraint_name = a.r_constraint_name
|
||||||
|
JOIN all_cons_columns d
|
||||||
|
ON d.owner = c.owner
|
||||||
|
AND d.constraint_name = c.constraint_name
|
||||||
|
AND d.position = b.position
|
||||||
|
WHERE a.constraint_type = 'R'
|
||||||
|
AND a.owner = SYS_CONTEXT('USERENV','CURRENT_SCHEMA')
|
||||||
|
),
|
||||||
|
|
||||||
|
/* ==============================================================
|
||||||
|
2. PRIMARY-KEY METADATA
|
||||||
|
==============================================================*/
|
||||||
|
pk_info AS (
|
||||||
|
SELECT JSON_OBJECT(
|
||||||
|
KEY 'schema' VALUE a.owner,
|
||||||
|
KEY 'table' VALUE a.table_name,
|
||||||
|
KEY 'column' VALUE LISTAGG(b.column_name, ', ')
|
||||||
|
WITHIN GROUP (ORDER BY b.position),
|
||||||
|
KEY 'pk_def' VALUE 'PRIMARY KEY ('||
|
||||||
|
LISTAGG(b.column_name, ', ')
|
||||||
|
WITHIN GROUP (ORDER BY b.position)||')'
|
||||||
|
RETURNING CLOB
|
||||||
|
) AS json_data
|
||||||
|
FROM all_constraints a
|
||||||
|
JOIN all_cons_columns b
|
||||||
|
ON b.owner = a.owner
|
||||||
|
AND b.constraint_name = a.constraint_name
|
||||||
|
WHERE a.constraint_type = 'P'
|
||||||
|
AND a.owner = SYS_CONTEXT('USERENV','CURRENT_SCHEMA')
|
||||||
|
GROUP BY a.owner, a.table_name
|
||||||
|
),
|
||||||
|
|
||||||
|
/* ==============================================================
|
||||||
|
3. COLUMN METADATA
|
||||||
|
==============================================================*/
|
||||||
|
cols AS (
|
||||||
|
SELECT JSON_OBJECT(
|
||||||
|
KEY 'schema' VALUE owner,
|
||||||
|
KEY 'table' VALUE table_name,
|
||||||
|
KEY 'name' VALUE column_name,
|
||||||
|
KEY 'type' VALUE LOWER(data_type),
|
||||||
|
KEY 'character_maximum_length' VALUE CASE
|
||||||
|
WHEN data_type LIKE '%CHAR%'
|
||||||
|
THEN TO_CHAR(char_length)
|
||||||
|
END,
|
||||||
|
KEY 'precision' VALUE CASE
|
||||||
|
WHEN data_type IN ('NUMBER','FLOAT','DECIMAL')
|
||||||
|
THEN JSON_OBJECT(
|
||||||
|
KEY 'precision' VALUE data_precision,
|
||||||
|
KEY 'scale' VALUE data_scale)
|
||||||
|
END,
|
||||||
|
KEY 'ordinal_position' VALUE column_id,
|
||||||
|
KEY 'nullable' VALUE CASE nullable
|
||||||
|
WHEN 'Y' THEN 'true' ELSE 'false' END FORMAT JSON,
|
||||||
|
KEY 'default' VALUE '""' FORMAT JSON,
|
||||||
|
KEY 'collation' VALUE '""' FORMAT JSON
|
||||||
|
RETURNING CLOB
|
||||||
|
) AS json_data
|
||||||
|
FROM all_tab_columns
|
||||||
|
WHERE owner = SYS_CONTEXT('USERENV','CURRENT_SCHEMA')
|
||||||
|
),
|
||||||
|
|
||||||
|
/* ==============================================================
|
||||||
|
4. INDEX METADATA
|
||||||
|
==============================================================*/
|
||||||
|
indexes AS (
|
||||||
|
SELECT JSON_OBJECT(
|
||||||
|
KEY 'schema' VALUE i.owner,
|
||||||
|
KEY 'table' VALUE i.table_name,
|
||||||
|
KEY 'name' VALUE i.index_name,
|
||||||
|
KEY 'size' VALUE -1,
|
||||||
|
KEY 'column' VALUE c.column_name,
|
||||||
|
KEY 'index_type' VALUE LOWER(i.index_type),
|
||||||
|
KEY 'cardinality' VALUE 0,
|
||||||
|
KEY 'direction' VALUE CASE c.descend WHEN 'DESC' THEN 'desc' ELSE 'asc' END,
|
||||||
|
KEY 'column_position' VALUE c.column_position,
|
||||||
|
/* boolean → use FORMAT JSON so true/false are not quoted */
|
||||||
|
KEY 'unique' VALUE CASE i.uniqueness WHEN 'UNIQUE' THEN 'true' ELSE 'false' END FORMAT JSON
|
||||||
|
RETURNING CLOB
|
||||||
|
) AS json_data
|
||||||
|
FROM all_indexes i
|
||||||
|
JOIN all_ind_columns c
|
||||||
|
ON c.index_owner = i.owner
|
||||||
|
AND c.index_name = i.index_name
|
||||||
|
WHERE i.owner = SYS_CONTEXT('USERENV','CURRENT_SCHEMA')
|
||||||
|
),
|
||||||
|
|
||||||
|
/* ==============================================================
|
||||||
|
5. TABLE & VIEW METADATA
|
||||||
|
==============================================================*/
|
||||||
|
tbls AS (
|
||||||
|
SELECT JSON_OBJECT(
|
||||||
|
KEY 'schema' VALUE owner,
|
||||||
|
KEY 'table' VALUE table_name,
|
||||||
|
KEY 'rows' VALUE num_rows,
|
||||||
|
KEY 'type' VALUE 'TABLE',
|
||||||
|
KEY 'engine' VALUE '""' FORMAT JSON,
|
||||||
|
KEY 'collation' VALUE '""' FORMAT JSON
|
||||||
|
RETURNING CLOB
|
||||||
|
) AS json_data
|
||||||
|
FROM all_tables
|
||||||
|
WHERE owner = SYS_CONTEXT('USERENV','CURRENT_SCHEMA')
|
||||||
|
),
|
||||||
|
views AS (
|
||||||
|
SELECT JSON_OBJECT(
|
||||||
|
KEY 'schema' VALUE owner,
|
||||||
|
KEY 'view_name' VALUE view_name,
|
||||||
|
/* JSON literal for empty string */
|
||||||
|
KEY 'view_definition' VALUE '""' FORMAT JSON
|
||||||
|
RETURNING CLOB
|
||||||
|
) AS json_data
|
||||||
|
FROM all_views
|
||||||
|
WHERE owner = SYS_CONTEXT('USERENV','CURRENT_SCHEMA')
|
||||||
|
)
|
||||||
|
|
||||||
|
/* ==============================================================
|
||||||
|
6. COMPOSE THE FINAL JSON DOCUMENT
|
||||||
|
==============================================================*/
|
||||||
|
SELECT JSON_OBJECT(
|
||||||
|
KEY 'fk_info' VALUE (SELECT JSON_ARRAYAGG(json_data RETURNING CLOB) FROM fk_info),
|
||||||
|
KEY 'pk_info' VALUE (SELECT JSON_ARRAYAGG(json_data RETURNING CLOB) FROM pk_info),
|
||||||
|
KEY 'columns' VALUE (SELECT JSON_ARRAYAGG(json_data RETURNING CLOB) FROM cols),
|
||||||
|
KEY 'indexes' VALUE (SELECT JSON_ARRAYAGG(json_data RETURNING CLOB) FROM indexes),
|
||||||
|
KEY 'tables' VALUE (SELECT JSON_ARRAYAGG(json_data RETURNING CLOB) FROM tbls),
|
||||||
|
KEY 'views' VALUE (SELECT JSON_ARRAYAGG(json_data RETURNING CLOB) FROM views),
|
||||||
|
KEY 'schema' VALUE SYS_CONTEXT('USERENV','CURRENT_SCHEMA'),
|
||||||
|
KEY 'database_name' VALUE SYS_CONTEXT('USERENV','DB_NAME'),
|
||||||
|
KEY 'version' VALUE SYS_CONTEXT('USERENV','DB_NAME')
|
||||||
|
RETURNING CLOB
|
||||||
|
) AS metadata_json_to_import
|
||||||
|
FROM dual
|
||||||
|
`;
|
||||||
@@ -55,6 +55,14 @@ export const getPostgresQuery = (
|
|||||||
AND views.schemaname !~ '^(timescaledb_|_timescaledb_)'
|
AND views.schemaname !~ '^(timescaledb_|_timescaledb_)'
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const withExtras = false;
|
||||||
|
|
||||||
|
const withDefault = `COALESCE(replace(replace(cols.column_default, '"', '\\"'), '\\x', '\\\\x'), '')`;
|
||||||
|
const withoutDefault = `null`;
|
||||||
|
|
||||||
|
const withComments = `COALESCE(replace(replace(dsc.description, '"', '\\"'), '\\x', '\\\\x'), '')`;
|
||||||
|
const withoutComments = `null`;
|
||||||
|
|
||||||
// Define the base query
|
// Define the base query
|
||||||
const query = `${`/* ${databaseEdition ? databaseEditionToLabelMap[databaseEdition] : 'PostgreSQL'} edition */`}
|
const query = `${`/* ${databaseEdition ? databaseEditionToLabelMap[databaseEdition] : 'PostgreSQL'} edition */`}
|
||||||
WITH fk_info${databaseEdition ? '_' + databaseEdition : ''} AS (
|
WITH fk_info${databaseEdition ? '_' + databaseEdition : ''} AS (
|
||||||
@@ -165,7 +173,7 @@ cols AS (
|
|||||||
'","table":"', cols.table_name,
|
'","table":"', cols.table_name,
|
||||||
'","name":"', cols.column_name,
|
'","name":"', cols.column_name,
|
||||||
'","ordinal_position":', cols.ordinal_position,
|
'","ordinal_position":', cols.ordinal_position,
|
||||||
',"type":"', LOWER(replace(cols.data_type, '"', '')),
|
',"type":"', case when LOWER(replace(cols.data_type, '"', '')) = 'user-defined' then pg_type.typname else LOWER(replace(cols.data_type, '"', '')) end,
|
||||||
'","character_maximum_length":"', COALESCE(cols.character_maximum_length::text, 'null'),
|
'","character_maximum_length":"', COALESCE(cols.character_maximum_length::text, 'null'),
|
||||||
'","precision":',
|
'","precision":',
|
||||||
CASE
|
CASE
|
||||||
@@ -175,17 +183,21 @@ cols AS (
|
|||||||
ELSE 'null'
|
ELSE 'null'
|
||||||
END,
|
END,
|
||||||
',"nullable":', CASE WHEN (cols.IS_NULLABLE = 'YES') THEN 'true' ELSE 'false' END,
|
',"nullable":', CASE WHEN (cols.IS_NULLABLE = 'YES') THEN 'true' ELSE 'false' END,
|
||||||
',"default":"', COALESCE(replace(replace(cols.column_default, '"', '\\"'), '\\x', '\\\\x'), ''),
|
',"default":"', ${withExtras ? withDefault : withoutDefault},
|
||||||
'","collation":"', COALESCE(cols.COLLATION_NAME, ''),
|
'","collation":"', COALESCE(cols.COLLATION_NAME, ''),
|
||||||
'","comment":"', COALESCE(replace(replace(dsc.description, '"', '\\"'), '\\x', '\\\\x'), ''),
|
'","comment":"', ${withExtras ? withComments : withoutComments},
|
||||||
'"}')), ',') AS cols_metadata
|
'"}')), ',') AS cols_metadata
|
||||||
FROM information_schema.columns cols
|
FROM information_schema.columns cols
|
||||||
LEFT JOIN pg_catalog.pg_class c
|
LEFT JOIN pg_catalog.pg_class c
|
||||||
ON c.relname = cols.table_name
|
ON c.relname = cols.table_name
|
||||||
JOIN pg_catalog.pg_namespace n
|
JOIN pg_catalog.pg_namespace n
|
||||||
ON n.oid = c.relnamespace AND n.nspname = cols.table_schema
|
ON n.oid = c.relnamespace AND n.nspname = cols.table_schema
|
||||||
LEFT JOIN pg_catalog.pg_description dsc ON dsc.objoid = c.oid
|
LEFT JOIN pg_catalog.pg_description dsc
|
||||||
AND dsc.objsubid = cols.ordinal_position
|
ON dsc.objoid = c.oid AND dsc.objsubid = cols.ordinal_position
|
||||||
|
LEFT JOIN pg_catalog.pg_attribute attr
|
||||||
|
ON attr.attrelid = c.oid AND attr.attname = cols.column_name
|
||||||
|
LEFT JOIN pg_catalog.pg_type
|
||||||
|
ON pg_type.oid = attr.atttypid
|
||||||
WHERE cols.table_schema NOT IN ('information_schema', 'pg_catalog')${
|
WHERE cols.table_schema NOT IN ('information_schema', 'pg_catalog')${
|
||||||
databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
|
databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
|
||||||
? timescaleColFilter
|
? timescaleColFilter
|
||||||
@@ -255,6 +267,62 @@ cols AS (
|
|||||||
? supabaseViewsFilter
|
? supabaseViewsFilter
|
||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
|
), custom_types AS (
|
||||||
|
SELECT array_to_string(array_agg(type_json), ',') AS custom_types_metadata
|
||||||
|
FROM (
|
||||||
|
-- ENUM types
|
||||||
|
SELECT CONCAT(
|
||||||
|
'{"schema":"', n.nspname,
|
||||||
|
'","type":"', t.typname,
|
||||||
|
'","kind":"enum"',
|
||||||
|
',"values":[', string_agg('"' || e.enumlabel || '"', ',' ORDER BY e.enumsortorder), ']}'
|
||||||
|
) AS type_json
|
||||||
|
FROM pg_type t
|
||||||
|
JOIN pg_enum e ON t.oid = e.enumtypid
|
||||||
|
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||||
|
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') ${
|
||||||
|
databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
|
||||||
|
? timescaleViewsFilter
|
||||||
|
: databaseEdition === DatabaseEdition.POSTGRESQL_SUPABASE
|
||||||
|
? supabaseViewsFilter
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
GROUP BY n.nspname, t.typname
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
-- COMPOSITE types
|
||||||
|
SELECT CONCAT(
|
||||||
|
'{"schema":"', schema_name,
|
||||||
|
'","type":"', type_name,
|
||||||
|
'","kind":"composite"',
|
||||||
|
',"fields":[', fields_json, ']}'
|
||||||
|
) AS type_json
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
n.nspname AS schema_name,
|
||||||
|
t.typname AS type_name,
|
||||||
|
string_agg(
|
||||||
|
CONCAT('{"field":"', a.attname, '","type":"', format_type(a.atttypid, a.atttypmod), '"}'),
|
||||||
|
',' ORDER BY a.attnum
|
||||||
|
) AS fields_json
|
||||||
|
FROM pg_type t
|
||||||
|
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||||
|
JOIN pg_class c ON c.oid = t.typrelid
|
||||||
|
JOIN pg_attribute a ON a.attrelid = c.oid
|
||||||
|
WHERE t.typtype = 'c'
|
||||||
|
AND c.relkind = 'c' -- ✅ Only user-defined composite types
|
||||||
|
AND a.attnum > 0 AND NOT a.attisdropped
|
||||||
|
AND n.nspname NOT IN ('pg_catalog', 'information_schema') ${
|
||||||
|
databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
|
||||||
|
? timescaleViewsFilter
|
||||||
|
: databaseEdition === DatabaseEdition.POSTGRESQL_SUPABASE
|
||||||
|
? supabaseViewsFilter
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
GROUP BY n.nspname, t.typname
|
||||||
|
) AS comp
|
||||||
|
) AS all_types
|
||||||
)
|
)
|
||||||
SELECT CONCAT('{ "fk_info": [', COALESCE(fk_metadata, ''),
|
SELECT CONCAT('{ "fk_info": [', COALESCE(fk_metadata, ''),
|
||||||
'], "pk_info": [', COALESCE(pk_metadata, ''),
|
'], "pk_info": [', COALESCE(pk_metadata, ''),
|
||||||
@@ -262,9 +330,10 @@ SELECT CONCAT('{ "fk_info": [', COALESCE(fk_metadata, ''),
|
|||||||
'], "indexes": [', COALESCE(indexes_metadata, ''),
|
'], "indexes": [', COALESCE(indexes_metadata, ''),
|
||||||
'], "tables":[', COALESCE(tbls_metadata, ''),
|
'], "tables":[', COALESCE(tbls_metadata, ''),
|
||||||
'], "views":[', COALESCE(views_metadata, ''),
|
'], "views":[', COALESCE(views_metadata, ''),
|
||||||
|
'], "custom_types": [', COALESCE(custom_types_metadata, ''),
|
||||||
'], "database_name": "', CURRENT_DATABASE(), '', '", "version": "', '',
|
'], "database_name": "', CURRENT_DATABASE(), '', '", "version": "', '',
|
||||||
'"}') AS metadata_json_to_import
|
'"}') AS metadata_json_to_import
|
||||||
FROM fk_info${databaseEdition ? '_' + databaseEdition : ''}, pk_info, cols, indexes_metadata, tbls, config, views;
|
FROM fk_info${databaseEdition ? '_' + databaseEdition : ''}, pk_info, cols, indexes_metadata, tbls, config, views, custom_types;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const psqlPreCommand = `# *** Remember to change! (HOST_NAME, PORT, USER_NAME, DATABASE_NAME) *** \n`;
|
const psqlPreCommand = `# *** Remember to change! (HOST_NAME, PORT, USER_NAME, DATABASE_NAME) *** \n`;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
|
|||||||
import type { DatabaseClient } from '@/lib/domain/database-clients';
|
import type { DatabaseClient } from '@/lib/domain/database-clients';
|
||||||
import { clickhouseQuery } from './clickhouse-script';
|
import { clickhouseQuery } from './clickhouse-script';
|
||||||
import { cockroachdbQuery } from './cockroachdb-script';
|
import { cockroachdbQuery } from './cockroachdb-script';
|
||||||
|
import { oracleDBQuery } from './oracle-script';
|
||||||
|
|
||||||
export type ImportMetadataScripts = Record<
|
export type ImportMetadataScripts = Record<
|
||||||
DatabaseType,
|
DatabaseType,
|
||||||
@@ -26,4 +27,5 @@ export const importMetadataScripts: ImportMetadataScripts = {
|
|||||||
[DatabaseType.MARIADB]: () => mariaDBQuery,
|
[DatabaseType.MARIADB]: () => mariaDBQuery,
|
||||||
[DatabaseType.CLICKHOUSE]: () => clickhouseQuery,
|
[DatabaseType.CLICKHOUSE]: () => clickhouseQuery,
|
||||||
[DatabaseType.COCKROACHDB]: () => cockroachdbQuery,
|
[DatabaseType.COCKROACHDB]: () => cockroachdbQuery,
|
||||||
|
[DatabaseType.ORACLE]: () => oracleDBQuery,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||||
import { DatabaseClient } from '@/lib/domain/database-clients';
|
import { DatabaseClient } from '@/lib/domain/database-clients';
|
||||||
|
|
||||||
|
const withExtras = true;
|
||||||
|
|
||||||
|
const withDefault = `COALESCE(REPLACE(p.dflt_value, '"', '\\"'), '')`;
|
||||||
|
const withoutDefault = `null`;
|
||||||
|
|
||||||
const sqliteQuery = `${`/* Standard SQLite */`}
|
const sqliteQuery = `${`/* Standard SQLite */`}
|
||||||
WITH fk_info AS (
|
WITH fk_info AS (
|
||||||
SELECT
|
SELECT
|
||||||
@@ -114,7 +119,7 @@ WITH fk_info AS (
|
|||||||
END
|
END
|
||||||
ELSE null
|
ELSE null
|
||||||
END,
|
END,
|
||||||
'default', COALESCE(REPLACE(p.dflt_value, '"', '\\"'), '')
|
'default', ${withExtras ? withDefault : withoutDefault}
|
||||||
)
|
)
|
||||||
) AS cols_metadata
|
) AS cols_metadata
|
||||||
FROM
|
FROM
|
||||||
@@ -287,7 +292,7 @@ WITH fk_info AS (
|
|||||||
END
|
END
|
||||||
ELSE null
|
ELSE null
|
||||||
END,
|
END,
|
||||||
'default', COALESCE(REPLACE(p.dflt_value, '"', '\\"'), '')
|
'default', ${withExtras ? withDefault : withoutDefault}
|
||||||
)
|
)
|
||||||
) AS cols_metadata
|
) AS cols_metadata
|
||||||
FROM
|
FROM
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||||
|
|
||||||
|
const withExtras = false;
|
||||||
|
|
||||||
|
const withDefault = `'"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"'`;
|
||||||
|
const withoutDefault = `'""'`;
|
||||||
|
|
||||||
const sqlServerQuery = `${`/* SQL Server 2017 and above edition (14.0, 15.0, 16.0, 17.0)*/`}
|
const sqlServerQuery = `${`/* SQL Server 2017 and above edition (14.0, 15.0, 16.0, 17.0)*/`}
|
||||||
WITH fk_info AS (
|
WITH fk_info AS (
|
||||||
SELECT
|
SELECT
|
||||||
@@ -81,8 +86,7 @@ cols AS (
|
|||||||
ELSE 'null'
|
ELSE 'null'
|
||||||
END +
|
END +
|
||||||
', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
|
', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
|
||||||
', "default": ' +
|
', "default": ' + ${withExtras ? withDefault : withoutDefault} +
|
||||||
'"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"' +
|
|
||||||
', "collation": ' + CASE
|
', "collation": ' + CASE
|
||||||
WHEN cols.COLLATION_NAME IS NULL THEN 'null'
|
WHEN cols.COLLATION_NAME IS NULL THEN 'null'
|
||||||
ELSE '"' + STRING_ESCAPE(cols.COLLATION_NAME, 'json') + '"'
|
ELSE '"' + STRING_ESCAPE(cols.COLLATION_NAME, 'json') + '"'
|
||||||
@@ -275,8 +279,7 @@ cols AS (
|
|||||||
ELSE 'null'
|
ELSE 'null'
|
||||||
END +
|
END +
|
||||||
', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
|
', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
|
||||||
', "default": ' +
|
', "default": ' + ${withExtras ? withDefault : withoutDefault} +
|
||||||
'"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"' +
|
|
||||||
', "collation": ' +
|
', "collation": ' +
|
||||||
CASE
|
CASE
|
||||||
WHEN cols.COLLATION_NAME IS NULL THEN 'null'
|
WHEN cols.COLLATION_NAME IS NULL THEN 'null'
|
||||||
|
|||||||
@@ -415,6 +415,16 @@ export const typeAffinity: Record<string, Record<string, string>> = {
|
|||||||
time: 'text',
|
time: 'text',
|
||||||
json: 'text',
|
json: 'text',
|
||||||
},
|
},
|
||||||
|
[DatabaseType.ORACLE]: {
|
||||||
|
// Oracle data types (all lowercase for consistency)
|
||||||
|
varchar2: 'varchar',
|
||||||
|
nvarchar2: 'varchar',
|
||||||
|
number: 'numeric',
|
||||||
|
date: 'date',
|
||||||
|
timestamp: 'timestamp',
|
||||||
|
clob: 'text',
|
||||||
|
blob: 'blob',
|
||||||
|
},
|
||||||
[DatabaseType.GENERIC]: {
|
[DatabaseType.GENERIC]: {
|
||||||
// Generic fallback types (all lowercase for consistency)
|
// Generic fallback types (all lowercase for consistency)
|
||||||
integer: 'integer',
|
integer: 'integer',
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ import ClickhouseLogo2 from '@/assets/clickhouse_logo_2.png';
|
|||||||
import CockroachDBLogo from '@/assets/cockroachdb_logo.png';
|
import CockroachDBLogo from '@/assets/cockroachdb_logo.png';
|
||||||
import CockroachDBLogoDark from '@/assets/cockroachdb_logo_dark.png';
|
import CockroachDBLogoDark from '@/assets/cockroachdb_logo_dark.png';
|
||||||
import CockroachDBLogo2 from '@/assets/cockroachdb_logo_2.png';
|
import CockroachDBLogo2 from '@/assets/cockroachdb_logo_2.png';
|
||||||
|
import OracleLogo from '@/assets/oracle_logo.png';
|
||||||
|
import OracleLogoDark from '@/assets/oracle_logo_dark.png';
|
||||||
|
import OracleLogo2 from '@/assets/oracle_logo_2.png';
|
||||||
import { DatabaseType } from './domain/database-type';
|
import { DatabaseType } from './domain/database-type';
|
||||||
import type { EffectiveTheme } from '@/context/theme-context/theme-context';
|
import type { EffectiveTheme } from '@/context/theme-context/theme-context';
|
||||||
|
|
||||||
@@ -32,6 +35,7 @@ export const databaseTypeToLabelMap: Record<DatabaseType, string> = {
|
|||||||
[DatabaseType.SQLITE]: 'SQLite',
|
[DatabaseType.SQLITE]: 'SQLite',
|
||||||
[DatabaseType.CLICKHOUSE]: 'ClickHouse',
|
[DatabaseType.CLICKHOUSE]: 'ClickHouse',
|
||||||
[DatabaseType.COCKROACHDB]: 'CockroachDB',
|
[DatabaseType.COCKROACHDB]: 'CockroachDB',
|
||||||
|
[DatabaseType.ORACLE]: 'Oracle',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const databaseLogoMap: Record<DatabaseType, string> = {
|
export const databaseLogoMap: Record<DatabaseType, string> = {
|
||||||
@@ -42,6 +46,7 @@ export const databaseLogoMap: Record<DatabaseType, string> = {
|
|||||||
[DatabaseType.SQL_SERVER]: SqlServerLogo,
|
[DatabaseType.SQL_SERVER]: SqlServerLogo,
|
||||||
[DatabaseType.CLICKHOUSE]: ClickhouseLogo,
|
[DatabaseType.CLICKHOUSE]: ClickhouseLogo,
|
||||||
[DatabaseType.COCKROACHDB]: CockroachDBLogo,
|
[DatabaseType.COCKROACHDB]: CockroachDBLogo,
|
||||||
|
[DatabaseType.ORACLE]: OracleLogo,
|
||||||
[DatabaseType.GENERIC]: '',
|
[DatabaseType.GENERIC]: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,6 +58,7 @@ export const databaseDarkLogoMap: Record<DatabaseType, string> = {
|
|||||||
[DatabaseType.SQL_SERVER]: SqlServerLogoDark,
|
[DatabaseType.SQL_SERVER]: SqlServerLogoDark,
|
||||||
[DatabaseType.CLICKHOUSE]: ClickhouseLogoDark,
|
[DatabaseType.CLICKHOUSE]: ClickhouseLogoDark,
|
||||||
[DatabaseType.COCKROACHDB]: CockroachDBLogoDark,
|
[DatabaseType.COCKROACHDB]: CockroachDBLogoDark,
|
||||||
|
[DatabaseType.ORACLE]: OracleLogoDark,
|
||||||
[DatabaseType.GENERIC]: '',
|
[DatabaseType.GENERIC]: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,5 +78,6 @@ export const databaseSecondaryLogoMap: Record<DatabaseType, string> = {
|
|||||||
[DatabaseType.SQL_SERVER]: SqlServerLogo2,
|
[DatabaseType.SQL_SERVER]: SqlServerLogo2,
|
||||||
[DatabaseType.CLICKHOUSE]: ClickhouseLogo2,
|
[DatabaseType.CLICKHOUSE]: ClickhouseLogo2,
|
||||||
[DatabaseType.COCKROACHDB]: CockroachDBLogo2,
|
[DatabaseType.COCKROACHDB]: CockroachDBLogo2,
|
||||||
|
[DatabaseType.ORACLE]: OracleLogo2,
|
||||||
[DatabaseType.GENERIC]: GeneralDBLogo2,
|
[DatabaseType.GENERIC]: GeneralDBLogo2,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const databaseTypeToClientsMap: Record<DatabaseType, DatabaseClient[]> =
|
|||||||
[DatabaseType.MARIADB]: [],
|
[DatabaseType.MARIADB]: [],
|
||||||
[DatabaseType.CLICKHOUSE]: [],
|
[DatabaseType.CLICKHOUSE]: [],
|
||||||
[DatabaseType.COCKROACHDB]: [],
|
[DatabaseType.COCKROACHDB]: [],
|
||||||
|
[DatabaseType.ORACLE]: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const databaseEditionToClientsMap: Record<
|
export const databaseEditionToClientsMap: Record<
|
||||||
|
|||||||
@@ -63,4 +63,5 @@ export const databaseTypeToEditionMap: Record<DatabaseType, DatabaseEdition[]> =
|
|||||||
[DatabaseType.MARIADB]: [],
|
[DatabaseType.MARIADB]: [],
|
||||||
[DatabaseType.CLICKHOUSE]: [],
|
[DatabaseType.CLICKHOUSE]: [],
|
||||||
[DatabaseType.COCKROACHDB]: [],
|
[DatabaseType.COCKROACHDB]: [],
|
||||||
|
[DatabaseType.ORACLE]: [],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ export enum DatabaseType {
|
|||||||
SQLITE = 'sqlite',
|
SQLITE = 'sqlite',
|
||||||
CLICKHOUSE = 'clickhouse',
|
CLICKHOUSE = 'clickhouse',
|
||||||
COCKROACHDB = 'cockroachdb',
|
COCKROACHDB = 'cockroachdb',
|
||||||
|
ORACLE = 'oracle',
|
||||||
}
|
}
|
||||||
|
|||||||
60
src/lib/domain/db-custom-type.ts
Normal file
60
src/lib/domain/db-custom-type.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import type { DBCustomTypeInfo } from '@/lib/data/import-metadata/metadata-types/custom-type-info';
|
||||||
|
import { generateId } from '../utils';
|
||||||
|
import { schemaNameToDomainSchemaName } from './db-schema';
|
||||||
|
|
||||||
|
export enum DBCustomTypeKind {
|
||||||
|
enum = 'enum',
|
||||||
|
composite = 'composite',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DBCustomTypeField {
|
||||||
|
field: string;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DBCustomType {
|
||||||
|
id: string;
|
||||||
|
schema?: string;
|
||||||
|
name: string;
|
||||||
|
kind: DBCustomTypeKind;
|
||||||
|
values?: string[]; // For enum types
|
||||||
|
fields?: DBCustomTypeField[]; // For composite types
|
||||||
|
order?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dbCustomTypeFieldSchema = z.object({
|
||||||
|
field: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const dbCustomTypeSchema: z.ZodType<DBCustomType> = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
schema: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
kind: z.nativeEnum(DBCustomTypeKind),
|
||||||
|
values: z.array(z.string()).optional(),
|
||||||
|
fields: z.array(dbCustomTypeFieldSchema).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createCustomTypesFromMetadata = ({
|
||||||
|
customTypes,
|
||||||
|
}: {
|
||||||
|
customTypes: DBCustomTypeInfo[];
|
||||||
|
}): DBCustomType[] => {
|
||||||
|
return customTypes.map((customType) => {
|
||||||
|
return {
|
||||||
|
id: generateId(),
|
||||||
|
schema: schemaNameToDomainSchemaName(customType.schema),
|
||||||
|
name: customType.type,
|
||||||
|
kind: customType.kind as DBCustomTypeKind,
|
||||||
|
values: customType.values,
|
||||||
|
fields: customType.fields,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const customTypeKindToLabel: Record<DBCustomTypeKind, string> = {
|
||||||
|
enum: 'Enum',
|
||||||
|
composite: 'Composite',
|
||||||
|
};
|
||||||
@@ -48,6 +48,7 @@ const astDatabaseTypes: Record<DatabaseType, string> = {
|
|||||||
[DatabaseType.SQL_SERVER]: 'postgresql',
|
[DatabaseType.SQL_SERVER]: 'postgresql',
|
||||||
[DatabaseType.CLICKHOUSE]: 'postgresql',
|
[DatabaseType.CLICKHOUSE]: 'postgresql',
|
||||||
[DatabaseType.COCKROACHDB]: 'postgresql',
|
[DatabaseType.COCKROACHDB]: 'postgresql',
|
||||||
|
[DatabaseType.ORACLE]: 'postgresql',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createDependenciesFromMetadata = async ({
|
export const createDependenciesFromMetadata = async ({
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import type { ColumnInfo } from '../data/import-metadata/metadata-types/column-i
|
|||||||
import type { AggregatedIndexInfo } from '../data/import-metadata/metadata-types/index-info';
|
import type { AggregatedIndexInfo } from '../data/import-metadata/metadata-types/index-info';
|
||||||
import type { PrimaryKeyInfo } from '../data/import-metadata/metadata-types/primary-key-info';
|
import type { PrimaryKeyInfo } from '../data/import-metadata/metadata-types/primary-key-info';
|
||||||
import type { TableInfo } from '../data/import-metadata/metadata-types/table-info';
|
import type { TableInfo } from '../data/import-metadata/metadata-types/table-info';
|
||||||
import { generateId } from '../utils';
|
|
||||||
import { schemaNameToDomainSchemaName } from './db-schema';
|
import { schemaNameToDomainSchemaName } from './db-schema';
|
||||||
|
import { generateId } from '../utils';
|
||||||
|
|
||||||
export interface DBField {
|
export interface DBField {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ export const databasesWithSchemas: DatabaseType[] = [
|
|||||||
DatabaseType.SQL_SERVER,
|
DatabaseType.SQL_SERVER,
|
||||||
DatabaseType.CLICKHOUSE,
|
DatabaseType.CLICKHOUSE,
|
||||||
DatabaseType.COCKROACHDB,
|
DatabaseType.COCKROACHDB,
|
||||||
|
DatabaseType.ORACLE,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ import {
|
|||||||
} from './db-table';
|
} from './db-table';
|
||||||
import { generateDiagramId } from '@/lib/utils';
|
import { generateDiagramId } from '@/lib/utils';
|
||||||
import { areaSchema, type Area } from './area';
|
import { areaSchema, type Area } from './area';
|
||||||
|
import type { DBCustomType } from './db-custom-type';
|
||||||
|
import {
|
||||||
|
dbCustomTypeSchema,
|
||||||
|
createCustomTypesFromMetadata,
|
||||||
|
} from './db-custom-type';
|
||||||
|
|
||||||
export interface Diagram {
|
export interface Diagram {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -29,6 +35,7 @@ export interface Diagram {
|
|||||||
relationships?: DBRelationship[];
|
relationships?: DBRelationship[];
|
||||||
dependencies?: DBDependency[];
|
dependencies?: DBDependency[];
|
||||||
areas?: Area[];
|
areas?: Area[];
|
||||||
|
customTypes?: DBCustomType[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
@@ -42,6 +49,7 @@ export const diagramSchema: z.ZodType<Diagram> = z.object({
|
|||||||
relationships: z.array(dbRelationshipSchema).optional(),
|
relationships: z.array(dbRelationshipSchema).optional(),
|
||||||
dependencies: z.array(dbDependencySchema).optional(),
|
dependencies: z.array(dbDependencySchema).optional(),
|
||||||
areas: z.array(areaSchema).optional(),
|
areas: z.array(areaSchema).optional(),
|
||||||
|
customTypes: z.array(dbCustomTypeSchema).optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date(),
|
updatedAt: z.date(),
|
||||||
});
|
});
|
||||||
@@ -57,7 +65,11 @@ export const loadFromDatabaseMetadata = async ({
|
|||||||
diagramNumber?: number;
|
diagramNumber?: number;
|
||||||
databaseEdition?: DatabaseEdition;
|
databaseEdition?: DatabaseEdition;
|
||||||
}): Promise<Diagram> => {
|
}): Promise<Diagram> => {
|
||||||
const { fk_info: foreignKeys, views: views } = databaseMetadata;
|
const {
|
||||||
|
fk_info: foreignKeys,
|
||||||
|
views: views,
|
||||||
|
custom_types: customTypes,
|
||||||
|
} = databaseMetadata;
|
||||||
|
|
||||||
const tables = createTablesFromMetadata({
|
const tables = createTablesFromMetadata({
|
||||||
databaseMetadata,
|
databaseMetadata,
|
||||||
@@ -75,6 +87,12 @@ export const loadFromDatabaseMetadata = async ({
|
|||||||
databaseType,
|
databaseType,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dbCustomTypes = customTypes
|
||||||
|
? createCustomTypesFromMetadata({
|
||||||
|
customTypes,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
const adjustedTables = adjustTablePositions({
|
const adjustedTables = adjustTablePositions({
|
||||||
tables,
|
tables,
|
||||||
relationships,
|
relationships,
|
||||||
@@ -102,6 +120,7 @@ export const loadFromDatabaseMetadata = async ({
|
|||||||
tables: sortedTables,
|
tables: sortedTables,
|
||||||
relationships,
|
relationships,
|
||||||
dependencies,
|
dependencies,
|
||||||
|
customTypes: dbCustomTypes,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from '@/components/sidebar/sidebar';
|
} from '@/components/sidebar/sidebar';
|
||||||
import { Twitter, BookOpen, Group } from 'lucide-react';
|
import { Twitter, BookOpen, Group, FileType } from 'lucide-react';
|
||||||
import { SquareStack, Table, Workflow } from 'lucide-react';
|
import { SquareStack, Table, Workflow } from 'lucide-react';
|
||||||
import { useLayout } from '@/hooks/use-layout';
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -20,6 +20,7 @@ import ChartDBLogo from '@/assets/logo-light.png';
|
|||||||
import ChartDBDarkLogo from '@/assets/logo-dark.png';
|
import ChartDBDarkLogo from '@/assets/logo-dark.png';
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
|
||||||
export interface SidebarItem {
|
export interface SidebarItem {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -37,7 +38,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isMd: isDesktop } = useBreakpoint('md');
|
const { isMd: isDesktop } = useBreakpoint('md');
|
||||||
const { effectiveTheme } = useTheme();
|
const { effectiveTheme } = useTheme();
|
||||||
const { dependencies } = useChartDB();
|
const { dependencies, databaseType } = useChartDB();
|
||||||
|
|
||||||
const items: SidebarItem[] = useMemo(() => {
|
const items: SidebarItem[] = useMemo(() => {
|
||||||
const baseItems = [
|
const baseItems = [
|
||||||
@@ -68,20 +69,38 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
|
|||||||
},
|
},
|
||||||
active: selectedSidebarSection === 'areas',
|
active: selectedSidebarSection === 'areas',
|
||||||
},
|
},
|
||||||
|
...(dependencies && dependencies.length > 0
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: t(
|
||||||
|
'side_panel.dependencies_section.dependencies'
|
||||||
|
),
|
||||||
|
icon: SquareStack,
|
||||||
|
onClick: () => {
|
||||||
|
showSidePanel();
|
||||||
|
selectSidebarSection('dependencies');
|
||||||
|
},
|
||||||
|
active: selectedSidebarSection === 'dependencies',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...(databaseType === DatabaseType.POSTGRESQL
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: t(
|
||||||
|
'side_panel.custom_types_section.custom_types'
|
||||||
|
),
|
||||||
|
icon: FileType,
|
||||||
|
onClick: () => {
|
||||||
|
showSidePanel();
|
||||||
|
selectSidebarSection('customTypes');
|
||||||
|
},
|
||||||
|
active: selectedSidebarSection === 'customTypes',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (dependencies && dependencies.length > 0) {
|
|
||||||
baseItems.splice(2, 0, {
|
|
||||||
title: t('side_panel.dependencies_section.dependencies'),
|
|
||||||
icon: SquareStack,
|
|
||||||
onClick: () => {
|
|
||||||
showSidePanel();
|
|
||||||
selectSidebarSection('dependencies');
|
|
||||||
},
|
|
||||||
active: selectedSidebarSection === 'dependencies',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return baseItems;
|
return baseItems;
|
||||||
}, [
|
}, [
|
||||||
selectSidebarSection,
|
selectSidebarSection,
|
||||||
@@ -89,6 +108,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
|
|||||||
t,
|
t,
|
||||||
showSidePanel,
|
showSidePanel,
|
||||||
dependencies,
|
dependencies,
|
||||||
|
databaseType,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const footerItems: SidebarItem[] = useMemo(
|
const footerItems: SidebarItem[] = useMemo(
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { X, GripVertical } from 'lucide-react';
|
||||||
|
import React from 'react';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import type { DBCustomTypeField } from '@/lib/domain/db-custom-type';
|
||||||
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
|
||||||
|
export interface CompositeFieldProps {
|
||||||
|
field: DBCustomTypeField;
|
||||||
|
onRemove: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CompositeField: React.FC<{
|
||||||
|
field: DBCustomTypeField;
|
||||||
|
onRemove: () => void;
|
||||||
|
}> = ({ field, onRemove }) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({ id: field.field });
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className="flex items-center gap-2 rounded-md border p-2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex cursor-move items-center justify-center"
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
<GripVertical className="size-3 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 text-sm">{field.field}</div>
|
||||||
|
<div className="text-xs text-muted-foreground">{field.type}</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="size-6 p-0 text-muted-foreground hover:text-red-500"
|
||||||
|
onClick={onRemove}
|
||||||
|
>
|
||||||
|
<X className="size-3" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
import { Plus, RectangleEllipsis } from 'lucide-react';
|
||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Input } from '@/components/input/input';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import type { DBCustomTypeField } from '@/lib/domain/db-custom-type';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/select/select';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import type { DataTypeData } from '@/lib/data/data-types/data-types';
|
||||||
|
import { dataTypeMap } from '@/lib/data/data-types/data-types';
|
||||||
|
import type { DragEndEvent } from '@dnd-kit/core';
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
closestCenter,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import {
|
||||||
|
arrayMove,
|
||||||
|
SortableContext,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from '@dnd-kit/sortable';
|
||||||
|
import { CompositeField } from './composite-field';
|
||||||
|
|
||||||
|
export interface CustomTypeCompositeFieldsProps {
|
||||||
|
fields: DBCustomTypeField[];
|
||||||
|
addField: (value: DBCustomTypeField) => void;
|
||||||
|
removeField: (value: DBCustomTypeField) => void;
|
||||||
|
reorderFields: (fields: DBCustomTypeField[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomTypeCompositeFields: React.FC<
|
||||||
|
CustomTypeCompositeFieldsProps
|
||||||
|
> = ({ fields, addField, removeField, reorderFields }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { currentDiagram, customTypes } = useChartDB();
|
||||||
|
const [newFieldName, setNewFieldName] = useState('');
|
||||||
|
const [newFieldType, setNewFieldType] = useState('');
|
||||||
|
|
||||||
|
const dataTypes = useMemo(
|
||||||
|
() => dataTypeMap[currentDiagram.databaseType] || [],
|
||||||
|
[currentDiagram.databaseType]
|
||||||
|
);
|
||||||
|
|
||||||
|
const customDataTypes = useMemo<DataTypeData[]>(
|
||||||
|
() =>
|
||||||
|
customTypes.map<DataTypeData>((type) => ({
|
||||||
|
id: type.name,
|
||||||
|
name: type.name,
|
||||||
|
})),
|
||||||
|
[customTypes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const sensors = useSensors(useSensor(PointerSensor));
|
||||||
|
|
||||||
|
const handleDragEnd = useCallback(
|
||||||
|
(event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
|
||||||
|
if (active?.id !== over?.id && !!over && !!active) {
|
||||||
|
const oldIndex = fields.findIndex(
|
||||||
|
(field) => field.field === active.id
|
||||||
|
);
|
||||||
|
const newIndex = fields.findIndex(
|
||||||
|
(field) => field.field === over.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (oldIndex !== -1 && newIndex !== -1) {
|
||||||
|
const reorderedFields = arrayMove(
|
||||||
|
fields,
|
||||||
|
oldIndex,
|
||||||
|
newIndex
|
||||||
|
);
|
||||||
|
reorderFields(reorderedFields);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fields, reorderFields]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddField = useCallback(() => {
|
||||||
|
if (newFieldName.trim() && newFieldType.trim()) {
|
||||||
|
// Check if field name already exists
|
||||||
|
const fieldExists = fields.some(
|
||||||
|
(field) => field.field === newFieldName.trim()
|
||||||
|
);
|
||||||
|
if (fieldExists) {
|
||||||
|
return; // Don't add duplicate field names
|
||||||
|
}
|
||||||
|
|
||||||
|
addField({
|
||||||
|
field: newFieldName.trim(),
|
||||||
|
type: newFieldType.trim(),
|
||||||
|
});
|
||||||
|
setNewFieldName('');
|
||||||
|
setNewFieldType('');
|
||||||
|
}
|
||||||
|
}, [newFieldName, newFieldType, addField, fields]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleAddField();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleAddField]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveField = useCallback(
|
||||||
|
(field: DBCustomTypeField) => {
|
||||||
|
removeField(field);
|
||||||
|
},
|
||||||
|
[removeField]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 text-xs">
|
||||||
|
<div className="flex flex-row items-center gap-1">
|
||||||
|
<RectangleEllipsis className="size-4 text-subtitle" />
|
||||||
|
<div className="font-bold text-subtitle">
|
||||||
|
{t(
|
||||||
|
'side_panel.custom_types_section.custom_type.composite_fields'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{fields.length === 0 ? (
|
||||||
|
<div className="py-2 text-muted-foreground">
|
||||||
|
{t('side_panel.custom_types_section.custom_type.no_fields')}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={fields.map((f) => f.field)}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{fields.map((field) => (
|
||||||
|
<CompositeField
|
||||||
|
key={field.field}
|
||||||
|
field={field}
|
||||||
|
onRemove={() => handleRemoveField(field)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
placeholder={t(
|
||||||
|
'side_panel.custom_types_section.custom_type.field_name_placeholder'
|
||||||
|
)}
|
||||||
|
value={newFieldName}
|
||||||
|
onChange={(e) => setNewFieldName(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
className="h-8 flex-1 text-xs"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={newFieldType}
|
||||||
|
onValueChange={setNewFieldType}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8 w-32 text-xs">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t(
|
||||||
|
'side_panel.custom_types_section.custom_type.field_type_placeholder'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Standard Types</SelectLabel>
|
||||||
|
{dataTypes.map((dataType) => (
|
||||||
|
<SelectItem
|
||||||
|
key={dataType.id}
|
||||||
|
value={dataType.name}
|
||||||
|
>
|
||||||
|
{dataType.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
{customDataTypes.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<SelectSeparator />
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Custom Types</SelectLabel>
|
||||||
|
{customDataTypes.map((dataType) => (
|
||||||
|
<SelectItem
|
||||||
|
key={dataType.id}
|
||||||
|
value={dataType.name}
|
||||||
|
>
|
||||||
|
{dataType.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectGroup>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-6 gap-1 self-start text-xs"
|
||||||
|
onClick={handleAddField}
|
||||||
|
disabled={!newFieldName.trim() || !newFieldType.trim()}
|
||||||
|
>
|
||||||
|
<Plus className="size-3" />
|
||||||
|
{t('side_panel.custom_types_section.custom_type.add_field')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/select/select';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import type {
|
||||||
|
DBCustomType,
|
||||||
|
DBCustomTypeField,
|
||||||
|
} from '@/lib/domain/db-custom-type';
|
||||||
|
import {
|
||||||
|
customTypeKindToLabel,
|
||||||
|
DBCustomTypeKind,
|
||||||
|
} from '@/lib/domain/db-custom-type';
|
||||||
|
import { Trash2, Braces } from 'lucide-react';
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { CustomTypeEnumValues } from './enum-values/enum-values';
|
||||||
|
import { CustomTypeCompositeFields } from './composite-fields/composite-fields';
|
||||||
|
|
||||||
|
export interface CustomTypeListItemContentProps {
|
||||||
|
customType: DBCustomType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomTypeListItemContent: React.FC<
|
||||||
|
CustomTypeListItemContentProps
|
||||||
|
> = ({ customType }) => {
|
||||||
|
const { removeCustomType, updateCustomType } = useChartDB();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const deleteCustomTypeHandler = useCallback(() => {
|
||||||
|
removeCustomType(customType.id);
|
||||||
|
}, [customType.id, removeCustomType]);
|
||||||
|
|
||||||
|
const updateCustomTypeKind = useCallback(
|
||||||
|
(kind: DBCustomTypeKind) => {
|
||||||
|
updateCustomType(customType.id, {
|
||||||
|
kind,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[customType.id, updateCustomType]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addEnumValue = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
updateCustomType(customType.id, {
|
||||||
|
values: [...(customType.values || []), value],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[customType.id, customType.values, updateCustomType]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeEnumValue = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
updateCustomType(customType.id, {
|
||||||
|
values: (customType.values || []).filter((v) => v !== value),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[customType.id, customType.values, updateCustomType]
|
||||||
|
);
|
||||||
|
|
||||||
|
const addCompositeField = useCallback(
|
||||||
|
(field: DBCustomTypeField) => {
|
||||||
|
updateCustomType(customType.id, {
|
||||||
|
fields: [...(customType.fields || []), field],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[customType.id, customType.fields, updateCustomType]
|
||||||
|
);
|
||||||
|
|
||||||
|
const removeCompositeField = useCallback(
|
||||||
|
(field: DBCustomTypeField) => {
|
||||||
|
updateCustomType(customType.id, {
|
||||||
|
fields: (customType.fields || []).filter(
|
||||||
|
(f) => f.field !== field.field
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[customType.id, customType.fields, updateCustomType]
|
||||||
|
);
|
||||||
|
|
||||||
|
const reorderCompositeFields = useCallback(
|
||||||
|
(fields: DBCustomTypeField[]) => {
|
||||||
|
updateCustomType(customType.id, {
|
||||||
|
fields,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[customType.id, updateCustomType]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="my-1 flex flex-col rounded-b-md px-1">
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div className="flex flex-col gap-2 text-xs">
|
||||||
|
<div className="flex flex-row items-center gap-1">
|
||||||
|
<Braces className="size-4 text-subtitle" />
|
||||||
|
<div className="font-bold text-subtitle">
|
||||||
|
{t(
|
||||||
|
'side_panel.custom_types_section.custom_type.kind'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={customType.kind}
|
||||||
|
onValueChange={updateCustomTypeKind}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="h-8">
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectItem value={DBCustomTypeKind.composite}>
|
||||||
|
{
|
||||||
|
customTypeKindToLabel[
|
||||||
|
DBCustomTypeKind.composite
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value={DBCustomTypeKind.enum}>
|
||||||
|
{
|
||||||
|
customTypeKindToLabel[
|
||||||
|
DBCustomTypeKind.enum
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{customType.kind === DBCustomTypeKind.enum ? (
|
||||||
|
<CustomTypeEnumValues
|
||||||
|
values={customType.values || []}
|
||||||
|
addValue={addEnumValue}
|
||||||
|
removeValue={removeEnumValue}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CustomTypeCompositeFields
|
||||||
|
fields={customType.fields || []}
|
||||||
|
addField={addCompositeField}
|
||||||
|
removeField={removeCompositeField}
|
||||||
|
reorderFields={reorderCompositeFields}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 items-center justify-center pt-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 p-2 text-xs"
|
||||||
|
onClick={deleteCustomTypeHandler}
|
||||||
|
>
|
||||||
|
<Trash2 className="mr-1 size-3.5 text-red-700" />
|
||||||
|
<div className="text-red-700">
|
||||||
|
{t(
|
||||||
|
'side_panel.custom_types_section.custom_type.delete_custom_type'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { Pause, Plus, X } from 'lucide-react';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Input } from '@/components/input/input';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
|
||||||
|
export interface EnumValuesProps {
|
||||||
|
values: string[];
|
||||||
|
addValue: (value: string) => void;
|
||||||
|
removeValue: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomTypeEnumValues: React.FC<EnumValuesProps> = ({
|
||||||
|
values,
|
||||||
|
addValue,
|
||||||
|
removeValue,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [newValue, setNewValue] = useState('');
|
||||||
|
|
||||||
|
const handleAddValue = useCallback(() => {
|
||||||
|
if (newValue.trim() && !values.includes(newValue.trim())) {
|
||||||
|
addValue(newValue.trim());
|
||||||
|
setNewValue('');
|
||||||
|
}
|
||||||
|
}, [newValue, values, addValue]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleAddValue();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleAddValue]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 text-xs">
|
||||||
|
<div className="flex flex-row items-center gap-1">
|
||||||
|
<Pause className="size-4 text-subtitle" />
|
||||||
|
<div className="font-bold text-subtitle">
|
||||||
|
{t(
|
||||||
|
'side_panel.custom_types_section.custom_type.enum_values'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{values.map((value, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between gap-1 rounded-md border border-border bg-muted/30 px-2 py-1"
|
||||||
|
>
|
||||||
|
<span className="flex-1 truncate text-sm font-medium">
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="size-6 p-0 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
||||||
|
onClick={() => removeValue(value)}
|
||||||
|
>
|
||||||
|
<X className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Input
|
||||||
|
value={newValue}
|
||||||
|
onChange={(e) => setNewValue(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder="Add enum value..."
|
||||||
|
className="h-8 flex-1 text-xs focus-visible:ring-0"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="h-8 px-2 text-xs"
|
||||||
|
onClick={handleAddValue}
|
||||||
|
disabled={
|
||||||
|
!newValue.trim() || values.includes(newValue.trim())
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Plus className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
GripVertical,
|
||||||
|
Pencil,
|
||||||
|
EllipsisVertical,
|
||||||
|
Trash2,
|
||||||
|
Check,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { ListItemHeaderButton } from '@/pages/editor-page/side-panel/list-item-header-button/list-item-header-button';
|
||||||
|
import { Input } from '@/components/input/input';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import { useClickAway, useKeyPressEvent } from 'react-use';
|
||||||
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/dropdown-menu/dropdown-menu';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/tooltip/tooltip';
|
||||||
|
import {
|
||||||
|
customTypeKindToLabel,
|
||||||
|
DBCustomTypeKind,
|
||||||
|
type DBCustomType,
|
||||||
|
} from '@/lib/domain/db-custom-type';
|
||||||
|
import { Badge } from '@/components/badge/badge';
|
||||||
|
|
||||||
|
export interface CustomTypeListItemHeaderProps {
|
||||||
|
customType: DBCustomType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomTypeListItemHeader: React.FC<
|
||||||
|
CustomTypeListItemHeaderProps
|
||||||
|
> = ({ customType }) => {
|
||||||
|
const { updateCustomType, removeCustomType, schemas, filteredSchemas } =
|
||||||
|
useChartDB();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [editMode, setEditMode] = React.useState(false);
|
||||||
|
const [customTypeName, setCustomTypeName] = React.useState(customType.name);
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const { listeners } = useSortable({ id: customType.id });
|
||||||
|
|
||||||
|
const editCustomTypeName = useCallback(() => {
|
||||||
|
if (!editMode) return;
|
||||||
|
if (customTypeName.trim()) {
|
||||||
|
updateCustomType(customType.id, { name: customTypeName.trim() });
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditMode(false);
|
||||||
|
}, [customTypeName, customType.id, updateCustomType, editMode]);
|
||||||
|
|
||||||
|
const abortEdit = useCallback(() => {
|
||||||
|
setEditMode(false);
|
||||||
|
setCustomTypeName(customType.name);
|
||||||
|
}, [customType.name]);
|
||||||
|
|
||||||
|
useClickAway(inputRef, editCustomTypeName);
|
||||||
|
useKeyPressEvent('Enter', editCustomTypeName);
|
||||||
|
useKeyPressEvent('Escape', abortEdit);
|
||||||
|
|
||||||
|
const enterEditMode = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setEditMode(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteCustomTypeHandler = useCallback(() => {
|
||||||
|
removeCustomType(customType.id);
|
||||||
|
}, [customType.id, removeCustomType]);
|
||||||
|
|
||||||
|
const renderDropDownMenu = useCallback(
|
||||||
|
() => (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
<ListItemHeaderButton>
|
||||||
|
<EllipsisVertical />
|
||||||
|
</ListItemHeaderButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-fit min-w-40">
|
||||||
|
<DropdownMenuLabel>
|
||||||
|
{t(
|
||||||
|
'side_panel.custom_types_section.custom_type.custom_type_actions.title'
|
||||||
|
)}
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={deleteCustomTypeHandler}
|
||||||
|
className="flex justify-between !text-red-700"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
'side_panel.custom_types_section.custom_type.custom_type_actions.delete_custom_type'
|
||||||
|
)}
|
||||||
|
<Trash2 className="size-3.5 text-red-700" />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
),
|
||||||
|
[deleteCustomTypeHandler, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
let schemaToDisplay;
|
||||||
|
|
||||||
|
if (schemas.length > 1 && !!filteredSchemas && filteredSchemas.length > 1) {
|
||||||
|
schemaToDisplay = customType.schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group flex h-11 flex-1 items-center justify-between gap-1 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="flex cursor-move items-center justify-center"
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
<GripVertical className="size-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<div className="flex min-w-0 flex-1 px-1">
|
||||||
|
{editMode ? (
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
autoFocus
|
||||||
|
type="text"
|
||||||
|
placeholder={customType.name}
|
||||||
|
value={customTypeName}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onChange={(e) => setCustomTypeName(e.target.value)}
|
||||||
|
className="h-7 w-full focus-visible:ring-0"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div
|
||||||
|
onDoubleClick={enterEditMode}
|
||||||
|
className="text-editable truncate px-2 py-0.5"
|
||||||
|
>
|
||||||
|
{customType.name}
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{schemaToDisplay
|
||||||
|
? ` (${schemaToDisplay})`
|
||||||
|
: ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{t('tool_tips.double_click_to_edit')}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row-reverse items-center">
|
||||||
|
{!editMode ? (
|
||||||
|
<>
|
||||||
|
<div>{renderDropDownMenu()}</div>
|
||||||
|
{customType.kind === DBCustomTypeKind.enum ? (
|
||||||
|
<Badge
|
||||||
|
variant="outline"
|
||||||
|
className="h-fit bg-background px-2 text-xs md:group-hover:hidden"
|
||||||
|
>
|
||||||
|
{customTypeKindToLabel[customType.kind]}
|
||||||
|
</Badge>
|
||||||
|
) : null}
|
||||||
|
<div className="flex flex-row-reverse md:hidden md:group-hover:flex">
|
||||||
|
<ListItemHeaderButton onClick={enterEditMode}>
|
||||||
|
<Pencil />
|
||||||
|
</ListItemHeaderButton>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ListItemHeaderButton onClick={editCustomTypeName}>
|
||||||
|
<Check />
|
||||||
|
</ListItemHeaderButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from '@/components/accordion/accordion';
|
||||||
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
|
import { CustomTypeListItemHeader } from './custom-type-list-item-header/custom-type-list-item-header';
|
||||||
|
import { CustomTypeListItemContent } from './custom-type-list-item-content/custom-type-list-item-content';
|
||||||
|
|
||||||
|
export interface CustomTypeListItemProps {
|
||||||
|
customType: DBCustomType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomTypeListItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AccordionItem>,
|
||||||
|
CustomTypeListItemProps
|
||||||
|
>(({ customType }, ref) => {
|
||||||
|
const { attributes, setNodeRef, transform, transition } = useSortable({
|
||||||
|
id: customType.id,
|
||||||
|
});
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Translate.toString(transform),
|
||||||
|
transition,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccordionItem value={customType.id} className="border-none" ref={ref}>
|
||||||
|
<div
|
||||||
|
className="w-full rounded-md border-b"
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
{...attributes}
|
||||||
|
>
|
||||||
|
<AccordionTrigger
|
||||||
|
className="w-full rounded-md bg-slate-50 px-2 py-0 hover:bg-accent hover:no-underline data-[state=open]:rounded-b-none dark:bg-slate-900"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<CustomTypeListItemHeader customType={customType} />
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="border-x border-slate-100 p-1 pb-0 dark:border-slate-800">
|
||||||
|
<CustomTypeListItemContent customType={customType} />
|
||||||
|
</AccordionContent>
|
||||||
|
</div>
|
||||||
|
</AccordionItem>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CustomTypeListItem.displayName = 'CustomTypeListItem';
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { Accordion } from '@/components/accordion/accordion';
|
||||||
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
|
import {
|
||||||
|
closestCenter,
|
||||||
|
DndContext,
|
||||||
|
type DragEndEvent,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import {
|
||||||
|
arrayMove,
|
||||||
|
SortableContext,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from '@dnd-kit/sortable';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb.ts';
|
||||||
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
|
import { CustomTypeListItem } from './custom-type-list-item/custom-type-list-item';
|
||||||
|
|
||||||
|
export interface CustomTypeProps {
|
||||||
|
customTypes: DBCustomType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CustomTypeList: React.FC<CustomTypeProps> = ({ customTypes }) => {
|
||||||
|
const { updateCustomType } = useChartDB();
|
||||||
|
|
||||||
|
const { openCustomTypeFromSidebar, openedCustomTypeInSidebar } =
|
||||||
|
useLayout();
|
||||||
|
const lastOpenedCustomType = React.useRef<string | null>(null);
|
||||||
|
const refs = useMemo(
|
||||||
|
() =>
|
||||||
|
customTypes.reduce(
|
||||||
|
(acc, customType) => {
|
||||||
|
acc[customType.id] = React.createRef();
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, React.RefObject<HTMLDivElement>>
|
||||||
|
),
|
||||||
|
[customTypes]
|
||||||
|
);
|
||||||
|
|
||||||
|
const scrollToCustomType = useCallback(
|
||||||
|
(id: string) =>
|
||||||
|
refs[id]?.current?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
}),
|
||||||
|
[refs]
|
||||||
|
);
|
||||||
|
|
||||||
|
const sensors = useSensors(useSensor(PointerSensor));
|
||||||
|
|
||||||
|
const handleDragEnd = useCallback(
|
||||||
|
(event: DragEndEvent) => {
|
||||||
|
const { active, over } = event;
|
||||||
|
|
||||||
|
if (active?.id !== over?.id && !!over && !!active) {
|
||||||
|
const oldIndex = customTypes.findIndex(
|
||||||
|
(customType) => customType.id === active.id
|
||||||
|
);
|
||||||
|
const newIndex = customTypes.findIndex(
|
||||||
|
(customType) => customType.id === over.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const newCustomTypesOrder = arrayMove<DBCustomType>(
|
||||||
|
customTypes,
|
||||||
|
oldIndex,
|
||||||
|
newIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
newCustomTypesOrder.forEach((customType, index) => {
|
||||||
|
updateCustomType(customType.id, { order: index });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[customTypes, updateCustomType]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleScrollToCustomType = useCallback(() => {
|
||||||
|
if (
|
||||||
|
openedCustomTypeInSidebar &&
|
||||||
|
lastOpenedCustomType.current !== openedCustomTypeInSidebar
|
||||||
|
) {
|
||||||
|
lastOpenedCustomType.current = openedCustomTypeInSidebar;
|
||||||
|
scrollToCustomType(openedCustomTypeInSidebar);
|
||||||
|
}
|
||||||
|
}, [scrollToCustomType, openedCustomTypeInSidebar]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Accordion
|
||||||
|
type="single"
|
||||||
|
collapsible
|
||||||
|
className="flex w-full flex-col gap-1"
|
||||||
|
value={openedCustomTypeInSidebar}
|
||||||
|
onValueChange={openCustomTypeFromSidebar}
|
||||||
|
onAnimationEnd={handleScrollToCustomType}
|
||||||
|
>
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={customTypes}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
{customTypes
|
||||||
|
.sort(
|
||||||
|
(
|
||||||
|
customType1: DBCustomType,
|
||||||
|
customType2: DBCustomType
|
||||||
|
) => {
|
||||||
|
// if one has order and the other doesn't, the one with order should come first
|
||||||
|
if (
|
||||||
|
customType1.order &&
|
||||||
|
customType2.order === undefined
|
||||||
|
) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
customType1.order === undefined &&
|
||||||
|
customType2.order
|
||||||
|
) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if both have order, sort by order
|
||||||
|
if (
|
||||||
|
customType1.order !== undefined &&
|
||||||
|
customType2.order !== undefined
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
customType1.order - customType2.order
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort by name
|
||||||
|
return customType1.name.localeCompare(
|
||||||
|
customType2.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.map((customType) => (
|
||||||
|
<CustomTypeListItem
|
||||||
|
key={customType.id}
|
||||||
|
customType={customType}
|
||||||
|
ref={refs[customType.id]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import { X, Plus } from 'lucide-react';
|
||||||
|
import { Input } from '@/components/input/input';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import { EmptyState } from '@/components/empty-state/empty-state';
|
||||||
|
import { ScrollArea } from '@/components/scroll-area/scroll-area';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { getOperatingSystem } from '@/lib/utils';
|
||||||
|
import { CustomTypeList } from './custom-type-list/custom-type-list';
|
||||||
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
|
||||||
|
export interface CustomTypesSectionProps {}
|
||||||
|
|
||||||
|
export const CustomTypesSection: React.FC<CustomTypesSectionProps> = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { customTypes, createCustomType, databaseType } = useChartDB();
|
||||||
|
const [filterText, setFilterText] = React.useState('');
|
||||||
|
const filterInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const isPostgres = databaseType === DatabaseType.POSTGRESQL;
|
||||||
|
|
||||||
|
const filteredCustomTypes = useMemo(() => {
|
||||||
|
return customTypes.filter(
|
||||||
|
(type) =>
|
||||||
|
!filterText?.trim?.() ||
|
||||||
|
type.name.toLowerCase().includes(filterText.toLowerCase())
|
||||||
|
);
|
||||||
|
}, [customTypes, filterText]);
|
||||||
|
|
||||||
|
const handleClearFilter = useCallback(() => {
|
||||||
|
setFilterText('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleCreateCustomType = useCallback(async () => {
|
||||||
|
await createCustomType();
|
||||||
|
}, [createCustomType]);
|
||||||
|
|
||||||
|
const operatingSystem = useMemo(() => getOperatingSystem(), []);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
operatingSystem === 'mac' ? 'meta+f' : 'ctrl+f',
|
||||||
|
() => {
|
||||||
|
filterInputRef.current?.focus();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[filterInputRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="flex flex-1 flex-col overflow-hidden px-2"
|
||||||
|
data-vaul-no-drag
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-4 py-1">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
ref={filterInputRef}
|
||||||
|
type="text"
|
||||||
|
placeholder={t(
|
||||||
|
'side_panel.custom_types_section.filter'
|
||||||
|
)}
|
||||||
|
className="h-8 w-full focus-visible:ring-0"
|
||||||
|
value={filterText}
|
||||||
|
onChange={(e) => setFilterText(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{isPostgres && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 px-2"
|
||||||
|
onClick={handleCreateCustomType}
|
||||||
|
>
|
||||||
|
<Plus className="mr-1 size-4" />
|
||||||
|
New Type
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col overflow-hidden">
|
||||||
|
<ScrollArea className="h-full">
|
||||||
|
{customTypes.length === 0 ? (
|
||||||
|
<EmptyState
|
||||||
|
title={t(
|
||||||
|
'side_panel.custom_types_section.empty_state.title'
|
||||||
|
)}
|
||||||
|
description={t(
|
||||||
|
'side_panel.custom_types_section.empty_state.description'
|
||||||
|
)}
|
||||||
|
className="mt-20"
|
||||||
|
/>
|
||||||
|
) : filterText && filteredCustomTypes.length === 0 ? (
|
||||||
|
<div className="mt-10 flex flex-col items-center gap-2">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
'side_panel.custom_types_section.no_results'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleClearFilter}
|
||||||
|
className="gap-1"
|
||||||
|
>
|
||||||
|
<X className="size-3.5" />
|
||||||
|
{t('side_panel.custom_types_section.clear')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<CustomTypeList customTypes={filteredCustomTypes} />
|
||||||
|
)}
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/tooltip/tooltip';
|
} from '@/components/tooltip/tooltip';
|
||||||
import { useDialog } from '@/hooks/use-dialog';
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { getOperatingSystem } from '@/lib/utils';
|
||||||
|
|
||||||
export interface RelationshipsSectionProps {}
|
export interface RelationshipsSectionProps {}
|
||||||
|
|
||||||
@@ -25,6 +27,7 @@ export const RelationshipsSection: React.FC<RelationshipsSectionProps> = () => {
|
|||||||
const { closeAllRelationshipsInSidebar } = useLayout();
|
const { closeAllRelationshipsInSidebar } = useLayout();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { openCreateRelationshipDialog } = useDialog();
|
const { openCreateRelationshipDialog } = useDialog();
|
||||||
|
const filterInputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const filteredRelationships = useMemo(() => {
|
const filteredRelationships = useMemo(() => {
|
||||||
const filterName: (relationship: DBRelationship) => boolean = (
|
const filterName: (relationship: DBRelationship) => boolean = (
|
||||||
@@ -46,6 +49,19 @@ export const RelationshipsSection: React.FC<RelationshipsSectionProps> = () => {
|
|||||||
openCreateRelationshipDialog();
|
openCreateRelationshipDialog();
|
||||||
}, [openCreateRelationshipDialog, setFilterText]);
|
}, [openCreateRelationshipDialog, setFilterText]);
|
||||||
|
|
||||||
|
const operatingSystem = useMemo(() => getOperatingSystem(), []);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
operatingSystem === 'mac' ? 'meta+f' : 'ctrl+f',
|
||||||
|
() => {
|
||||||
|
filterInputRef.current?.focus();
|
||||||
|
},
|
||||||
|
{
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[filterInputRef]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-1 flex-col overflow-hidden px-2">
|
<section className="flex flex-1 flex-col overflow-hidden px-2">
|
||||||
<div className="flex items-center justify-between gap-4 py-1">
|
<div className="flex items-center justify-between gap-4 py-1">
|
||||||
@@ -69,6 +85,7 @@ export const RelationshipsSection: React.FC<RelationshipsSectionProps> = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Input
|
<Input
|
||||||
|
ref={filterInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
'side_panel.relationships_section.filter'
|
'side_panel.relationships_section.filter'
|
||||||
|
|||||||
@@ -18,12 +18,15 @@ import { useChartDB } from '@/hooks/use-chartdb';
|
|||||||
import { DependenciesSection } from './dependencies-section/dependencies-section';
|
import { DependenciesSection } from './dependencies-section/dependencies-section';
|
||||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
||||||
import { AreasSection } from './areas-section/areas-section';
|
import { AreasSection } from './areas-section/areas-section';
|
||||||
|
import { CustomTypesSection } from './custom-types-section/custom-types-section';
|
||||||
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
|
||||||
export interface SidePanelProps {}
|
export interface SidePanelProps {}
|
||||||
|
|
||||||
export const SidePanel: React.FC<SidePanelProps> = () => {
|
export const SidePanel: React.FC<SidePanelProps> = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { schemas, filterSchemas, filteredSchemas } = useChartDB();
|
const { schemas, filterSchemas, filteredSchemas, databaseType } =
|
||||||
|
useChartDB();
|
||||||
const {
|
const {
|
||||||
selectSidebarSection,
|
selectSidebarSection,
|
||||||
selectedSidebarSection,
|
selectedSidebarSection,
|
||||||
@@ -117,6 +120,13 @@ export const SidePanel: React.FC<SidePanelProps> = () => {
|
|||||||
<SelectItem value="areas">
|
<SelectItem value="areas">
|
||||||
{t('side_panel.areas_section.areas')}
|
{t('side_panel.areas_section.areas')}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
{databaseType === DatabaseType.POSTGRESQL ? (
|
||||||
|
<SelectItem value="customTypes">
|
||||||
|
{t(
|
||||||
|
'side_panel.custom_types_section.custom_types'
|
||||||
|
)}
|
||||||
|
</SelectItem>
|
||||||
|
) : null}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -128,8 +138,10 @@ export const SidePanel: React.FC<SidePanelProps> = () => {
|
|||||||
<RelationshipsSection />
|
<RelationshipsSection />
|
||||||
) : selectedSidebarSection === 'dependencies' ? (
|
) : selectedSidebarSection === 'dependencies' ? (
|
||||||
<DependenciesSection />
|
<DependenciesSection />
|
||||||
) : (
|
) : selectedSidebarSection === 'areas' ? (
|
||||||
<AreasSection />
|
<AreasSection />
|
||||||
|
) : (
|
||||||
|
<CustomTypesSection />
|
||||||
)}
|
)}
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,11 +12,41 @@ import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-lang
|
|||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
import { ArrowLeftRight } from 'lucide-react';
|
import { ArrowLeftRight } from 'lucide-react';
|
||||||
import { type DBField } from '@/lib/domain/db-field';
|
import { type DBField } from '@/lib/domain/db-field';
|
||||||
|
import type { DBCustomType } from '@/lib/domain/db-custom-type';
|
||||||
|
import { DBCustomTypeKind } from '@/lib/domain/db-custom-type';
|
||||||
|
|
||||||
export interface TableDBMLProps {
|
export interface TableDBMLProps {
|
||||||
filteredTables: DBTable[];
|
filteredTables: DBTable[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use DBCustomType for generating Enum DBML
|
||||||
|
const generateEnumsDBML = (customTypes: DBCustomType[] | undefined): string => {
|
||||||
|
if (!customTypes || customTypes.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter for enum types and map them
|
||||||
|
return customTypes
|
||||||
|
.filter((ct) => ct.kind === DBCustomTypeKind.enum)
|
||||||
|
.map((enumDef) => {
|
||||||
|
const enumIdentifier = enumDef.schema
|
||||||
|
? `"${enumDef.schema}"."${enumDef.name.replace(/"/g, '\\"')}"`
|
||||||
|
: `"${enumDef.name.replace(/"/g, '\\"')}"`;
|
||||||
|
|
||||||
|
const valuesString = (enumDef.values || []) // Ensure values array exists
|
||||||
|
.map((valueName) => {
|
||||||
|
// valueName is a string as per DBCustomType
|
||||||
|
const valLine = ` "${valueName.replace(/"/g, '\\"')}"`;
|
||||||
|
// If you have notes per enum value, you'd need to adjust DBCustomType
|
||||||
|
// For now, assuming no notes per value in DBCustomType
|
||||||
|
return valLine;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
return `Enum ${enumIdentifier} {\n${valuesString}\n}\n`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
};
|
||||||
|
|
||||||
const getEditorTheme = (theme: EffectiveTheme) => {
|
const getEditorTheme = (theme: EffectiveTheme) => {
|
||||||
return theme === 'dark' ? 'dbml-dark' : 'dbml-light';
|
return theme === 'dark' ? 'dbml-dark' : 'dbml-light';
|
||||||
};
|
};
|
||||||
@@ -33,6 +63,7 @@ const databaseTypeToImportFormat = (
|
|||||||
case DatabaseType.POSTGRESQL:
|
case DatabaseType.POSTGRESQL:
|
||||||
case DatabaseType.COCKROACHDB:
|
case DatabaseType.COCKROACHDB:
|
||||||
case DatabaseType.SQLITE:
|
case DatabaseType.SQLITE:
|
||||||
|
case DatabaseType.ORACLE:
|
||||||
return 'postgres';
|
return 'postgres';
|
||||||
default:
|
default:
|
||||||
return 'postgres';
|
return 'postgres';
|
||||||
@@ -140,6 +171,19 @@ const sanitizeSQLforDBML = (sql: string): string => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Comment out self-referencing foreign keys to prevent "Two endpoints are the same" error
|
||||||
|
// Example: ALTER TABLE public.class ADD CONSTRAINT ... FOREIGN KEY (class_id) REFERENCES public.class (class_id);
|
||||||
|
const lines = sanitized.split('\n');
|
||||||
|
const processedLines = lines.map((line) => {
|
||||||
|
const selfRefFKPattern =
|
||||||
|
/ALTER\s+TABLE\s+(?:\S+\.)?(\S+)\s+ADD\s+CONSTRAINT\s+\S+\s+FOREIGN\s+KEY\s*\([^)]+\)\s+REFERENCES\s+(?:\S+\.)?\1\s*\([^)]+\)\s*;/i;
|
||||||
|
if (selfRefFKPattern.test(line)) {
|
||||||
|
return `-- ${line}`; // Comment out the line
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
sanitized = processedLines.join('\n');
|
||||||
|
|
||||||
// Replace any remaining problematic characters
|
// Replace any remaining problematic characters
|
||||||
sanitized = sanitized.replace(/\?\?/g, '__');
|
sanitized = sanitized.replace(/\?\?/g, '__');
|
||||||
|
|
||||||
@@ -286,7 +330,7 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
|
|||||||
const { effectiveTheme } = useTheme();
|
const { effectiveTheme } = useTheme();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [dbmlFormat, setDbmlFormat] = useState<'inline' | 'standard'>(
|
const [dbmlFormat, setDbmlFormat] = useState<'inline' | 'standard'>(
|
||||||
'standard'
|
'inline'
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Effect for handling empty field name warnings ---
|
// --- Effect for handling empty field name warnings ---
|
||||||
@@ -438,6 +482,9 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
|
|||||||
let inline = '';
|
let inline = '';
|
||||||
let baseScript = ''; // Define baseScript outside try
|
let baseScript = ''; // Define baseScript outside try
|
||||||
|
|
||||||
|
// Use finalDiagramForExport.customTypes which should be DBCustomType[]
|
||||||
|
const enumsDBML = generateEnumsDBML(finalDiagramForExport.customTypes);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
baseScript = exportBaseSQL({
|
baseScript = exportBaseSQL({
|
||||||
diagram: finalDiagramForExport, // Use final diagram
|
diagram: finalDiagramForExport, // Use final diagram
|
||||||
@@ -450,13 +497,27 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
|
|||||||
// Append COMMENTS for tables renamed due to SQL keywords
|
// Append COMMENTS for tables renamed due to SQL keywords
|
||||||
sqlRenamedTables.forEach((originalName, newName) => {
|
sqlRenamedTables.forEach((originalName, newName) => {
|
||||||
const escapedOriginal = originalName.replace(/'/g, "\\'");
|
const escapedOriginal = originalName.replace(/'/g, "\\'");
|
||||||
baseScript += `\nCOMMENT ON TABLE "${newName}" IS 'Original name was "${escapedOriginal}" (renamed due to SQL keyword conflict).';`;
|
// Find the table to get its schema
|
||||||
|
const table = finalDiagramForExport.tables?.find(
|
||||||
|
(t) => t.name === newName
|
||||||
|
);
|
||||||
|
const tableIdentifier = table?.schema
|
||||||
|
? `"${table.schema}"."${newName}"`
|
||||||
|
: `"${newName}"`;
|
||||||
|
baseScript += `\nCOMMENT ON TABLE ${tableIdentifier} IS 'Original name was "${escapedOriginal}" (renamed due to SQL keyword conflict).';`;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Append COMMENTS for fields renamed due to SQL keyword conflicts
|
// Append COMMENTS for fields renamed due to SQL keyword conflicts
|
||||||
fieldRenames.forEach(({ table, originalName, newName }) => {
|
fieldRenames.forEach(({ table, originalName, newName }) => {
|
||||||
const escapedOriginal = originalName.replace(/'/g, "\\'");
|
const escapedOriginal = originalName.replace(/'/g, "\\'");
|
||||||
baseScript += `\nCOMMENT ON COLUMN "${table}"."${newName}" IS 'Original name was "${escapedOriginal}" (renamed due to SQL keyword conflict).';`;
|
// Find the table to get its schema
|
||||||
|
const tableObj = finalDiagramForExport.tables?.find(
|
||||||
|
(t) => t.name === table
|
||||||
|
);
|
||||||
|
const tableIdentifier = tableObj?.schema
|
||||||
|
? `"${tableObj.schema}"."${table}"`
|
||||||
|
: `"${table}"`;
|
||||||
|
baseScript += `\nCOMMENT ON COLUMN ${tableIdentifier}."${newName}" IS 'Original name was "${escapedOriginal}" (renamed due to SQL keyword conflict).';`;
|
||||||
});
|
});
|
||||||
|
|
||||||
standard = normalizeCharTypeFormat(
|
standard = normalizeCharTypeFormat(
|
||||||
@@ -466,6 +527,9 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Prepend Enum DBML to the standard output
|
||||||
|
standard = enumsDBML + '\n' + standard;
|
||||||
|
|
||||||
inline = normalizeCharTypeFormat(convertToInlineRefs(standard));
|
inline = normalizeCharTypeFormat(convertToInlineRefs(standard));
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -494,6 +558,15 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
|
|||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If an error occurred, still prepend enums if they exist, or they'll be lost.
|
||||||
|
// The error message will then follow.
|
||||||
|
if (standard.startsWith('// Error generating DBML:')) {
|
||||||
|
standard = enumsDBML + standard;
|
||||||
|
}
|
||||||
|
if (inline.startsWith('// Error generating DBML:')) {
|
||||||
|
inline = enumsDBML + inline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { standardDbml: standard, inlineDbml: inline };
|
return { standardDbml: standard, inlineDbml: inline };
|
||||||
}, [currentDiagram, filteredTables, toast]); // Keep toast dependency for now, although direct call is removed
|
}, [currentDiagram, filteredTables, toast]); // Keep toast dependency for now, although direct call is removed
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [localField, setLocalField] = React.useState<DBField>(field);
|
const [localField, setLocalField] = React.useState<DBField>(field);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLocalField(field);
|
||||||
|
}, [field]);
|
||||||
|
|
||||||
const debouncedUpdateFieldRef = useRef<((value?: DBField) => void) | null>(
|
const debouncedUpdateFieldRef = useRef<((value?: DBField) => void) | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
@@ -49,11 +53,17 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
|
|||||||
};
|
};
|
||||||
}, [updateField]);
|
}, [updateField]);
|
||||||
|
|
||||||
|
const prevFieldRef = useRef<DBField>(field);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (debouncedUpdateFieldRef.current && !equal(field, localField)) {
|
if (
|
||||||
|
debouncedUpdateFieldRef.current &&
|
||||||
|
!equal(prevFieldRef.current, localField)
|
||||||
|
) {
|
||||||
debouncedUpdateFieldRef.current(localField);
|
debouncedUpdateFieldRef.current(localField);
|
||||||
}
|
}
|
||||||
}, [localField, field]);
|
prevFieldRef.current = localField;
|
||||||
|
}, [localField]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export const TableField: React.FC<TableFieldProps> = ({
|
|||||||
<span>
|
<span>
|
||||||
<SelectBox
|
<SelectBox
|
||||||
className="flex h-8 min-h-8 w-full"
|
className="flex h-8 min-h-8 w-full"
|
||||||
|
popoverClassName="min-w-[350px]"
|
||||||
options={dataFieldOptions}
|
options={dataFieldOptions}
|
||||||
placeholder={t(
|
placeholder={t(
|
||||||
'side_panel.tables_section.table.field_type'
|
'side_panel.tables_section.table.field_type'
|
||||||
|
|||||||
@@ -233,6 +233,15 @@ export const Menu: React.FC<MenuProps> = () => {
|
|||||||
>
|
>
|
||||||
{databaseTypeToLabelMap['sqlite']}
|
{databaseTypeToLabelMap['sqlite']}
|
||||||
</MenubarItem>
|
</MenubarItem>
|
||||||
|
<MenubarItem
|
||||||
|
onClick={() =>
|
||||||
|
openImportDatabaseDialog({
|
||||||
|
databaseType: DatabaseType.ORACLE,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{databaseTypeToLabelMap['oracle']}
|
||||||
|
</MenubarItem>
|
||||||
</MenubarSubContent>
|
</MenubarSubContent>
|
||||||
</MenubarSub>
|
</MenubarSub>
|
||||||
<MenubarSeparator />
|
<MenubarSeparator />
|
||||||
|
|||||||
Reference in New Issue
Block a user