mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-11-03 21:43:23 +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
 | 
			
		||||
 | 
			
		||||
## [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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,12 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "chartdb",
 | 
			
		||||
    "version": "1.12.0",
 | 
			
		||||
    "version": "1.13.0",
 | 
			
		||||
    "lockfileVersion": 3,
 | 
			
		||||
    "requires": true,
 | 
			
		||||
    "packages": {
 | 
			
		||||
        "": {
 | 
			
		||||
            "name": "chartdb",
 | 
			
		||||
            "version": "1.12.0",
 | 
			
		||||
            "version": "1.13.0",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@ai-sdk/openai": "^0.0.51",
 | 
			
		||||
                "@dbml/core": "^3.9.5",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "chartdb",
 | 
			
		||||
    "private": true,
 | 
			
		||||
    "version": "1.12.0",
 | 
			
		||||
    "version": "1.13.0",
 | 
			
		||||
    "type": "module",
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "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>
 | 
			
		||||
                <img
 | 
			
		||||
                    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"
 | 
			
		||||
                    onClick={onClick}
 | 
			
		||||
                />
 | 
			
		||||
@@ -42,7 +42,7 @@ export const DiagramIcon = React.forwardRef<
 | 
			
		||||
            <TooltipTrigger className={cn('mr-2', className)} ref={ref} asChild>
 | 
			
		||||
                <img
 | 
			
		||||
                    src={databaseSecondaryLogoMap[databaseType]}
 | 
			
		||||
                    className={cn('h-5 max-w-fit', imgClassName)}
 | 
			
		||||
                    className={cn('max-h-5 max-w-5', imgClassName)}
 | 
			
		||||
                    alt="database"
 | 
			
		||||
                    onClick={onClick}
 | 
			
		||||
                />
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ export interface SelectBoxOption {
 | 
			
		||||
    description?: string;
 | 
			
		||||
    regex?: string;
 | 
			
		||||
    extractRegex?: RegExp;
 | 
			
		||||
    group?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SelectBoxProps {
 | 
			
		||||
@@ -51,6 +52,7 @@ export interface SelectBoxProps {
 | 
			
		||||
    disabled?: boolean;
 | 
			
		||||
    open?: boolean;
 | 
			
		||||
    onOpenChange?: (open: boolean) => void;
 | 
			
		||||
    popoverClassName?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
@@ -75,6 +77,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
            disabled,
 | 
			
		||||
            open,
 | 
			
		||||
            onOpenChange: setOpen,
 | 
			
		||||
            popoverClassName,
 | 
			
		||||
        },
 | 
			
		||||
        ref
 | 
			
		||||
    ) => {
 | 
			
		||||
@@ -175,6 +178,101 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
            [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 (
 | 
			
		||||
            <Popover open={isOpen} onOpenChange={onOpenChange} modal={true}>
 | 
			
		||||
                <PopoverTrigger asChild tabIndex={0} onKeyDown={handleKeyDown}>
 | 
			
		||||
@@ -245,7 +343,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                    </div>
 | 
			
		||||
                </PopoverTrigger>
 | 
			
		||||
                <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"
 | 
			
		||||
                >
 | 
			
		||||
                    <Command
 | 
			
		||||
@@ -319,91 +420,23 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                            <div className="max-h-64 w-full">
 | 
			
		||||
                                <CommandGroup>
 | 
			
		||||
                                    <CommandList className="max-h-fit w-full">
 | 
			
		||||
                                        {options.map((option) => {
 | 
			
		||||
                                            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
 | 
			
		||||
                                        {hasGroups
 | 
			
		||||
                                            ? Object.entries(groups).map(
 | 
			
		||||
                                                  ([
 | 
			
		||||
                                                      groupName,
 | 
			
		||||
                                                      groupOptions,
 | 
			
		||||
                                                  ]) => (
 | 
			
		||||
                                                      <CommandGroup
 | 
			
		||||
                                                          key={groupName}
 | 
			
		||||
                                                          heading={groupName}
 | 
			
		||||
                                                      >
 | 
			
		||||
                                                          {groupOptions.map(
 | 
			
		||||
                                                              renderOption
 | 
			
		||||
                                                          )}
 | 
			
		||||
                                                      </CommandGroup>
 | 
			
		||||
                                                  )
 | 
			
		||||
                                                : 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 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>
 | 
			
		||||
                                            );
 | 
			
		||||
                                        })}
 | 
			
		||||
                                              )
 | 
			
		||||
                                            : options.map(renderOption)}
 | 
			
		||||
                                    </CommandList>
 | 
			
		||||
                                </CommandGroup>
 | 
			
		||||
                            </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import type { DBSchema } from '@/lib/domain/db-schema';
 | 
			
		||||
import type { DBDependency } from '@/lib/domain/db-dependency';
 | 
			
		||||
import { EventEmitter } from 'ahooks/lib/useEventEmitter';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
 | 
			
		||||
 | 
			
		||||
export type ChartDBEventType =
 | 
			
		||||
    | 'add_tables'
 | 
			
		||||
@@ -72,6 +73,7 @@ export interface ChartDBContext {
 | 
			
		||||
    relationships: DBRelationship[];
 | 
			
		||||
    dependencies: DBDependency[];
 | 
			
		||||
    areas: Area[];
 | 
			
		||||
    customTypes: DBCustomType[];
 | 
			
		||||
    currentDiagram: Diagram;
 | 
			
		||||
    events: EventEmitter<ChartDBEvent>;
 | 
			
		||||
    readonly?: boolean;
 | 
			
		||||
@@ -248,6 +250,33 @@ export interface ChartDBContext {
 | 
			
		||||
        area: Partial<Area>,
 | 
			
		||||
        options?: { updateHistory: boolean }
 | 
			
		||||
    ) => 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>({
 | 
			
		||||
@@ -258,6 +287,7 @@ export const chartDBContext = createContext<ChartDBContext>({
 | 
			
		||||
    relationships: [],
 | 
			
		||||
    dependencies: [],
 | 
			
		||||
    areas: [],
 | 
			
		||||
    customTypes: [],
 | 
			
		||||
    schemas: [],
 | 
			
		||||
    filteredSchemas: [],
 | 
			
		||||
    filterSchemas: emptyFn,
 | 
			
		||||
@@ -333,4 +363,13 @@ export const chartDBContext = createContext<ChartDBContext>({
 | 
			
		||||
    removeArea: emptyFn,
 | 
			
		||||
    removeAreas: 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 { useDiff } from '../diff-context/use-diff';
 | 
			
		||||
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
 | 
			
		||||
import {
 | 
			
		||||
    DBCustomTypeKind,
 | 
			
		||||
    type DBCustomType,
 | 
			
		||||
} from '@/lib/domain/db-custom-type';
 | 
			
		||||
 | 
			
		||||
export interface ChartDBProviderProps {
 | 
			
		||||
    diagram?: Diagram;
 | 
			
		||||
@@ -58,6 +62,9 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
        diagram?.dependencies ?? []
 | 
			
		||||
    );
 | 
			
		||||
    const [areas, setAreas] = useState<Area[]>(diagram?.areas ?? []);
 | 
			
		||||
    const [customTypes, setCustomTypes] = useState<DBCustomType[]>(
 | 
			
		||||
        diagram?.customTypes ?? []
 | 
			
		||||
    );
 | 
			
		||||
    const { events: diffEvents } = useDiff();
 | 
			
		||||
 | 
			
		||||
    const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
 | 
			
		||||
@@ -155,6 +162,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            relationships,
 | 
			
		||||
            dependencies,
 | 
			
		||||
            areas,
 | 
			
		||||
            customTypes,
 | 
			
		||||
        }),
 | 
			
		||||
        [
 | 
			
		||||
            diagramId,
 | 
			
		||||
@@ -165,6 +173,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            relationships,
 | 
			
		||||
            dependencies,
 | 
			
		||||
            areas,
 | 
			
		||||
            customTypes,
 | 
			
		||||
            diagramCreatedAt,
 | 
			
		||||
            diagramUpdatedAt,
 | 
			
		||||
        ]
 | 
			
		||||
@@ -177,6 +186,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            setRelationships([]);
 | 
			
		||||
            setDependencies([]);
 | 
			
		||||
            setAreas([]);
 | 
			
		||||
            setCustomTypes([]);
 | 
			
		||||
            setDiagramUpdatedAt(updatedAt);
 | 
			
		||||
 | 
			
		||||
            resetRedoStack();
 | 
			
		||||
@@ -188,6 +198,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                db.deleteDiagramRelationships(diagramId),
 | 
			
		||||
                db.deleteDiagramDependencies(diagramId),
 | 
			
		||||
                db.deleteDiagramAreas(diagramId),
 | 
			
		||||
                db.deleteDiagramCustomTypes(diagramId),
 | 
			
		||||
            ]);
 | 
			
		||||
        }, [db, diagramId, resetRedoStack, resetUndoStack]);
 | 
			
		||||
 | 
			
		||||
@@ -201,6 +212,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
            setRelationships([]);
 | 
			
		||||
            setDependencies([]);
 | 
			
		||||
            setAreas([]);
 | 
			
		||||
            setCustomTypes([]);
 | 
			
		||||
            resetRedoStack();
 | 
			
		||||
            resetUndoStack();
 | 
			
		||||
 | 
			
		||||
@@ -210,6 +222,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                db.deleteDiagram(diagramId),
 | 
			
		||||
                db.deleteDiagramDependencies(diagramId),
 | 
			
		||||
                db.deleteDiagramAreas(diagramId),
 | 
			
		||||
                db.deleteDiagramCustomTypes(diagramId),
 | 
			
		||||
            ]);
 | 
			
		||||
        }, [db, diagramId, resetRedoStack, resetUndoStack]);
 | 
			
		||||
 | 
			
		||||
@@ -1506,6 +1519,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                setRelationships(diagram?.relationships ?? []);
 | 
			
		||||
                setDependencies(diagram?.dependencies ?? []);
 | 
			
		||||
                setAreas(diagram?.areas ?? []);
 | 
			
		||||
                setCustomTypes(diagram?.customTypes ?? []);
 | 
			
		||||
                setDiagramCreatedAt(diagram.createdAt);
 | 
			
		||||
                setDiagramUpdatedAt(diagram.updatedAt);
 | 
			
		||||
 | 
			
		||||
@@ -1520,6 +1534,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                setRelationships,
 | 
			
		||||
                setDependencies,
 | 
			
		||||
                setAreas,
 | 
			
		||||
                setCustomTypes,
 | 
			
		||||
                setDiagramCreatedAt,
 | 
			
		||||
                setDiagramUpdatedAt,
 | 
			
		||||
                events,
 | 
			
		||||
@@ -1533,6 +1548,7 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                includeTables: true,
 | 
			
		||||
                includeDependencies: true,
 | 
			
		||||
                includeAreas: true,
 | 
			
		||||
                includeCustomTypes: true,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diagram) {
 | 
			
		||||
@@ -1544,6 +1560,150 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
        [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 (
 | 
			
		||||
        <chartDBContext.Provider
 | 
			
		||||
            value={{
 | 
			
		||||
@@ -1608,6 +1768,14 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
                removeArea,
 | 
			
		||||
                removeAreas,
 | 
			
		||||
                updateArea,
 | 
			
		||||
                customTypes,
 | 
			
		||||
                createCustomType,
 | 
			
		||||
                addCustomType,
 | 
			
		||||
                addCustomTypes,
 | 
			
		||||
                getCustomType,
 | 
			
		||||
                removeCustomType,
 | 
			
		||||
                removeCustomTypes,
 | 
			
		||||
                updateCustomType,
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        addAreas,
 | 
			
		||||
        removeAreas,
 | 
			
		||||
        updateArea,
 | 
			
		||||
        addCustomTypes,
 | 
			
		||||
        removeCustomTypes,
 | 
			
		||||
        updateCustomType,
 | 
			
		||||
    } = useChartDB();
 | 
			
		||||
 | 
			
		||||
    const redoActionHandlers = useMemo(
 | 
			
		||||
@@ -119,6 +122,19 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            updateArea: ({ redoData: { areaId, area } }) => {
 | 
			
		||||
                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,
 | 
			
		||||
@@ -141,6 +157,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            addAreas,
 | 
			
		||||
            removeAreas,
 | 
			
		||||
            updateArea,
 | 
			
		||||
            addCustomTypes,
 | 
			
		||||
            removeCustomTypes,
 | 
			
		||||
            updateCustomType,
 | 
			
		||||
        ]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@@ -239,6 +258,19 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            updateArea: ({ undoData: { areaId, area } }) => {
 | 
			
		||||
                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,
 | 
			
		||||
@@ -261,6 +293,9 @@ export const HistoryProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            addAreas,
 | 
			
		||||
            removeAreas,
 | 
			
		||||
            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 { DBDependency } from '@/lib/domain/db-dependency';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
 | 
			
		||||
 | 
			
		||||
type Action = keyof ChartDBContext;
 | 
			
		||||
 | 
			
		||||
@@ -142,6 +143,24 @@ type RedoUndoActionRemoveAreas = RedoUndoActionBase<
 | 
			
		||||
    { 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 =
 | 
			
		||||
    | RedoUndoActionAddTables
 | 
			
		||||
    | RedoUndoActionRemoveTables
 | 
			
		||||
@@ -162,7 +181,10 @@ export type RedoUndoAction =
 | 
			
		||||
    | RedoUndoActionRemoveDependencies
 | 
			
		||||
    | RedoUndoActionAddAreas
 | 
			
		||||
    | RedoUndoActionUpdateArea
 | 
			
		||||
    | RedoUndoActionRemoveAreas;
 | 
			
		||||
    | RedoUndoActionRemoveAreas
 | 
			
		||||
    | RedoUndoActionAddCustomTypes
 | 
			
		||||
    | RedoUndoActionUpdateCustomType
 | 
			
		||||
    | RedoUndoActionRemoveCustomTypes;
 | 
			
		||||
 | 
			
		||||
export type RedoActionData<T extends Action> = Extract<
 | 
			
		||||
    RedoUndoAction,
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,8 @@ export type SidebarSection =
 | 
			
		||||
    | 'tables'
 | 
			
		||||
    | 'relationships'
 | 
			
		||||
    | 'dependencies'
 | 
			
		||||
    | 'areas';
 | 
			
		||||
    | 'areas'
 | 
			
		||||
    | 'customTypes';
 | 
			
		||||
 | 
			
		||||
export interface LayoutContext {
 | 
			
		||||
    openedTableInSidebar: string | undefined;
 | 
			
		||||
@@ -24,6 +25,10 @@ export interface LayoutContext {
 | 
			
		||||
    openAreaFromSidebar: (areaId: string) => void;
 | 
			
		||||
    closeAllAreasInSidebar: () => void;
 | 
			
		||||
 | 
			
		||||
    openedCustomTypeInSidebar: string | undefined;
 | 
			
		||||
    openCustomTypeFromSidebar: (customTypeId: string) => void;
 | 
			
		||||
    closeAllCustomTypesInSidebar: () => void;
 | 
			
		||||
 | 
			
		||||
    selectedSidebarSection: SidebarSection;
 | 
			
		||||
    selectSidebarSection: (section: SidebarSection) => void;
 | 
			
		||||
 | 
			
		||||
@@ -53,6 +58,10 @@ export const layoutContext = createContext<LayoutContext>({
 | 
			
		||||
    openAreaFromSidebar: emptyFn,
 | 
			
		||||
    closeAllAreasInSidebar: emptyFn,
 | 
			
		||||
 | 
			
		||||
    openedCustomTypeInSidebar: undefined,
 | 
			
		||||
    openCustomTypeFromSidebar: emptyFn,
 | 
			
		||||
    closeAllCustomTypesInSidebar: emptyFn,
 | 
			
		||||
 | 
			
		||||
    selectSidebarSection: emptyFn,
 | 
			
		||||
    openTableFromSidebar: emptyFn,
 | 
			
		||||
    closeAllTablesInSidebar: emptyFn,
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,8 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    const [openedAreaInSidebar, setOpenedAreaInSidebar] = React.useState<
 | 
			
		||||
        string | undefined
 | 
			
		||||
    >();
 | 
			
		||||
    const [openedCustomTypeInSidebar, setOpenedCustomTypeInSidebar] =
 | 
			
		||||
        React.useState<string | undefined>();
 | 
			
		||||
    const [selectedSidebarSection, setSelectedSidebarSection] =
 | 
			
		||||
        React.useState<SidebarSection>('tables');
 | 
			
		||||
    const [isSidePanelShowed, setIsSidePanelShowed] =
 | 
			
		||||
@@ -36,6 +38,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    const closeAllAreasInSidebar: LayoutContext['closeAllAreasInSidebar'] =
 | 
			
		||||
        () => setOpenedAreaInSidebar('');
 | 
			
		||||
 | 
			
		||||
    const closeAllCustomTypesInSidebar: LayoutContext['closeAllCustomTypesInSidebar'] =
 | 
			
		||||
        () => setOpenedCustomTypeInSidebar('');
 | 
			
		||||
 | 
			
		||||
    const hideSidePanel: LayoutContext['hideSidePanel'] = () =>
 | 
			
		||||
        setIsSidePanelShowed(false);
 | 
			
		||||
 | 
			
		||||
@@ -76,6 +81,13 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        setOpenedAreaInSidebar(areaId);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const openCustomTypeFromSidebar: LayoutContext['openCustomTypeFromSidebar'] =
 | 
			
		||||
        (customTypeId) => {
 | 
			
		||||
            showSidePanel();
 | 
			
		||||
            setSelectedSidebarSection('customTypes');
 | 
			
		||||
            setOpenedTableInSidebar(customTypeId);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    const openSelectSchema: LayoutContext['openSelectSchema'] = () =>
 | 
			
		||||
        setIsSelectSchemaOpen(true);
 | 
			
		||||
 | 
			
		||||
@@ -105,6 +117,9 @@ export const LayoutProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                openedAreaInSidebar,
 | 
			
		||||
                openAreaFromSidebar,
 | 
			
		||||
                closeAllAreasInSidebar,
 | 
			
		||||
                openedCustomTypeInSidebar,
 | 
			
		||||
                openCustomTypeFromSidebar,
 | 
			
		||||
                closeAllCustomTypesInSidebar,
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import type { ChartDBConfig } from '@/lib/domain/config';
 | 
			
		||||
import type { DBDependency } from '@/lib/domain/db-dependency';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
 | 
			
		||||
 | 
			
		||||
export interface StorageContext {
 | 
			
		||||
    // Config operations
 | 
			
		||||
@@ -19,6 +20,7 @@ export interface StorageContext {
 | 
			
		||||
        includeRelationships?: boolean;
 | 
			
		||||
        includeDependencies?: boolean;
 | 
			
		||||
        includeAreas?: boolean;
 | 
			
		||||
        includeCustomTypes?: boolean;
 | 
			
		||||
    }) => Promise<Diagram[]>;
 | 
			
		||||
    getDiagram: (
 | 
			
		||||
        id: string,
 | 
			
		||||
@@ -27,6 +29,7 @@ export interface StorageContext {
 | 
			
		||||
            includeRelationships?: boolean;
 | 
			
		||||
            includeDependencies?: boolean;
 | 
			
		||||
            includeAreas?: boolean;
 | 
			
		||||
            includeCustomTypes?: boolean;
 | 
			
		||||
        }
 | 
			
		||||
    ) => Promise<Diagram | undefined>;
 | 
			
		||||
    updateDiagram: (params: {
 | 
			
		||||
@@ -103,6 +106,26 @@ export interface StorageContext {
 | 
			
		||||
    deleteArea: (params: { diagramId: string; id: string }) => Promise<void>;
 | 
			
		||||
    listAreas: (diagramId: string) => Promise<Area[]>;
 | 
			
		||||
    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 = {
 | 
			
		||||
@@ -143,6 +166,14 @@ export const storageInitialValue: StorageContext = {
 | 
			
		||||
    deleteArea: emptyFn,
 | 
			
		||||
    listAreas: emptyFn,
 | 
			
		||||
    deleteDiagramAreas: emptyFn,
 | 
			
		||||
 | 
			
		||||
    // Custom type operations
 | 
			
		||||
    addCustomType: emptyFn,
 | 
			
		||||
    getCustomType: emptyFn,
 | 
			
		||||
    updateCustomType: emptyFn,
 | 
			
		||||
    deleteCustomType: emptyFn,
 | 
			
		||||
    listCustomTypes: emptyFn,
 | 
			
		||||
    deleteDiagramCustomTypes: emptyFn,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const storageContext =
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import { determineCardinalities } from '@/lib/domain/db-relationship';
 | 
			
		||||
import type { ChartDBConfig } from '@/lib/domain/config';
 | 
			
		||||
import type { DBDependency } from '@/lib/domain/db-dependency';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import type { DBCustomType } from '@/lib/domain/db-custom-type';
 | 
			
		||||
 | 
			
		||||
export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
@@ -34,6 +35,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            Area & { diagramId: string },
 | 
			
		||||
            '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<
 | 
			
		||||
            ChartDBConfig & { id: number },
 | 
			
		||||
            'id' // primary key "id" (for the typings only)
 | 
			
		||||
@@ -166,6 +171,20 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        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 () => {
 | 
			
		||||
        const config = await getConfig();
 | 
			
		||||
 | 
			
		||||
@@ -232,6 +251,13 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            ...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);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@@ -241,11 +267,13 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            includeRelationships?: boolean;
 | 
			
		||||
            includeDependencies?: boolean;
 | 
			
		||||
            includeAreas?: boolean;
 | 
			
		||||
            includeCustomTypes?: boolean;
 | 
			
		||||
        } = {
 | 
			
		||||
            includeRelationships: false,
 | 
			
		||||
            includeTables: false,
 | 
			
		||||
            includeDependencies: false,
 | 
			
		||||
            includeAreas: false,
 | 
			
		||||
            includeCustomTypes: false,
 | 
			
		||||
        }
 | 
			
		||||
    ): Promise<Diagram[]> => {
 | 
			
		||||
        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;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@@ -296,11 +333,13 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            includeRelationships?: boolean;
 | 
			
		||||
            includeDependencies?: boolean;
 | 
			
		||||
            includeAreas?: boolean;
 | 
			
		||||
            includeCustomTypes?: boolean;
 | 
			
		||||
        } = {
 | 
			
		||||
            includeRelationships: false,
 | 
			
		||||
            includeTables: false,
 | 
			
		||||
            includeDependencies: false,
 | 
			
		||||
            includeAreas: false,
 | 
			
		||||
            includeCustomTypes: false,
 | 
			
		||||
        }
 | 
			
		||||
    ): Promise<Diagram | undefined> => {
 | 
			
		||||
        const diagram = await db.diagrams.get(id);
 | 
			
		||||
@@ -325,6 +364,10 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            diagram.areas = await listAreas(id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (options.includeCustomTypes) {
 | 
			
		||||
            diagram.customTypes = await listCustomTypes(id);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return diagram;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
@@ -351,6 +394,13 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                    .where('diagramId')
 | 
			
		||||
                    .equals(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_dependencies.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();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // 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 (
 | 
			
		||||
        <storageContext.Provider
 | 
			
		||||
            value={{
 | 
			
		||||
@@ -615,6 +731,12 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                deleteArea,
 | 
			
		||||
                listAreas,
 | 
			
		||||
                deleteDiagramAreas,
 | 
			
		||||
                addCustomType,
 | 
			
		||||
                getCustomType,
 | 
			
		||||
                updateCustomType,
 | 
			
		||||
                deleteCustomType,
 | 
			
		||||
                listCustomTypes,
 | 
			
		||||
                deleteDiagramCustomTypes,
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
 
 | 
			
		||||
@@ -366,7 +366,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
 | 
			
		||||
                {isDesktop ? (
 | 
			
		||||
                    <ResizablePanelGroup
 | 
			
		||||
                        direction={isDesktop ? 'horizontal' : 'vertical'}
 | 
			
		||||
                        className="min-h-[500px] md:min-h-fit"
 | 
			
		||||
                        className="min-h-[500px]"
 | 
			
		||||
                    >
 | 
			
		||||
                        <ResizablePanel
 | 
			
		||||
                            defaultSize={25}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ import { DDLInstructions } from './instructions/ddl-instructions';
 | 
			
		||||
 | 
			
		||||
const DatabasesWithoutDDLInstructions: DatabaseType[] = [
 | 
			
		||||
    DatabaseType.CLICKHOUSE,
 | 
			
		||||
    DatabaseType.ORACLE,
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
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.',
 | 
			
		||||
        },
 | 
			
		||||
    ],
 | 
			
		||||
    [DatabaseType.ORACLE]: [],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface DDLInstructionsProps {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ const SUPPORTED_DB_TYPES: DatabaseType[] = [
 | 
			
		||||
    DatabaseType.MARIADB,
 | 
			
		||||
    DatabaseType.SQLITE,
 | 
			
		||||
    DatabaseType.SQL_SERVER,
 | 
			
		||||
    DatabaseType.ORACLE,
 | 
			
		||||
    DatabaseType.COCKROACHDB,
 | 
			
		||||
    DatabaseType.CLICKHOUSE,
 | 
			
		||||
];
 | 
			
		||||
 
 | 
			
		||||
@@ -231,6 +231,33 @@ export const ar: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -232,6 +232,32 @@ export const bn: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -234,6 +234,32 @@ export const de: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -226,6 +226,32 @@ export const en = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -222,6 +222,32 @@ export const es: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -220,6 +220,32 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -233,6 +233,32 @@ export const gu: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -233,6 +233,32 @@ export const hi: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -231,6 +231,32 @@ export const id_ID: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -237,6 +237,32 @@ export const ja: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -231,6 +231,32 @@ export const ko_KR: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -236,6 +236,32 @@ export const mr: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -233,6 +233,32 @@ export const ne: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -232,6 +232,32 @@ export const pt_BR: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -229,6 +229,32 @@ export const ru: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -233,6 +233,32 @@ export const te: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -232,6 +232,32 @@ export const tr: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
            zoom_in: 'Yakınlaştır',
 | 
			
		||||
 
 | 
			
		||||
@@ -230,6 +230,32 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -231,6 +231,32 @@ export const vi: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -228,6 +228,32 @@ export const zh_CN: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -228,6 +228,32 @@ export const zh_TW: LanguageTranslation = {
 | 
			
		||||
                    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: {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ import { mysqlDataTypes } from './mysql-data-types';
 | 
			
		||||
import { postgresDataTypes } from './postgres-data-types';
 | 
			
		||||
import { sqlServerDataTypes } from './sql-server-data-types';
 | 
			
		||||
import { sqliteDataTypes } from './sqlite-data-types';
 | 
			
		||||
import { oracleDataTypes } from './oracle-data-types';
 | 
			
		||||
 | 
			
		||||
export interface DataType {
 | 
			
		||||
    id: string;
 | 
			
		||||
@@ -32,6 +33,7 @@ export const dataTypeMap: Record<DatabaseType, readonly DataTypeData[]> = {
 | 
			
		||||
    [DatabaseType.SQLITE]: sqliteDataTypes,
 | 
			
		||||
    [DatabaseType.CLICKHOUSE]: clickhouseDataTypes,
 | 
			
		||||
    [DatabaseType.COCKROACHDB]: postgresDataTypes,
 | 
			
		||||
    [DatabaseType.ORACLE]: oracleDataTypes,
 | 
			
		||||
} as const;
 | 
			
		||||
 | 
			
		||||
export const sortDataTypes = (dataTypes: DataTypeData[]): DataTypeData[] => {
 | 
			
		||||
@@ -71,6 +73,9 @@ export const sortedDataTypeMap: Record<DatabaseType, readonly DataTypeData[]> =
 | 
			
		||||
        [DatabaseType.COCKROACHDB]: sortDataTypes([
 | 
			
		||||
            ...dataTypeMap[DatabaseType.COCKROACHDB],
 | 
			
		||||
        ]),
 | 
			
		||||
        [DatabaseType.ORACLE]: sortDataTypes([
 | 
			
		||||
            ...dataTypeMap[DatabaseType.ORACLE],
 | 
			
		||||
        ]),
 | 
			
		||||
    } as const;
 | 
			
		||||
 | 
			
		||||
const compatibleTypes: Record<DatabaseType, Record<string, string[]>> = {
 | 
			
		||||
@@ -88,6 +93,7 @@ const compatibleTypes: Record<DatabaseType, Record<string, string[]>> = {
 | 
			
		||||
    [DatabaseType.SQLITE]: {},
 | 
			
		||||
    [DatabaseType.CLICKHOUSE]: {},
 | 
			
		||||
    [DatabaseType.COCKROACHDB]: {},
 | 
			
		||||
    [DatabaseType.ORACLE]: {},
 | 
			
		||||
    [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 { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
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 {
 | 
			
		||||
    if (!field.default || typeof field.default !== 'string') {
 | 
			
		||||
@@ -90,6 +92,57 @@ function mapPostgresType(typeName: string, fieldName: string): string {
 | 
			
		||||
    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 {
 | 
			
		||||
    if (!diagram.tables || !diagram.relationships) {
 | 
			
		||||
        return '';
 | 
			
		||||
@@ -97,6 +150,7 @@ export function exportPostgreSQL(diagram: Diagram): string {
 | 
			
		||||
 | 
			
		||||
    const tables = diagram.tables;
 | 
			
		||||
    const relationships = diagram.relationships;
 | 
			
		||||
    const customTypes = diagram.customTypes || [];
 | 
			
		||||
 | 
			
		||||
    // Create CREATE SCHEMA statements for all schemas
 | 
			
		||||
    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
 | 
			
		||||
    schemas.forEach((schema) => {
 | 
			
		||||
        sqlScript += `CREATE SCHEMA IF NOT EXISTS "${schema}";\n`;
 | 
			
		||||
    });
 | 
			
		||||
    sqlScript += '\n';
 | 
			
		||||
 | 
			
		||||
    // Add custom types (enums and composite types)
 | 
			
		||||
    sqlScript += exportCustomTypes(customTypes);
 | 
			
		||||
 | 
			
		||||
    // Add sequence creation statements
 | 
			
		||||
    const sequences = new Set<string>();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -84,7 +84,55 @@ export const exportBaseSQL = ({
 | 
			
		||||
    schemas.forEach((schema) => {
 | 
			
		||||
        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
 | 
			
		||||
    const sequences = new Set<string>();
 | 
			
		||||
@@ -119,8 +167,45 @@ export const exportBaseSQL = ({
 | 
			
		||||
            let typeName = simplifyDataType(field.type.name);
 | 
			
		||||
 | 
			
		||||
            // Handle ENUM type
 | 
			
		||||
            if (typeName.toLowerCase() === 'enum') {
 | 
			
		||||
                // Map enum to TEXT for broader compatibility, especially with DBML importer
 | 
			
		||||
            // If we are generating SQL for DBML flow, and we ALREADY generated CREATE TYPE for enums (e.g., for PG),
 | 
			
		||||
            // 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';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -129,11 +214,6 @@ export const exportBaseSQL = ({
 | 
			
		||||
                typeName = 'text[]';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Temp fix for 'user-defined' to be text
 | 
			
		||||
            if (typeName.toLowerCase() === 'user-defined') {
 | 
			
		||||
                typeName = 'text';
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            sqlScript += `  ${field.name} ${typeName}`;
 | 
			
		||||
 | 
			
		||||
            // 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\`.
 | 
			
		||||
        - **Conditional Statements**: Utilize PostgreSQL's support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
 | 
			
		||||
    `,
 | 
			
		||||
        oracle: '',
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    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 { TableInfoSchema, type TableInfo } from './table-info';
 | 
			
		||||
import { ViewInfoSchema, type ViewInfo } from './view-info';
 | 
			
		||||
import {
 | 
			
		||||
    DBCustomTypeInfoSchema,
 | 
			
		||||
    type DBCustomTypeInfo,
 | 
			
		||||
} from './custom-type-info';
 | 
			
		||||
 | 
			
		||||
export interface DatabaseMetadata {
 | 
			
		||||
    fk_info: ForeignKeyInfo[];
 | 
			
		||||
@@ -13,6 +17,7 @@ export interface DatabaseMetadata {
 | 
			
		||||
    indexes: IndexInfo[];
 | 
			
		||||
    tables: TableInfo[];
 | 
			
		||||
    views: ViewInfo[];
 | 
			
		||||
    custom_types?: DBCustomTypeInfo[];
 | 
			
		||||
    database_name: string;
 | 
			
		||||
    version: string;
 | 
			
		||||
}
 | 
			
		||||
@@ -24,6 +29,7 @@ export const DatabaseMetadataSchema: z.ZodType<DatabaseMetadata> = z.object({
 | 
			
		||||
    indexes: z.array(IndexInfoSchema),
 | 
			
		||||
    tables: z.array(TableInfoSchema),
 | 
			
		||||
    views: z.array(ViewInfoSchema),
 | 
			
		||||
    custom_types: z.array(DBCustomTypeInfoSchema).optional(),
 | 
			
		||||
    database_name: z.string(),
 | 
			
		||||
    version: z.string(),
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ cols AS (
 | 
			
		||||
               ',"ordinal_position":', toString(col_tuple.4),
 | 
			
		||||
               ',"type":"', col_tuple.5, '"',
 | 
			
		||||
               ',"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))), '}'),
 | 
			
		||||
        groupArray((
 | 
			
		||||
            col.database,
 | 
			
		||||
@@ -15,7 +15,7 @@ cols AS (
 | 
			
		||||
            col.name,
 | 
			
		||||
            col.position,
 | 
			
		||||
            col.type,
 | 
			
		||||
            col.default_kind,
 | 
			
		||||
            null as default_kind,
 | 
			
		||||
            col.default_expression,
 | 
			
		||||
            col.comment
 | 
			
		||||
        ))
 | 
			
		||||
 
 | 
			
		||||
@@ -124,7 +124,7 @@ cols AS (
 | 
			
		||||
                                                    ELSE 'null'
 | 
			
		||||
                                                END,
 | 
			
		||||
                                            ',"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, ''),
 | 
			
		||||
                                            '","comment":"', COALESCE(replace(replace(dsc.description::TEXT, '"', '\\"'), '\\x', '\\\\x'), ''),
 | 
			
		||||
                                            '"}')), ',') 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 (
 | 
			
		||||
  (SELECT (@fk_info:=NULL),
 | 
			
		||||
              (SELECT (0)
 | 
			
		||||
@@ -76,7 +80,7 @@ export const mariaDBQuery = `WITH fk_info as (
 | 
			
		||||
                    END,
 | 
			
		||||
                ',"ordinal_position":', cols.ordinal_position,
 | 
			
		||||
                ',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
 | 
			
		||||
                ',"default":"', IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', '\\"'), ''),
 | 
			
		||||
                ',"default":"', ${withExtras ? withDefault : withoutDefault},
 | 
			
		||||
                '","collation":"', IFNULL(cols.collation_name, ''), '"}'
 | 
			
		||||
            )))))
 | 
			
		||||
), indexes as (
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,11 @@ export const getMySQLQuery = (
 | 
			
		||||
    const databaseEdition: DatabaseEdition | undefined =
 | 
			
		||||
        options.databaseEdition;
 | 
			
		||||
 | 
			
		||||
    const withExtras = false;
 | 
			
		||||
 | 
			
		||||
    const withDefault = `IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', 'ֿֿֿ\\"'), '')`;
 | 
			
		||||
    const withoutDefault = `""`;
 | 
			
		||||
 | 
			
		||||
    const newMySQLQuery = `WITH fk_info as (
 | 
			
		||||
(SELECT (@fk_info:=NULL),
 | 
			
		||||
    (SELECT (0)
 | 
			
		||||
@@ -86,7 +91,7 @@ export const getMySQLQuery = (
 | 
			
		||||
                    END,
 | 
			
		||||
                ',"ordinal_position":', cols.ordinal_position,
 | 
			
		||||
                ',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
 | 
			
		||||
                ',"default":"', IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', 'ֿֿֿ\\"'), ''),
 | 
			
		||||
                ',"default":"', ${withExtras ? withDefault : withoutDefault},
 | 
			
		||||
                '","collation":"', IFNULL(cols.collation_name, ''), '"}'
 | 
			
		||||
            )))))
 | 
			
		||||
), indexes as (
 | 
			
		||||
@@ -211,7 +216,7 @@ export const getMySQLQuery = (
 | 
			
		||||
                         ',"scale":', IFNULL(cols.numeric_scale, 'null'), '}'), 'null'),
 | 
			
		||||
               ',"ordinal_position":', cols.ordinal_position,
 | 
			
		||||
               ',"nullable":', IF(cols.is_nullable = 'YES', 'true', 'false'),
 | 
			
		||||
               ',"default":"', IFNULL(REPLACE(REPLACE(cols.column_default, '\\\\', ''), '"', '\\"'), ''),
 | 
			
		||||
               ',"default":"', ${withExtras ? withDefault : withoutDefault},
 | 
			
		||||
               '","collation":"', IFNULL(cols.collation_name, ''), '"}')
 | 
			
		||||
    ) FROM (
 | 
			
		||||
        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_)'
 | 
			
		||||
    `;
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
    const query = `${`/* ${databaseEdition ? databaseEditionToLabelMap[databaseEdition] : 'PostgreSQL'} edition */`}
 | 
			
		||||
WITH fk_info${databaseEdition ? '_' + databaseEdition : ''} AS (
 | 
			
		||||
@@ -165,7 +173,7 @@ cols AS (
 | 
			
		||||
                                            '","table":"', cols.table_name,
 | 
			
		||||
                                            '","name":"', cols.column_name,
 | 
			
		||||
                                            '","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'),
 | 
			
		||||
                                            '","precision":',
 | 
			
		||||
                                                CASE
 | 
			
		||||
@@ -175,17 +183,21 @@ cols AS (
 | 
			
		||||
                                                    ELSE 'null'
 | 
			
		||||
                                                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, ''),
 | 
			
		||||
                                            '","comment":"', COALESCE(replace(replace(dsc.description, '"', '\\"'), '\\x', '\\\\x'), ''),
 | 
			
		||||
                                            '","comment":"', ${withExtras ? withComments : withoutComments},
 | 
			
		||||
                                            '"}')), ',') AS cols_metadata
 | 
			
		||||
    FROM information_schema.columns cols
 | 
			
		||||
    LEFT JOIN pg_catalog.pg_class c
 | 
			
		||||
        ON c.relname = cols.table_name
 | 
			
		||||
    JOIN pg_catalog.pg_namespace n
 | 
			
		||||
        ON n.oid = c.relnamespace AND n.nspname = cols.table_schema
 | 
			
		||||
    LEFT JOIN pg_catalog.pg_description dsc ON dsc.objoid = c.oid
 | 
			
		||||
                                        AND dsc.objsubid = cols.ordinal_position
 | 
			
		||||
    LEFT JOIN pg_catalog.pg_description dsc
 | 
			
		||||
        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')${
 | 
			
		||||
        databaseEdition === DatabaseEdition.POSTGRESQL_TIMESCALE
 | 
			
		||||
            ? timescaleColFilter
 | 
			
		||||
@@ -255,6 +267,62 @@ cols AS (
 | 
			
		||||
              ? 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, ''),
 | 
			
		||||
                    '], "pk_info": [', COALESCE(pk_metadata, ''),
 | 
			
		||||
@@ -262,9 +330,10 @@ SELECT CONCAT('{    "fk_info": [', COALESCE(fk_metadata, ''),
 | 
			
		||||
                    '], "indexes": [', COALESCE(indexes_metadata, ''),
 | 
			
		||||
                    '], "tables":[', COALESCE(tbls_metadata, ''),
 | 
			
		||||
                    '], "views":[', COALESCE(views_metadata, ''),
 | 
			
		||||
                    '], "custom_types": [', COALESCE(custom_types_metadata, ''),
 | 
			
		||||
                    '], "database_name": "', CURRENT_DATABASE(), '', '", "version": "', '',
 | 
			
		||||
              '"}') 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`;
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import type { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
			
		||||
import type { DatabaseClient } from '@/lib/domain/database-clients';
 | 
			
		||||
import { clickhouseQuery } from './clickhouse-script';
 | 
			
		||||
import { cockroachdbQuery } from './cockroachdb-script';
 | 
			
		||||
import { oracleDBQuery } from './oracle-script';
 | 
			
		||||
 | 
			
		||||
export type ImportMetadataScripts = Record<
 | 
			
		||||
    DatabaseType,
 | 
			
		||||
@@ -26,4 +27,5 @@ export const importMetadataScripts: ImportMetadataScripts = {
 | 
			
		||||
    [DatabaseType.MARIADB]: () => mariaDBQuery,
 | 
			
		||||
    [DatabaseType.CLICKHOUSE]: () => clickhouseQuery,
 | 
			
		||||
    [DatabaseType.COCKROACHDB]: () => cockroachdbQuery,
 | 
			
		||||
    [DatabaseType.ORACLE]: () => oracleDBQuery,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,11 @@
 | 
			
		||||
import { DatabaseEdition } from '@/lib/domain/database-edition';
 | 
			
		||||
import { DatabaseClient } from '@/lib/domain/database-clients';
 | 
			
		||||
 | 
			
		||||
const withExtras = true;
 | 
			
		||||
 | 
			
		||||
const withDefault = `COALESCE(REPLACE(p.dflt_value, '"', '\\"'), '')`;
 | 
			
		||||
const withoutDefault = `null`;
 | 
			
		||||
 | 
			
		||||
const sqliteQuery = `${`/* Standard SQLite */`}
 | 
			
		||||
WITH fk_info AS (
 | 
			
		||||
  SELECT
 | 
			
		||||
@@ -114,7 +119,7 @@ WITH fk_info AS (
 | 
			
		||||
                      END
 | 
			
		||||
                  ELSE null
 | 
			
		||||
              END,
 | 
			
		||||
              'default', COALESCE(REPLACE(p.dflt_value, '"', '\\"'), '')
 | 
			
		||||
              'default', ${withExtras ? withDefault : withoutDefault}
 | 
			
		||||
          )
 | 
			
		||||
      ) AS cols_metadata
 | 
			
		||||
  FROM
 | 
			
		||||
@@ -287,7 +292,7 @@ WITH fk_info AS (
 | 
			
		||||
                      END
 | 
			
		||||
                  ELSE null
 | 
			
		||||
              END,
 | 
			
		||||
              'default', COALESCE(REPLACE(p.dflt_value, '"', '\\"'), '')
 | 
			
		||||
              'default', ${withExtras ? withDefault : withoutDefault}
 | 
			
		||||
          )
 | 
			
		||||
      ) AS cols_metadata
 | 
			
		||||
  FROM
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,10 @@
 | 
			
		||||
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)*/`}
 | 
			
		||||
WITH fk_info AS (
 | 
			
		||||
    SELECT
 | 
			
		||||
@@ -81,8 +86,7 @@ cols AS (
 | 
			
		||||
                                ELSE 'null'
 | 
			
		||||
                            END +
 | 
			
		||||
                        ', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
 | 
			
		||||
                        ', "default": ' +
 | 
			
		||||
                            '"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"' +
 | 
			
		||||
                        ', "default": ' + ${withExtras ? withDefault : withoutDefault} +
 | 
			
		||||
                        ', "collation": ' + CASE
 | 
			
		||||
                            WHEN cols.COLLATION_NAME IS NULL THEN 'null'
 | 
			
		||||
                            ELSE '"' + STRING_ESCAPE(cols.COLLATION_NAME, 'json') + '"'
 | 
			
		||||
@@ -275,8 +279,7 @@ cols AS (
 | 
			
		||||
                                        ELSE 'null'
 | 
			
		||||
                                    END +
 | 
			
		||||
                                ', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
 | 
			
		||||
                                ', "default": ' +
 | 
			
		||||
                                    '"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"' +
 | 
			
		||||
                                ', "default": ' + ${withExtras ? withDefault : withoutDefault} +
 | 
			
		||||
                                ', "collation": ' +
 | 
			
		||||
                                    CASE
 | 
			
		||||
                                        WHEN cols.COLLATION_NAME IS NULL THEN 'null'
 | 
			
		||||
 
 | 
			
		||||
@@ -415,6 +415,16 @@ export const typeAffinity: Record<string, Record<string, string>> = {
 | 
			
		||||
        time: '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]: {
 | 
			
		||||
        // Generic fallback types (all lowercase for consistency)
 | 
			
		||||
        integer: 'integer',
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,9 @@ import ClickhouseLogo2 from '@/assets/clickhouse_logo_2.png';
 | 
			
		||||
import CockroachDBLogo from '@/assets/cockroachdb_logo.png';
 | 
			
		||||
import CockroachDBLogoDark from '@/assets/cockroachdb_logo_dark.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 type { EffectiveTheme } from '@/context/theme-context/theme-context';
 | 
			
		||||
 | 
			
		||||
@@ -32,6 +35,7 @@ export const databaseTypeToLabelMap: Record<DatabaseType, string> = {
 | 
			
		||||
    [DatabaseType.SQLITE]: 'SQLite',
 | 
			
		||||
    [DatabaseType.CLICKHOUSE]: 'ClickHouse',
 | 
			
		||||
    [DatabaseType.COCKROACHDB]: 'CockroachDB',
 | 
			
		||||
    [DatabaseType.ORACLE]: 'Oracle',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const databaseLogoMap: Record<DatabaseType, string> = {
 | 
			
		||||
@@ -42,6 +46,7 @@ export const databaseLogoMap: Record<DatabaseType, string> = {
 | 
			
		||||
    [DatabaseType.SQL_SERVER]: SqlServerLogo,
 | 
			
		||||
    [DatabaseType.CLICKHOUSE]: ClickhouseLogo,
 | 
			
		||||
    [DatabaseType.COCKROACHDB]: CockroachDBLogo,
 | 
			
		||||
    [DatabaseType.ORACLE]: OracleLogo,
 | 
			
		||||
    [DatabaseType.GENERIC]: '',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -53,6 +58,7 @@ export const databaseDarkLogoMap: Record<DatabaseType, string> = {
 | 
			
		||||
    [DatabaseType.SQL_SERVER]: SqlServerLogoDark,
 | 
			
		||||
    [DatabaseType.CLICKHOUSE]: ClickhouseLogoDark,
 | 
			
		||||
    [DatabaseType.COCKROACHDB]: CockroachDBLogoDark,
 | 
			
		||||
    [DatabaseType.ORACLE]: OracleLogoDark,
 | 
			
		||||
    [DatabaseType.GENERIC]: '',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -72,5 +78,6 @@ export const databaseSecondaryLogoMap: Record<DatabaseType, string> = {
 | 
			
		||||
    [DatabaseType.SQL_SERVER]: SqlServerLogo2,
 | 
			
		||||
    [DatabaseType.CLICKHOUSE]: ClickhouseLogo2,
 | 
			
		||||
    [DatabaseType.COCKROACHDB]: CockroachDBLogo2,
 | 
			
		||||
    [DatabaseType.ORACLE]: OracleLogo2,
 | 
			
		||||
    [DatabaseType.GENERIC]: GeneralDBLogo2,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@ export const databaseTypeToClientsMap: Record<DatabaseType, DatabaseClient[]> =
 | 
			
		||||
        [DatabaseType.MARIADB]: [],
 | 
			
		||||
        [DatabaseType.CLICKHOUSE]: [],
 | 
			
		||||
        [DatabaseType.COCKROACHDB]: [],
 | 
			
		||||
        [DatabaseType.ORACLE]: [],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
export const databaseEditionToClientsMap: Record<
 | 
			
		||||
 
 | 
			
		||||
@@ -63,4 +63,5 @@ export const databaseTypeToEditionMap: Record<DatabaseType, DatabaseEdition[]> =
 | 
			
		||||
        [DatabaseType.MARIADB]: [],
 | 
			
		||||
        [DatabaseType.CLICKHOUSE]: [],
 | 
			
		||||
        [DatabaseType.COCKROACHDB]: [],
 | 
			
		||||
        [DatabaseType.ORACLE]: [],
 | 
			
		||||
    };
 | 
			
		||||
 
 | 
			
		||||
@@ -7,4 +7,5 @@ export enum DatabaseType {
 | 
			
		||||
    SQLITE = 'sqlite',
 | 
			
		||||
    CLICKHOUSE = 'clickhouse',
 | 
			
		||||
    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.CLICKHOUSE]: 'postgresql',
 | 
			
		||||
    [DatabaseType.COCKROACHDB]: 'postgresql',
 | 
			
		||||
    [DatabaseType.ORACLE]: 'postgresql',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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 { PrimaryKeyInfo } from '../data/import-metadata/metadata-types/primary-key-info';
 | 
			
		||||
import type { TableInfo } from '../data/import-metadata/metadata-types/table-info';
 | 
			
		||||
import { generateId } from '../utils';
 | 
			
		||||
import { schemaNameToDomainSchemaName } from './db-schema';
 | 
			
		||||
import { generateId } from '../utils';
 | 
			
		||||
 | 
			
		||||
export interface DBField {
 | 
			
		||||
    id: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -23,4 +23,5 @@ export const databasesWithSchemas: DatabaseType[] = [
 | 
			
		||||
    DatabaseType.SQL_SERVER,
 | 
			
		||||
    DatabaseType.CLICKHOUSE,
 | 
			
		||||
    DatabaseType.COCKROACHDB,
 | 
			
		||||
    DatabaseType.ORACLE,
 | 
			
		||||
];
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,12 @@ import {
 | 
			
		||||
} from './db-table';
 | 
			
		||||
import { generateDiagramId } from '@/lib/utils';
 | 
			
		||||
import { areaSchema, type Area } from './area';
 | 
			
		||||
import type { DBCustomType } from './db-custom-type';
 | 
			
		||||
import {
 | 
			
		||||
    dbCustomTypeSchema,
 | 
			
		||||
    createCustomTypesFromMetadata,
 | 
			
		||||
} from './db-custom-type';
 | 
			
		||||
 | 
			
		||||
export interface Diagram {
 | 
			
		||||
    id: string;
 | 
			
		||||
    name: string;
 | 
			
		||||
@@ -29,6 +35,7 @@ export interface Diagram {
 | 
			
		||||
    relationships?: DBRelationship[];
 | 
			
		||||
    dependencies?: DBDependency[];
 | 
			
		||||
    areas?: Area[];
 | 
			
		||||
    customTypes?: DBCustomType[];
 | 
			
		||||
    createdAt: Date;
 | 
			
		||||
    updatedAt: Date;
 | 
			
		||||
}
 | 
			
		||||
@@ -42,6 +49,7 @@ export const diagramSchema: z.ZodType<Diagram> = z.object({
 | 
			
		||||
    relationships: z.array(dbRelationshipSchema).optional(),
 | 
			
		||||
    dependencies: z.array(dbDependencySchema).optional(),
 | 
			
		||||
    areas: z.array(areaSchema).optional(),
 | 
			
		||||
    customTypes: z.array(dbCustomTypeSchema).optional(),
 | 
			
		||||
    createdAt: z.date(),
 | 
			
		||||
    updatedAt: z.date(),
 | 
			
		||||
});
 | 
			
		||||
@@ -57,7 +65,11 @@ export const loadFromDatabaseMetadata = async ({
 | 
			
		||||
    diagramNumber?: number;
 | 
			
		||||
    databaseEdition?: DatabaseEdition;
 | 
			
		||||
}): Promise<Diagram> => {
 | 
			
		||||
    const { fk_info: foreignKeys, views: views } = databaseMetadata;
 | 
			
		||||
    const {
 | 
			
		||||
        fk_info: foreignKeys,
 | 
			
		||||
        views: views,
 | 
			
		||||
        custom_types: customTypes,
 | 
			
		||||
    } = databaseMetadata;
 | 
			
		||||
 | 
			
		||||
    const tables = createTablesFromMetadata({
 | 
			
		||||
        databaseMetadata,
 | 
			
		||||
@@ -75,6 +87,12 @@ export const loadFromDatabaseMetadata = async ({
 | 
			
		||||
        databaseType,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const dbCustomTypes = customTypes
 | 
			
		||||
        ? createCustomTypesFromMetadata({
 | 
			
		||||
              customTypes,
 | 
			
		||||
          })
 | 
			
		||||
        : [];
 | 
			
		||||
 | 
			
		||||
    const adjustedTables = adjustTablePositions({
 | 
			
		||||
        tables,
 | 
			
		||||
        relationships,
 | 
			
		||||
@@ -102,6 +120,7 @@ export const loadFromDatabaseMetadata = async ({
 | 
			
		||||
        tables: sortedTables,
 | 
			
		||||
        relationships,
 | 
			
		||||
        dependencies,
 | 
			
		||||
        customTypes: dbCustomTypes,
 | 
			
		||||
        createdAt: new Date(),
 | 
			
		||||
        updatedAt: new Date(),
 | 
			
		||||
    };
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ import {
 | 
			
		||||
    SidebarMenuButton,
 | 
			
		||||
    SidebarMenuItem,
 | 
			
		||||
} 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 { useLayout } from '@/hooks/use-layout';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
@@ -20,6 +20,7 @@ import ChartDBLogo from '@/assets/logo-light.png';
 | 
			
		||||
import ChartDBDarkLogo from '@/assets/logo-dark.png';
 | 
			
		||||
import { useTheme } from '@/hooks/use-theme';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
 | 
			
		||||
export interface SidebarItem {
 | 
			
		||||
    title: string;
 | 
			
		||||
@@ -37,7 +38,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { isMd: isDesktop } = useBreakpoint('md');
 | 
			
		||||
    const { effectiveTheme } = useTheme();
 | 
			
		||||
    const { dependencies } = useChartDB();
 | 
			
		||||
    const { dependencies, databaseType } = useChartDB();
 | 
			
		||||
 | 
			
		||||
    const items: SidebarItem[] = useMemo(() => {
 | 
			
		||||
        const baseItems = [
 | 
			
		||||
@@ -68,20 +69,38 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
 | 
			
		||||
                },
 | 
			
		||||
                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;
 | 
			
		||||
    }, [
 | 
			
		||||
        selectSidebarSection,
 | 
			
		||||
@@ -89,6 +108,7 @@ export const EditorSidebar: React.FC<EditorSidebarProps> = () => {
 | 
			
		||||
        t,
 | 
			
		||||
        showSidePanel,
 | 
			
		||||
        dependencies,
 | 
			
		||||
        databaseType,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook';
 | 
			
		||||
import { getOperatingSystem } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
export interface RelationshipsSectionProps {}
 | 
			
		||||
 | 
			
		||||
@@ -25,6 +27,7 @@ export const RelationshipsSection: React.FC<RelationshipsSectionProps> = () => {
 | 
			
		||||
    const { closeAllRelationshipsInSidebar } = useLayout();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { openCreateRelationshipDialog } = useDialog();
 | 
			
		||||
    const filterInputRef = React.useRef<HTMLInputElement>(null);
 | 
			
		||||
 | 
			
		||||
    const filteredRelationships = useMemo(() => {
 | 
			
		||||
        const filterName: (relationship: DBRelationship) => boolean = (
 | 
			
		||||
@@ -46,6 +49,19 @@ export const RelationshipsSection: React.FC<RelationshipsSectionProps> = () => {
 | 
			
		||||
        openCreateRelationshipDialog();
 | 
			
		||||
    }, [openCreateRelationshipDialog, setFilterText]);
 | 
			
		||||
 | 
			
		||||
    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">
 | 
			
		||||
            <div className="flex items-center justify-between gap-4 py-1">
 | 
			
		||||
@@ -69,6 +85,7 @@ export const RelationshipsSection: React.FC<RelationshipsSectionProps> = () => {
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="flex-1">
 | 
			
		||||
                    <Input
 | 
			
		||||
                        ref={filterInputRef}
 | 
			
		||||
                        type="text"
 | 
			
		||||
                        placeholder={t(
 | 
			
		||||
                            'side_panel.relationships_section.filter'
 | 
			
		||||
 
 | 
			
		||||
@@ -18,12 +18,15 @@ import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { DependenciesSection } from './dependencies-section/dependencies-section';
 | 
			
		||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
 | 
			
		||||
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 const SidePanel: React.FC<SidePanelProps> = () => {
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const { schemas, filterSchemas, filteredSchemas } = useChartDB();
 | 
			
		||||
    const { schemas, filterSchemas, filteredSchemas, databaseType } =
 | 
			
		||||
        useChartDB();
 | 
			
		||||
    const {
 | 
			
		||||
        selectSidebarSection,
 | 
			
		||||
        selectedSidebarSection,
 | 
			
		||||
@@ -117,6 +120,13 @@ export const SidePanel: React.FC<SidePanelProps> = () => {
 | 
			
		||||
                                <SelectItem value="areas">
 | 
			
		||||
                                    {t('side_panel.areas_section.areas')}
 | 
			
		||||
                                </SelectItem>
 | 
			
		||||
                                {databaseType === DatabaseType.POSTGRESQL ? (
 | 
			
		||||
                                    <SelectItem value="customTypes">
 | 
			
		||||
                                        {t(
 | 
			
		||||
                                            'side_panel.custom_types_section.custom_types'
 | 
			
		||||
                                        )}
 | 
			
		||||
                                    </SelectItem>
 | 
			
		||||
                                ) : null}
 | 
			
		||||
                            </SelectGroup>
 | 
			
		||||
                        </SelectContent>
 | 
			
		||||
                    </Select>
 | 
			
		||||
@@ -128,8 +138,10 @@ export const SidePanel: React.FC<SidePanelProps> = () => {
 | 
			
		||||
                <RelationshipsSection />
 | 
			
		||||
            ) : selectedSidebarSection === 'dependencies' ? (
 | 
			
		||||
                <DependenciesSection />
 | 
			
		||||
            ) : (
 | 
			
		||||
            ) : selectedSidebarSection === 'areas' ? (
 | 
			
		||||
                <AreasSection />
 | 
			
		||||
            ) : (
 | 
			
		||||
                <CustomTypesSection />
 | 
			
		||||
            )}
 | 
			
		||||
        </aside>
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -12,11 +12,41 @@ import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-lang
 | 
			
		||||
import { DatabaseType } from '@/lib/domain/database-type';
 | 
			
		||||
import { ArrowLeftRight } from 'lucide-react';
 | 
			
		||||
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 {
 | 
			
		||||
    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) => {
 | 
			
		||||
    return theme === 'dark' ? 'dbml-dark' : 'dbml-light';
 | 
			
		||||
};
 | 
			
		||||
@@ -33,6 +63,7 @@ const databaseTypeToImportFormat = (
 | 
			
		||||
        case DatabaseType.POSTGRESQL:
 | 
			
		||||
        case DatabaseType.COCKROACHDB:
 | 
			
		||||
        case DatabaseType.SQLITE:
 | 
			
		||||
        case DatabaseType.ORACLE:
 | 
			
		||||
            return 'postgres';
 | 
			
		||||
        default:
 | 
			
		||||
            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
 | 
			
		||||
    sanitized = sanitized.replace(/\?\?/g, '__');
 | 
			
		||||
 | 
			
		||||
@@ -286,7 +330,7 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
 | 
			
		||||
    const { effectiveTheme } = useTheme();
 | 
			
		||||
    const { toast } = useToast();
 | 
			
		||||
    const [dbmlFormat, setDbmlFormat] = useState<'inline' | 'standard'>(
 | 
			
		||||
        'standard'
 | 
			
		||||
        'inline'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // --- Effect for handling empty field name warnings ---
 | 
			
		||||
@@ -438,6 +482,9 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
 | 
			
		||||
        let inline = '';
 | 
			
		||||
        let baseScript = ''; // Define baseScript outside try
 | 
			
		||||
 | 
			
		||||
        // Use finalDiagramForExport.customTypes which should be DBCustomType[]
 | 
			
		||||
        const enumsDBML = generateEnumsDBML(finalDiagramForExport.customTypes);
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            baseScript = exportBaseSQL({
 | 
			
		||||
                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
 | 
			
		||||
            sqlRenamedTables.forEach((originalName, newName) => {
 | 
			
		||||
                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
 | 
			
		||||
            fieldRenames.forEach(({ table, originalName, newName }) => {
 | 
			
		||||
                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(
 | 
			
		||||
@@ -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));
 | 
			
		||||
        } catch (error: unknown) {
 | 
			
		||||
            console.error(
 | 
			
		||||
@@ -494,6 +558,15 @@ export const TableDBML: React.FC<TableDBMLProps> = ({ filteredTables }) => {
 | 
			
		||||
                    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 };
 | 
			
		||||
    }, [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 [localField, setLocalField] = React.useState<DBField>(field);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setLocalField(field);
 | 
			
		||||
    }, [field]);
 | 
			
		||||
 | 
			
		||||
    const debouncedUpdateFieldRef = useRef<((value?: DBField) => void) | null>(
 | 
			
		||||
        null
 | 
			
		||||
    );
 | 
			
		||||
@@ -49,11 +53,17 @@ export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
 | 
			
		||||
        };
 | 
			
		||||
    }, [updateField]);
 | 
			
		||||
 | 
			
		||||
    const prevFieldRef = useRef<DBField>(field);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (debouncedUpdateFieldRef.current && !equal(field, localField)) {
 | 
			
		||||
        if (
 | 
			
		||||
            debouncedUpdateFieldRef.current &&
 | 
			
		||||
            !equal(prevFieldRef.current, localField)
 | 
			
		||||
        ) {
 | 
			
		||||
            debouncedUpdateFieldRef.current(localField);
 | 
			
		||||
        }
 | 
			
		||||
    }, [localField, field]);
 | 
			
		||||
        prevFieldRef.current = localField;
 | 
			
		||||
    }, [localField]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Popover
 | 
			
		||||
 
 | 
			
		||||
@@ -128,6 +128,7 @@ export const TableField: React.FC<TableFieldProps> = ({
 | 
			
		||||
                        <span>
 | 
			
		||||
                            <SelectBox
 | 
			
		||||
                                className="flex h-8 min-h-8 w-full"
 | 
			
		||||
                                popoverClassName="min-w-[350px]"
 | 
			
		||||
                                options={dataFieldOptions}
 | 
			
		||||
                                placeholder={t(
 | 
			
		||||
                                    'side_panel.tables_section.table.field_type'
 | 
			
		||||
 
 | 
			
		||||
@@ -233,6 +233,15 @@ export const Menu: React.FC<MenuProps> = () => {
 | 
			
		||||
                            >
 | 
			
		||||
                                {databaseTypeToLabelMap['sqlite']}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                            <MenubarItem
 | 
			
		||||
                                onClick={() =>
 | 
			
		||||
                                    openImportDatabaseDialog({
 | 
			
		||||
                                        databaseType: DatabaseType.ORACLE,
 | 
			
		||||
                                    })
 | 
			
		||||
                                }
 | 
			
		||||
                            >
 | 
			
		||||
                                {databaseTypeToLabelMap['oracle']}
 | 
			
		||||
                            </MenubarItem>
 | 
			
		||||
                        </MenubarSubContent>
 | 
			
		||||
                    </MenubarSub>
 | 
			
		||||
                    <MenubarSeparator />
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user