mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-11-04 05:53:15 +00:00 
			
		
		
		
	Compare commits
	
		
			9 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					44eac7daff | ||
| 
						 | 
					502472b083 | ||
| 
						 | 
					52d2ea596c | ||
| 
						 | 
					bd67ccfbcf | ||
| 
						 | 
					62beb68fa1 | ||
| 
						 | 
					09b1275475 | ||
| 
						 | 
					5dd7fe75d1 | ||
| 
						 | 
					2939320a15 | ||
| 
						 | 
					a643852837 | 
							
								
								
									
										17
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -1,5 +1,22 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## [1.9.0](https://github.com/chartdb/chartdb/compare/v1.8.1...v1.9.0) (2025-03-13)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Features
 | 
			
		||||
 | 
			
		||||
* **canvas:** highlight the Show-All button when No-Tables are visible in the canvas ([#612](https://github.com/chartdb/chartdb/issues/612)) ([62beb68](https://github.com/chartdb/chartdb/commit/62beb68fa1ec22ccd4fe5e59a8ceb9d3e8f6d374))
 | 
			
		||||
* **chart max length:** add support for edit char max length ([#613](https://github.com/chartdb/chartdb/issues/613)) ([09b1275](https://github.com/chartdb/chartdb/commit/09b12754757b9625ca287d91a92cf0d83c9e2b89))
 | 
			
		||||
* **chart max length:** enable edit length from data type select box ([#616](https://github.com/chartdb/chartdb/issues/616)) ([bd67ccf](https://github.com/chartdb/chartdb/commit/bd67ccfbcf66b919453ca6c0bfd71e16772b3d8e))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
### Bug Fixes
 | 
			
		||||
 | 
			
		||||
* **cardinality:** set true as default ([#583](https://github.com/chartdb/chartdb/issues/583)) ([2939320](https://github.com/chartdb/chartdb/commit/2939320a15a9ccd9eccfe46c26e04ca1edca2420))
 | 
			
		||||
* **performance:** Optimize performance of field comments editing ([#610](https://github.com/chartdb/chartdb/issues/610)) ([5dd7fe7](https://github.com/chartdb/chartdb/commit/5dd7fe75d1b0378ba406c75183c5e2356730c3b4))
 | 
			
		||||
* remove Buckle dialog ([#617](https://github.com/chartdb/chartdb/issues/617)) ([502472b](https://github.com/chartdb/chartdb/commit/502472b08342be425e66e2b6c94e5fe37ba14aa9))
 | 
			
		||||
* **shorcuts:** add shortcut to toggle the theme ([#602](https://github.com/chartdb/chartdb/issues/602)) ([a643852](https://github.com/chartdb/chartdb/commit/a6438528375ab54d3ec7d80ac6b6ddd65ea8cf1e))
 | 
			
		||||
 | 
			
		||||
## [1.8.1](https://github.com/chartdb/chartdb/compare/v1.8.0...v1.8.1) (2025-03-02)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,12 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "chartdb",
 | 
			
		||||
    "version": "1.8.1",
 | 
			
		||||
    "version": "1.9.0",
 | 
			
		||||
    "lockfileVersion": 3,
 | 
			
		||||
    "requires": true,
 | 
			
		||||
    "packages": {
 | 
			
		||||
        "": {
 | 
			
		||||
            "name": "chartdb",
 | 
			
		||||
            "version": "1.8.1",
 | 
			
		||||
            "version": "1.9.0",
 | 
			
		||||
            "dependencies": {
 | 
			
		||||
                "@ai-sdk/openai": "^0.0.51",
 | 
			
		||||
                "@dbml/core": "^3.9.5",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
    "name": "chartdb",
 | 
			
		||||
    "private": true,
 | 
			
		||||
    "version": "1.8.1",
 | 
			
		||||
    "version": "1.9.0",
 | 
			
		||||
    "type": "module",
 | 
			
		||||
    "scripts": {
 | 
			
		||||
        "dev": "vite",
 | 
			
		||||
 
 | 
			
		||||
@@ -24,12 +24,19 @@ export interface SelectBoxOption {
 | 
			
		||||
    value: string;
 | 
			
		||||
    label: string;
 | 
			
		||||
    description?: string;
 | 
			
		||||
    regex?: string;
 | 
			
		||||
    extractRegex?: RegExp;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SelectBoxProps {
 | 
			
		||||
    options: SelectBoxOption[];
 | 
			
		||||
    value?: string[] | string;
 | 
			
		||||
    onChange?: (values: string[] | string) => void;
 | 
			
		||||
    valueSuffix?: string;
 | 
			
		||||
    optionSuffix?: (option: SelectBoxOption) => string;
 | 
			
		||||
    onChange?: (
 | 
			
		||||
        values: string[] | string,
 | 
			
		||||
        regexMatches?: string[] | string
 | 
			
		||||
    ) => void;
 | 
			
		||||
    placeholder?: string;
 | 
			
		||||
    inputPlaceholder?: string;
 | 
			
		||||
    emptyPlaceholder?: string;
 | 
			
		||||
@@ -55,10 +62,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
            className,
 | 
			
		||||
            options,
 | 
			
		||||
            value,
 | 
			
		||||
            valueSuffix,
 | 
			
		||||
            onChange,
 | 
			
		||||
            multiple,
 | 
			
		||||
            oneLine,
 | 
			
		||||
            selectAll,
 | 
			
		||||
            optionSuffix,
 | 
			
		||||
            deselectAll,
 | 
			
		||||
            clearText,
 | 
			
		||||
            showClear,
 | 
			
		||||
@@ -86,7 +95,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const handleSelect = React.useCallback(
 | 
			
		||||
            (selectedValue: string) => {
 | 
			
		||||
            (selectedValue: string, regexMatches?: string[]) => {
 | 
			
		||||
                if (multiple) {
 | 
			
		||||
                    const newValue =
 | 
			
		||||
                        value?.includes(selectedValue) && Array.isArray(value)
 | 
			
		||||
@@ -94,7 +103,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                            : [...(value ?? []), selectedValue];
 | 
			
		||||
                    onChange?.(newValue);
 | 
			
		||||
                } else {
 | 
			
		||||
                    onChange?.(selectedValue);
 | 
			
		||||
                    onChange?.(selectedValue, regexMatches);
 | 
			
		||||
                    setIsOpen(false);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
@@ -199,6 +208,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                                                (opt) => opt.value === value
 | 
			
		||||
                                            )?.label
 | 
			
		||||
                                        }
 | 
			
		||||
                                        {valueSuffix ? valueSuffix : ''}
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                )
 | 
			
		||||
                            ) : (
 | 
			
		||||
@@ -239,11 +249,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                    align="center"
 | 
			
		||||
                >
 | 
			
		||||
                    <Command
 | 
			
		||||
                        filter={(value, search) =>
 | 
			
		||||
                            value.toLowerCase().includes(search.toLowerCase())
 | 
			
		||||
                                ? 1
 | 
			
		||||
                                : 0
 | 
			
		||||
                        filter={(value, search, keywords) => {
 | 
			
		||||
                            if (
 | 
			
		||||
                                keywords?.length &&
 | 
			
		||||
                                keywords.some((keyword) =>
 | 
			
		||||
                                    new RegExp(keyword).test(search)
 | 
			
		||||
                                )
 | 
			
		||||
                            ) {
 | 
			
		||||
                                return 1;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            return value
 | 
			
		||||
                                .toLowerCase()
 | 
			
		||||
                                .includes(search.toLowerCase())
 | 
			
		||||
                                ? 1
 | 
			
		||||
                                : 0;
 | 
			
		||||
                        }}
 | 
			
		||||
                    >
 | 
			
		||||
                        <div className="relative">
 | 
			
		||||
                            <CommandInput
 | 
			
		||||
@@ -302,14 +323,36 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                                            const isSelected =
 | 
			
		||||
                                                Array.isArray(value) &&
 | 
			
		||||
                                                value.includes(option.value);
 | 
			
		||||
 | 
			
		||||
                                            const isRegexMatch =
 | 
			
		||||
                                                option.regex &&
 | 
			
		||||
                                                new RegExp(option.regex)?.test(
 | 
			
		||||
                                                    searchTerm
 | 
			
		||||
                                                );
 | 
			
		||||
 | 
			
		||||
                                            const matches = option.extractRegex
 | 
			
		||||
                                                ? searchTerm.match(
 | 
			
		||||
                                                      option.extractRegex
 | 
			
		||||
                                                  )
 | 
			
		||||
                                                : undefined;
 | 
			
		||||
 | 
			
		||||
                                            return (
 | 
			
		||||
                                                <CommandItem
 | 
			
		||||
                                                    className="flex items-center"
 | 
			
		||||
                                                    key={option.value}
 | 
			
		||||
                                                    keywords={
 | 
			
		||||
                                                        option.regex
 | 
			
		||||
                                                            ? [option.regex]
 | 
			
		||||
                                                            : undefined
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                    // value={option.value}
 | 
			
		||||
                                                    onSelect={() =>
 | 
			
		||||
                                                        handleSelect(
 | 
			
		||||
                                                            option.value
 | 
			
		||||
                                                            option.value,
 | 
			
		||||
                                                            matches?.map(
 | 
			
		||||
                                                                (match) =>
 | 
			
		||||
                                                                    match.toString()
 | 
			
		||||
                                                            )
 | 
			
		||||
                                                        )
 | 
			
		||||
                                                    }
 | 
			
		||||
                                                >
 | 
			
		||||
@@ -327,7 +370,15 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                                                    )}
 | 
			
		||||
                                                    <div className="flex items-center truncate">
 | 
			
		||||
                                                        <span>
 | 
			
		||||
                                                            {option.label}
 | 
			
		||||
                                                            {isRegexMatch
 | 
			
		||||
                                                                ? searchTerm
 | 
			
		||||
                                                                : option.label}
 | 
			
		||||
                                                            {!isRegexMatch &&
 | 
			
		||||
                                                            optionSuffix
 | 
			
		||||
                                                                ? optionSuffix(
 | 
			
		||||
                                                                      option
 | 
			
		||||
                                                                  )
 | 
			
		||||
                                                                : ''}
 | 
			
		||||
                                                        </span>
 | 
			
		||||
                                                        {option.description && (
 | 
			
		||||
                                                            <span className="ml-1 text-xs text-muted-foreground">
 | 
			
		||||
@@ -337,9 +388,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
 | 
			
		||||
                                                            </span>
 | 
			
		||||
                                                        )}
 | 
			
		||||
                                                    </div>
 | 
			
		||||
                                                    {!multiple &&
 | 
			
		||||
                                                    {((!multiple &&
 | 
			
		||||
                                                        option.value ===
 | 
			
		||||
                                                            value && (
 | 
			
		||||
                                                            value) ||
 | 
			
		||||
                                                        isRegexMatch) && (
 | 
			
		||||
                                                        <CheckIcon
 | 
			
		||||
                                                            className={cn(
 | 
			
		||||
                                                                'ml-auto',
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,8 @@ import { defaultSchemas } from '@/lib/data/default-schemas';
 | 
			
		||||
import { useEventEmitter } from 'ahooks';
 | 
			
		||||
import type { DBDependency } from '@/lib/domain/db-dependency';
 | 
			
		||||
import { storageInitialValue } from '../storage-context/storage-context';
 | 
			
		||||
import { useDiff } from '../diff-context/use-diff';
 | 
			
		||||
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
 | 
			
		||||
 | 
			
		||||
export interface ChartDBProviderProps {
 | 
			
		||||
    diagram?: Diagram;
 | 
			
		||||
@@ -30,7 +32,8 @@ export interface ChartDBProviderProps {
 | 
			
		||||
 | 
			
		||||
export const ChartDBProvider: React.FC<
 | 
			
		||||
    React.PropsWithChildren<ChartDBProviderProps>
 | 
			
		||||
> = ({ children, diagram, readonly }) => {
 | 
			
		||||
> = ({ children, diagram, readonly: readonlyProp }) => {
 | 
			
		||||
    const { hasDiff } = useDiff();
 | 
			
		||||
    let db = useStorage();
 | 
			
		||||
    const events = useEventEmitter<ChartDBEvent>();
 | 
			
		||||
    const { setSchemasFilter, schemasFilter } = useLocalConfig();
 | 
			
		||||
@@ -53,9 +56,33 @@ export const ChartDBProvider: React.FC<
 | 
			
		||||
    const [dependencies, setDependencies] = useState<DBDependency[]>(
 | 
			
		||||
        diagram?.dependencies ?? []
 | 
			
		||||
    );
 | 
			
		||||
    const { events: diffEvents } = useDiff();
 | 
			
		||||
 | 
			
		||||
    const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
 | 
			
		||||
        const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
 | 
			
		||||
        setTables((tables) =>
 | 
			
		||||
            [...tables, ...(tablesAdded ?? [])].map((table) => {
 | 
			
		||||
                const fields = fieldsAdded.get(table.id);
 | 
			
		||||
                return fields
 | 
			
		||||
                    ? { ...table, fields: [...table.fields, ...fields] }
 | 
			
		||||
                    : table;
 | 
			
		||||
            })
 | 
			
		||||
        );
 | 
			
		||||
        setRelationships((relationships) => [
 | 
			
		||||
            ...relationships,
 | 
			
		||||
            ...(relationshipsAdded ?? []),
 | 
			
		||||
        ]);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    diffEvents.useSubscription(diffCalculatedHandler);
 | 
			
		||||
 | 
			
		||||
    const defaultSchemaName = defaultSchemas[databaseType];
 | 
			
		||||
 | 
			
		||||
    const readonly = useMemo(
 | 
			
		||||
        () => readonlyProp ?? hasDiff ?? false,
 | 
			
		||||
        [readonlyProp, hasDiff]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (readonly) {
 | 
			
		||||
        db = storageInitialValue;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -47,10 +47,6 @@ export interface DialogContext {
 | 
			
		||||
    openStarUsDialog: () => void;
 | 
			
		||||
    closeStarUsDialog: () => void;
 | 
			
		||||
 | 
			
		||||
    // Buckle dialog
 | 
			
		||||
    openBuckleDialog: () => void;
 | 
			
		||||
    closeBuckleDialog: () => void;
 | 
			
		||||
 | 
			
		||||
    // Export image dialog
 | 
			
		||||
    openExportImageDialog: (
 | 
			
		||||
        params: Omit<ExportImageDialogProps, 'dialog'>
 | 
			
		||||
@@ -97,8 +93,6 @@ export const dialogContext = createContext<DialogContext>({
 | 
			
		||||
    closeExportDiagramDialog: emptyFn,
 | 
			
		||||
    openImportDiagramDialog: emptyFn,
 | 
			
		||||
    closeImportDiagramDialog: emptyFn,
 | 
			
		||||
    openBuckleDialog: emptyFn,
 | 
			
		||||
    closeBuckleDialog: emptyFn,
 | 
			
		||||
    openImportDBMLDialog: emptyFn,
 | 
			
		||||
    closeImportDBMLDialog: emptyFn,
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,6 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
 | 
			
		||||
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
 | 
			
		||||
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
 | 
			
		||||
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
 | 
			
		||||
import { BuckleDialog } from '@/dialogs/buckle-dialog/buckle-dialog';
 | 
			
		||||
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
 | 
			
		||||
import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
 | 
			
		||||
 | 
			
		||||
@@ -54,7 +53,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const [openStarUsDialog, setOpenStarUsDialog] = useState(false);
 | 
			
		||||
    const [openBuckleDialog, setOpenBuckleDialog] = useState(false);
 | 
			
		||||
 | 
			
		||||
    // Export image dialog
 | 
			
		||||
    const [openExportImageDialog, setOpenExportImageDialog] = useState(false);
 | 
			
		||||
@@ -147,8 +145,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                closeTableSchemaDialog: () => setOpenTableSchemaDialog(false),
 | 
			
		||||
                openStarUsDialog: () => setOpenStarUsDialog(true),
 | 
			
		||||
                closeStarUsDialog: () => setOpenStarUsDialog(false),
 | 
			
		||||
                closeBuckleDialog: () => setOpenBuckleDialog(false),
 | 
			
		||||
                openBuckleDialog: () => setOpenBuckleDialog(true),
 | 
			
		||||
                closeExportImageDialog: () => setOpenExportImageDialog(false),
 | 
			
		||||
                openExportImageDialog: openExportImageDialogHandler,
 | 
			
		||||
                openExportDiagramDialog: () => setOpenExportDiagramDialog(true),
 | 
			
		||||
@@ -193,7 +189,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            />
 | 
			
		||||
            <ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
 | 
			
		||||
            <ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
 | 
			
		||||
            <BuckleDialog dialog={{ open: openBuckleDialog }} />
 | 
			
		||||
            <ImportDBMLDialog
 | 
			
		||||
                dialog={{ open: openImportDBMLDialog }}
 | 
			
		||||
                {...importDBMLDialogParams}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										433
									
								
								src/context/diff-context/diff-check/diff-check.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										433
									
								
								src/context/diff-context/diff-check/diff-check.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,433 @@
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import type {
 | 
			
		||||
    ChartDBDiff,
 | 
			
		||||
    DiffMap,
 | 
			
		||||
    DiffObject,
 | 
			
		||||
    FieldDiffAttribute,
 | 
			
		||||
} from '../types';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DBIndex } from '@/lib/domain/db-index';
 | 
			
		||||
 | 
			
		||||
export function getDiffMapKey({
 | 
			
		||||
    diffObject,
 | 
			
		||||
    objectId,
 | 
			
		||||
    attribute,
 | 
			
		||||
}: {
 | 
			
		||||
    diffObject: DiffObject;
 | 
			
		||||
    objectId: string;
 | 
			
		||||
    attribute?: string;
 | 
			
		||||
}): string {
 | 
			
		||||
    return attribute
 | 
			
		||||
        ? `${diffObject}-${attribute}-${objectId}`
 | 
			
		||||
        : `${diffObject}-${objectId}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function generateDiff({
 | 
			
		||||
    diagram,
 | 
			
		||||
    newDiagram,
 | 
			
		||||
}: {
 | 
			
		||||
    diagram: Diagram;
 | 
			
		||||
    newDiagram: Diagram;
 | 
			
		||||
}): {
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
    changedTables: Map<string, boolean>;
 | 
			
		||||
    changedFields: Map<string, boolean>;
 | 
			
		||||
} {
 | 
			
		||||
    const newDiffs = new Map<string, ChartDBDiff>();
 | 
			
		||||
    const changedTables = new Map<string, boolean>();
 | 
			
		||||
    const changedFields = new Map<string, boolean>();
 | 
			
		||||
 | 
			
		||||
    // Compare tables
 | 
			
		||||
    compareTables({ diagram, newDiagram, diffMap: newDiffs, changedTables });
 | 
			
		||||
 | 
			
		||||
    // Compare fields and indexes for matching tables
 | 
			
		||||
    compareTableContents({
 | 
			
		||||
        diagram,
 | 
			
		||||
        newDiagram,
 | 
			
		||||
        diffMap: newDiffs,
 | 
			
		||||
        changedTables,
 | 
			
		||||
        changedFields,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Compare relationships
 | 
			
		||||
    compareRelationships({ diagram, newDiagram, diffMap: newDiffs });
 | 
			
		||||
 | 
			
		||||
    return { diffMap: newDiffs, changedTables, changedFields };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Compare tables between diagrams
 | 
			
		||||
function compareTables({
 | 
			
		||||
    diagram,
 | 
			
		||||
    newDiagram,
 | 
			
		||||
    diffMap,
 | 
			
		||||
    changedTables,
 | 
			
		||||
}: {
 | 
			
		||||
    diagram: Diagram;
 | 
			
		||||
    newDiagram: Diagram;
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
    changedTables: Map<string, boolean>;
 | 
			
		||||
}) {
 | 
			
		||||
    const oldTables = diagram.tables || [];
 | 
			
		||||
    const newTables = newDiagram.tables || [];
 | 
			
		||||
 | 
			
		||||
    // Check for added tables
 | 
			
		||||
    for (const newTable of newTables) {
 | 
			
		||||
        if (!oldTables.find((t) => t.id === newTable.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({ diffObject: 'table', objectId: newTable.id }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'table',
 | 
			
		||||
                    type: 'added',
 | 
			
		||||
                    tableId: newTable.id,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            changedTables.set(newTable.id, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check for removed tables
 | 
			
		||||
    for (const oldTable of oldTables) {
 | 
			
		||||
        if (!newTables.find((t) => t.id === oldTable.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({ diffObject: 'table', objectId: oldTable.id }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'table',
 | 
			
		||||
                    type: 'removed',
 | 
			
		||||
                    tableId: oldTable.id,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            changedTables.set(oldTable.id, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check for table name and comments changes
 | 
			
		||||
    for (const oldTable of oldTables) {
 | 
			
		||||
        const newTable = newTables.find((t) => t.id === oldTable.id);
 | 
			
		||||
 | 
			
		||||
        if (!newTable) continue;
 | 
			
		||||
 | 
			
		||||
        if (oldTable.name !== newTable.name) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'table',
 | 
			
		||||
                    objectId: oldTable.id,
 | 
			
		||||
                    attribute: 'name',
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'table',
 | 
			
		||||
                    type: 'changed',
 | 
			
		||||
                    tableId: oldTable.id,
 | 
			
		||||
                    attributes: 'name',
 | 
			
		||||
                    newValue: newTable.name,
 | 
			
		||||
                    oldValue: oldTable.name,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            changedTables.set(oldTable.id, true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (oldTable.comments !== newTable.comments) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'table',
 | 
			
		||||
                    objectId: oldTable.id,
 | 
			
		||||
                    attribute: 'comments',
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'table',
 | 
			
		||||
                    type: 'changed',
 | 
			
		||||
                    tableId: oldTable.id,
 | 
			
		||||
                    attributes: 'comments',
 | 
			
		||||
                    newValue: newTable.comments,
 | 
			
		||||
                    oldValue: oldTable.comments,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            changedTables.set(oldTable.id, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Compare fields and indexes for matching tables
 | 
			
		||||
function compareTableContents({
 | 
			
		||||
    diagram,
 | 
			
		||||
    newDiagram,
 | 
			
		||||
    diffMap,
 | 
			
		||||
    changedTables,
 | 
			
		||||
    changedFields,
 | 
			
		||||
}: {
 | 
			
		||||
    diagram: Diagram;
 | 
			
		||||
    newDiagram: Diagram;
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
    changedTables: Map<string, boolean>;
 | 
			
		||||
    changedFields: Map<string, boolean>;
 | 
			
		||||
}) {
 | 
			
		||||
    const oldTables = diagram.tables || [];
 | 
			
		||||
    const newTables = newDiagram.tables || [];
 | 
			
		||||
 | 
			
		||||
    // For each table that exists in both diagrams
 | 
			
		||||
    for (const oldTable of oldTables) {
 | 
			
		||||
        const newTable = newTables.find((t) => t.id === oldTable.id);
 | 
			
		||||
        if (!newTable) continue;
 | 
			
		||||
 | 
			
		||||
        // Compare fields
 | 
			
		||||
        compareFields({
 | 
			
		||||
            tableId: oldTable.id,
 | 
			
		||||
            oldFields: oldTable.fields,
 | 
			
		||||
            newFields: newTable.fields,
 | 
			
		||||
            diffMap,
 | 
			
		||||
            changedTables,
 | 
			
		||||
            changedFields,
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Compare indexes
 | 
			
		||||
        compareIndexes({
 | 
			
		||||
            tableId: oldTable.id,
 | 
			
		||||
            oldIndexes: oldTable.indexes,
 | 
			
		||||
            newIndexes: newTable.indexes,
 | 
			
		||||
            diffMap,
 | 
			
		||||
            changedTables,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Compare fields between tables
 | 
			
		||||
function compareFields({
 | 
			
		||||
    tableId,
 | 
			
		||||
    oldFields,
 | 
			
		||||
    newFields,
 | 
			
		||||
    diffMap,
 | 
			
		||||
    changedTables,
 | 
			
		||||
    changedFields,
 | 
			
		||||
}: {
 | 
			
		||||
    tableId: string;
 | 
			
		||||
    oldFields: DBField[];
 | 
			
		||||
    newFields: DBField[];
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
    changedTables: Map<string, boolean>;
 | 
			
		||||
    changedFields: Map<string, boolean>;
 | 
			
		||||
}) {
 | 
			
		||||
    // Check for added fields
 | 
			
		||||
    for (const newField of newFields) {
 | 
			
		||||
        if (!oldFields.find((f) => f.id === newField.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'field',
 | 
			
		||||
                    objectId: newField.id,
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'field',
 | 
			
		||||
                    type: 'added',
 | 
			
		||||
                    fieldId: newField.id,
 | 
			
		||||
                    tableId,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            changedTables.set(tableId, true);
 | 
			
		||||
            changedFields.set(newField.id, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check for removed fields
 | 
			
		||||
    for (const oldField of oldFields) {
 | 
			
		||||
        if (!newFields.find((f) => f.id === oldField.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'field',
 | 
			
		||||
                    objectId: oldField.id,
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'field',
 | 
			
		||||
                    type: 'removed',
 | 
			
		||||
                    fieldId: oldField.id,
 | 
			
		||||
                    tableId,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            changedTables.set(tableId, true);
 | 
			
		||||
            changedFields.set(oldField.id, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check for field changes
 | 
			
		||||
    for (const oldField of oldFields) {
 | 
			
		||||
        const newField = newFields.find((f) => f.id === oldField.id);
 | 
			
		||||
        if (!newField) continue;
 | 
			
		||||
 | 
			
		||||
        // Compare basic field properties
 | 
			
		||||
        compareFieldProperties({
 | 
			
		||||
            tableId,
 | 
			
		||||
            oldField,
 | 
			
		||||
            newField,
 | 
			
		||||
            diffMap,
 | 
			
		||||
            changedTables,
 | 
			
		||||
            changedFields,
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Compare field properties
 | 
			
		||||
function compareFieldProperties({
 | 
			
		||||
    tableId,
 | 
			
		||||
    oldField,
 | 
			
		||||
    newField,
 | 
			
		||||
    diffMap,
 | 
			
		||||
    changedTables,
 | 
			
		||||
    changedFields,
 | 
			
		||||
}: {
 | 
			
		||||
    tableId: string;
 | 
			
		||||
    oldField: DBField;
 | 
			
		||||
    newField: DBField;
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
    changedTables: Map<string, boolean>;
 | 
			
		||||
    changedFields: Map<string, boolean>;
 | 
			
		||||
}) {
 | 
			
		||||
    const changedAttributes: FieldDiffAttribute[] = [];
 | 
			
		||||
 | 
			
		||||
    if (oldField.name !== newField.name) {
 | 
			
		||||
        changedAttributes.push('name');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (oldField.type.id !== newField.type.id) {
 | 
			
		||||
        changedAttributes.push('type');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (oldField.primaryKey !== newField.primaryKey) {
 | 
			
		||||
        changedAttributes.push('primaryKey');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (oldField.unique !== newField.unique) {
 | 
			
		||||
        changedAttributes.push('unique');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (oldField.nullable !== newField.nullable) {
 | 
			
		||||
        changedAttributes.push('nullable');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (oldField.comments !== newField.comments) {
 | 
			
		||||
        changedAttributes.push('comments');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (changedAttributes.length > 0) {
 | 
			
		||||
        for (const attribute of changedAttributes) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'field',
 | 
			
		||||
                    objectId: oldField.id,
 | 
			
		||||
                    attribute: attribute,
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'field',
 | 
			
		||||
                    type: 'changed',
 | 
			
		||||
                    fieldId: oldField.id,
 | 
			
		||||
                    tableId,
 | 
			
		||||
                    attributes: attribute,
 | 
			
		||||
                    oldValue: oldField[attribute],
 | 
			
		||||
                    newValue: newField[attribute],
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
        changedTables.set(tableId, true);
 | 
			
		||||
        changedFields.set(oldField.id, true);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Compare indexes between tables
 | 
			
		||||
function compareIndexes({
 | 
			
		||||
    tableId,
 | 
			
		||||
    oldIndexes,
 | 
			
		||||
    newIndexes,
 | 
			
		||||
    diffMap,
 | 
			
		||||
    changedTables,
 | 
			
		||||
}: {
 | 
			
		||||
    tableId: string;
 | 
			
		||||
    oldIndexes: DBIndex[];
 | 
			
		||||
    newIndexes: DBIndex[];
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
    changedTables: Map<string, boolean>;
 | 
			
		||||
}) {
 | 
			
		||||
    // Check for added indexes
 | 
			
		||||
    for (const newIndex of newIndexes) {
 | 
			
		||||
        if (!oldIndexes.find((i) => i.id === newIndex.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'index',
 | 
			
		||||
                    objectId: newIndex.id,
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'index',
 | 
			
		||||
                    type: 'added',
 | 
			
		||||
                    indexId: newIndex.id,
 | 
			
		||||
                    tableId,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            changedTables.set(tableId, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check for removed indexes
 | 
			
		||||
    for (const oldIndex of oldIndexes) {
 | 
			
		||||
        if (!newIndexes.find((i) => i.id === oldIndex.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'index',
 | 
			
		||||
                    objectId: oldIndex.id,
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'index',
 | 
			
		||||
                    type: 'removed',
 | 
			
		||||
                    indexId: oldIndex.id,
 | 
			
		||||
                    tableId,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            changedTables.set(tableId, true);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Compare relationships between diagrams
 | 
			
		||||
function compareRelationships({
 | 
			
		||||
    diagram,
 | 
			
		||||
    newDiagram,
 | 
			
		||||
    diffMap,
 | 
			
		||||
}: {
 | 
			
		||||
    diagram: Diagram;
 | 
			
		||||
    newDiagram: Diagram;
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
}) {
 | 
			
		||||
    const oldRelationships = diagram.relationships || [];
 | 
			
		||||
    const newRelationships = newDiagram.relationships || [];
 | 
			
		||||
 | 
			
		||||
    // Check for added relationships
 | 
			
		||||
    for (const newRelationship of newRelationships) {
 | 
			
		||||
        if (!oldRelationships.find((r) => r.id === newRelationship.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'relationship',
 | 
			
		||||
                    objectId: newRelationship.id,
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'relationship',
 | 
			
		||||
                    type: 'added',
 | 
			
		||||
                    relationshipId: newRelationship.id,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Check for removed relationships
 | 
			
		||||
    for (const oldRelationship of oldRelationships) {
 | 
			
		||||
        if (!newRelationships.find((r) => r.id === oldRelationship.id)) {
 | 
			
		||||
            diffMap.set(
 | 
			
		||||
                getDiffMapKey({
 | 
			
		||||
                    diffObject: 'relationship',
 | 
			
		||||
                    objectId: oldRelationship.id,
 | 
			
		||||
                }),
 | 
			
		||||
                {
 | 
			
		||||
                    object: 'relationship',
 | 
			
		||||
                    type: 'removed',
 | 
			
		||||
                    relationshipId: oldRelationship.id,
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										75
									
								
								src/context/diff-context/diff-context.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/context/diff-context/diff-context.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
import { createContext } from 'react';
 | 
			
		||||
import type { DiffMap } from './types';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import type { EventEmitter } from 'ahooks/lib/useEventEmitter';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DataType } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
 | 
			
		||||
 | 
			
		||||
export type DiffEventType = 'diff_calculated';
 | 
			
		||||
 | 
			
		||||
export type DiffEventBase<T extends DiffEventType, D> = {
 | 
			
		||||
    action: T;
 | 
			
		||||
    data: D;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type DiffCalculatedEvent = DiffEventBase<
 | 
			
		||||
    'diff_calculated',
 | 
			
		||||
    {
 | 
			
		||||
        tablesAdded: DBTable[];
 | 
			
		||||
        fieldsAdded: Map<string, DBField[]>;
 | 
			
		||||
        relationshipsAdded: DBRelationship[];
 | 
			
		||||
    }
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export type DiffEvent = DiffCalculatedEvent;
 | 
			
		||||
 | 
			
		||||
export interface DiffContext {
 | 
			
		||||
    newDiagram: Diagram | null;
 | 
			
		||||
    diffMap: DiffMap;
 | 
			
		||||
    hasDiff: boolean;
 | 
			
		||||
 | 
			
		||||
    calculateDiff: ({
 | 
			
		||||
        diagram,
 | 
			
		||||
        newDiagram,
 | 
			
		||||
    }: {
 | 
			
		||||
        diagram: Diagram;
 | 
			
		||||
        newDiagram: Diagram;
 | 
			
		||||
    }) => void;
 | 
			
		||||
 | 
			
		||||
    // table diff
 | 
			
		||||
    checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
 | 
			
		||||
    checkIfNewTable: ({ tableId }: { tableId: string }) => boolean;
 | 
			
		||||
    checkIfTableRemoved: ({ tableId }: { tableId: string }) => boolean;
 | 
			
		||||
    getTableNewName: ({ tableId }: { tableId: string }) => string | null;
 | 
			
		||||
 | 
			
		||||
    // field diff
 | 
			
		||||
    checkIfFieldHasChange: ({
 | 
			
		||||
        tableId,
 | 
			
		||||
        fieldId,
 | 
			
		||||
    }: {
 | 
			
		||||
        tableId: string;
 | 
			
		||||
        fieldId: string;
 | 
			
		||||
    }) => boolean;
 | 
			
		||||
    checkIfFieldRemoved: ({ fieldId }: { fieldId: string }) => boolean;
 | 
			
		||||
    checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
 | 
			
		||||
    getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
 | 
			
		||||
    getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
 | 
			
		||||
 | 
			
		||||
    // relationship diff
 | 
			
		||||
    checkIfNewRelationship: ({
 | 
			
		||||
        relationshipId,
 | 
			
		||||
    }: {
 | 
			
		||||
        relationshipId: string;
 | 
			
		||||
    }) => boolean;
 | 
			
		||||
    checkIfRelationshipRemoved: ({
 | 
			
		||||
        relationshipId,
 | 
			
		||||
    }: {
 | 
			
		||||
        relationshipId: string;
 | 
			
		||||
    }) => boolean;
 | 
			
		||||
 | 
			
		||||
    events: EventEmitter<DiffEvent>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const diffContext = createContext<DiffContext | undefined>(undefined);
 | 
			
		||||
							
								
								
									
										327
									
								
								src/context/diff-context/diff-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										327
									
								
								src/context/diff-context/diff-provider.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,327 @@
 | 
			
		||||
import React, { useCallback } from 'react';
 | 
			
		||||
import type { DiffContext, DiffEvent } from './diff-context';
 | 
			
		||||
import { diffContext } from './diff-context';
 | 
			
		||||
import type { ChartDBDiff, DiffMap } from './types';
 | 
			
		||||
import { generateDiff, getDiffMapKey } from './diff-check/diff-check';
 | 
			
		||||
import type { Diagram } from '@/lib/domain/diagram';
 | 
			
		||||
import { useEventEmitter } from 'ahooks';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import type { DataType } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
 | 
			
		||||
 | 
			
		||||
export const DiffProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
}) => {
 | 
			
		||||
    const [newDiagram, setNewDiagram] = React.useState<Diagram | null>(null);
 | 
			
		||||
    const [diffMap, setDiffMap] = React.useState<DiffMap>(
 | 
			
		||||
        new Map<string, ChartDBDiff>()
 | 
			
		||||
    );
 | 
			
		||||
    const [tablesChanged, setTablesChanged] = React.useState<
 | 
			
		||||
        Map<string, boolean>
 | 
			
		||||
    >(new Map<string, boolean>());
 | 
			
		||||
    const [fieldsChanged, setFieldsChanged] = React.useState<
 | 
			
		||||
        Map<string, boolean>
 | 
			
		||||
    >(new Map<string, boolean>());
 | 
			
		||||
 | 
			
		||||
    const events = useEventEmitter<DiffEvent>();
 | 
			
		||||
 | 
			
		||||
    const generateNewFieldsMap = useCallback(
 | 
			
		||||
        ({
 | 
			
		||||
            diffMap,
 | 
			
		||||
            newDiagram,
 | 
			
		||||
        }: {
 | 
			
		||||
            diffMap: DiffMap;
 | 
			
		||||
            newDiagram: Diagram;
 | 
			
		||||
        }) => {
 | 
			
		||||
            const newFieldsMap = new Map<string, DBField[]>();
 | 
			
		||||
 | 
			
		||||
            diffMap.forEach((diff) => {
 | 
			
		||||
                if (diff.object === 'field' && diff.type === 'added') {
 | 
			
		||||
                    const field = newDiagram?.tables
 | 
			
		||||
                        ?.find((table) => table.id === diff.tableId)
 | 
			
		||||
                        ?.fields.find((f) => f.id === diff.fieldId);
 | 
			
		||||
 | 
			
		||||
                    if (field) {
 | 
			
		||||
                        newFieldsMap.set(diff.tableId, [
 | 
			
		||||
                            ...(newFieldsMap.get(diff.tableId) ?? []),
 | 
			
		||||
                            field,
 | 
			
		||||
                        ]);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return newFieldsMap;
 | 
			
		||||
        },
 | 
			
		||||
        []
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const findNewRelationships = useCallback(
 | 
			
		||||
        ({
 | 
			
		||||
            diffMap,
 | 
			
		||||
            newDiagram,
 | 
			
		||||
        }: {
 | 
			
		||||
            diffMap: DiffMap;
 | 
			
		||||
            newDiagram: Diagram;
 | 
			
		||||
        }) => {
 | 
			
		||||
            const relationships: DBRelationship[] = [];
 | 
			
		||||
            diffMap.forEach((diff) => {
 | 
			
		||||
                if (diff.object === 'relationship' && diff.type === 'added') {
 | 
			
		||||
                    const relationship = newDiagram?.relationships?.find(
 | 
			
		||||
                        (rel) => rel.id === diff.relationshipId
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    if (relationship) {
 | 
			
		||||
                        relationships.push(relationship);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return relationships;
 | 
			
		||||
        },
 | 
			
		||||
        []
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const calculateDiff: DiffContext['calculateDiff'] = useCallback(
 | 
			
		||||
        ({ diagram, newDiagram: newDiagramArg }) => {
 | 
			
		||||
            const {
 | 
			
		||||
                diffMap: newDiffs,
 | 
			
		||||
                changedTables: newChangedTables,
 | 
			
		||||
                changedFields: newChangedFields,
 | 
			
		||||
            } = generateDiff({ diagram, newDiagram: newDiagramArg });
 | 
			
		||||
 | 
			
		||||
            setDiffMap(newDiffs);
 | 
			
		||||
            setTablesChanged(newChangedTables);
 | 
			
		||||
            setFieldsChanged(newChangedFields);
 | 
			
		||||
            setNewDiagram(newDiagramArg);
 | 
			
		||||
 | 
			
		||||
            events.emit({
 | 
			
		||||
                action: 'diff_calculated',
 | 
			
		||||
                data: {
 | 
			
		||||
                    tablesAdded:
 | 
			
		||||
                        newDiagramArg?.tables?.filter((table) => {
 | 
			
		||||
                            const tableKey = getDiffMapKey({
 | 
			
		||||
                                diffObject: 'table',
 | 
			
		||||
                                objectId: table.id,
 | 
			
		||||
                            });
 | 
			
		||||
 | 
			
		||||
                            return (
 | 
			
		||||
                                newDiffs.has(tableKey) &&
 | 
			
		||||
                                newDiffs.get(tableKey)?.type === 'added'
 | 
			
		||||
                            );
 | 
			
		||||
                        }) ?? [],
 | 
			
		||||
 | 
			
		||||
                    fieldsAdded: generateNewFieldsMap({
 | 
			
		||||
                        diffMap: newDiffs,
 | 
			
		||||
                        newDiagram: newDiagramArg,
 | 
			
		||||
                    }),
 | 
			
		||||
                    relationshipsAdded: findNewRelationships({
 | 
			
		||||
                        diffMap: newDiffs,
 | 
			
		||||
                        newDiagram: newDiagramArg,
 | 
			
		||||
                    }),
 | 
			
		||||
                },
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [setDiffMap, events, generateNewFieldsMap, findNewRelationships]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getTableNewName = useCallback<DiffContext['getTableNewName']>(
 | 
			
		||||
        ({ tableId }) => {
 | 
			
		||||
            const tableNameKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'table',
 | 
			
		||||
                objectId: tableId,
 | 
			
		||||
                attribute: 'name',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diffMap.has(tableNameKey)) {
 | 
			
		||||
                const diff = diffMap.get(tableNameKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as string;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const checkIfTableHasChange = useCallback<
 | 
			
		||||
        DiffContext['checkIfTableHasChange']
 | 
			
		||||
    >(({ tableId }) => tablesChanged.get(tableId) ?? false, [tablesChanged]);
 | 
			
		||||
 | 
			
		||||
    const checkIfNewTable = useCallback<DiffContext['checkIfNewTable']>(
 | 
			
		||||
        ({ tableId }) => {
 | 
			
		||||
            const tableKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'table',
 | 
			
		||||
                objectId: tableId,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
                diffMap.has(tableKey) && diffMap.get(tableKey)?.type === 'added'
 | 
			
		||||
            );
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const checkIfTableRemoved = useCallback<DiffContext['checkIfTableRemoved']>(
 | 
			
		||||
        ({ tableId }) => {
 | 
			
		||||
            const tableKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'table',
 | 
			
		||||
                objectId: tableId,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
                diffMap.has(tableKey) &&
 | 
			
		||||
                diffMap.get(tableKey)?.type === 'removed'
 | 
			
		||||
            );
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const checkIfFieldHasChange = useCallback<
 | 
			
		||||
        DiffContext['checkIfFieldHasChange']
 | 
			
		||||
    >(
 | 
			
		||||
        ({ fieldId }) => {
 | 
			
		||||
            return fieldsChanged.get(fieldId) ?? false;
 | 
			
		||||
        },
 | 
			
		||||
        [fieldsChanged]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const checkIfFieldRemoved = useCallback<DiffContext['checkIfFieldRemoved']>(
 | 
			
		||||
        ({ fieldId }) => {
 | 
			
		||||
            const fieldKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'field',
 | 
			
		||||
                objectId: fieldId,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
                diffMap.has(fieldKey) &&
 | 
			
		||||
                diffMap.get(fieldKey)?.type === 'removed'
 | 
			
		||||
            );
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const checkIfNewField = useCallback<DiffContext['checkIfNewField']>(
 | 
			
		||||
        ({ fieldId }) => {
 | 
			
		||||
            const fieldKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'field',
 | 
			
		||||
                objectId: fieldId,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
                diffMap.has(fieldKey) && diffMap.get(fieldKey)?.type === 'added'
 | 
			
		||||
            );
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getFieldNewName = useCallback<DiffContext['getFieldNewName']>(
 | 
			
		||||
        ({ fieldId }) => {
 | 
			
		||||
            const fieldKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'field',
 | 
			
		||||
                objectId: fieldId,
 | 
			
		||||
                attribute: 'name',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diffMap.has(fieldKey)) {
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as string;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const getFieldNewType = useCallback<DiffContext['getFieldNewType']>(
 | 
			
		||||
        ({ fieldId }) => {
 | 
			
		||||
            const fieldKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'field',
 | 
			
		||||
                objectId: fieldId,
 | 
			
		||||
                attribute: 'type',
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            if (diffMap.has(fieldKey)) {
 | 
			
		||||
                const diff = diffMap.get(fieldKey);
 | 
			
		||||
 | 
			
		||||
                if (diff?.type === 'changed') {
 | 
			
		||||
                    return diff.newValue as DataType;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return null;
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const checkIfNewRelationship = useCallback<
 | 
			
		||||
        DiffContext['checkIfNewRelationship']
 | 
			
		||||
    >(
 | 
			
		||||
        ({ relationshipId }) => {
 | 
			
		||||
            const relationshipKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'relationship',
 | 
			
		||||
                objectId: relationshipId,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
                diffMap.has(relationshipKey) &&
 | 
			
		||||
                diffMap.get(relationshipKey)?.type === 'added'
 | 
			
		||||
            );
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const checkIfRelationshipRemoved = useCallback<
 | 
			
		||||
        DiffContext['checkIfRelationshipRemoved']
 | 
			
		||||
    >(
 | 
			
		||||
        ({ relationshipId }) => {
 | 
			
		||||
            const relationshipKey = getDiffMapKey({
 | 
			
		||||
                diffObject: 'relationship',
 | 
			
		||||
                objectId: relationshipId,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
                diffMap.has(relationshipKey) &&
 | 
			
		||||
                diffMap.get(relationshipKey)?.type === 'removed'
 | 
			
		||||
            );
 | 
			
		||||
        },
 | 
			
		||||
        [diffMap]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <diffContext.Provider
 | 
			
		||||
            value={{
 | 
			
		||||
                newDiagram,
 | 
			
		||||
                diffMap,
 | 
			
		||||
                hasDiff: diffMap.size > 0,
 | 
			
		||||
 | 
			
		||||
                calculateDiff,
 | 
			
		||||
 | 
			
		||||
                // table diff
 | 
			
		||||
                getTableNewName,
 | 
			
		||||
                checkIfNewTable,
 | 
			
		||||
                checkIfTableRemoved,
 | 
			
		||||
                checkIfTableHasChange,
 | 
			
		||||
 | 
			
		||||
                // field diff
 | 
			
		||||
                checkIfFieldHasChange,
 | 
			
		||||
                checkIfFieldRemoved,
 | 
			
		||||
                checkIfNewField,
 | 
			
		||||
                getFieldNewName,
 | 
			
		||||
                getFieldNewType,
 | 
			
		||||
 | 
			
		||||
                // relationship diff
 | 
			
		||||
                checkIfNewRelationship,
 | 
			
		||||
                checkIfRelationshipRemoved,
 | 
			
		||||
 | 
			
		||||
                events,
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            {children}
 | 
			
		||||
        </diffContext.Provider>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										53
									
								
								src/context/diff-context/types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/context/diff-context/types.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
import type { DataType } from '@/lib/data/data-types/data-types';
 | 
			
		||||
 | 
			
		||||
export type TableDiffAttribute = 'name' | 'comments';
 | 
			
		||||
 | 
			
		||||
export interface TableDiff {
 | 
			
		||||
    object: 'table';
 | 
			
		||||
    type: 'added' | 'removed' | 'changed';
 | 
			
		||||
    tableId: string;
 | 
			
		||||
    attributes?: TableDiffAttribute;
 | 
			
		||||
    oldValue?: string;
 | 
			
		||||
    newValue?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RelationshipDiff {
 | 
			
		||||
    object: 'relationship';
 | 
			
		||||
    type: 'added' | 'removed';
 | 
			
		||||
    relationshipId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type FieldDiffAttribute =
 | 
			
		||||
    | 'name'
 | 
			
		||||
    | 'type'
 | 
			
		||||
    | 'primaryKey'
 | 
			
		||||
    | 'unique'
 | 
			
		||||
    | 'nullable'
 | 
			
		||||
    | 'comments';
 | 
			
		||||
 | 
			
		||||
export interface FieldDiff {
 | 
			
		||||
    object: 'field';
 | 
			
		||||
    type: 'added' | 'removed' | 'changed';
 | 
			
		||||
    fieldId: string;
 | 
			
		||||
    tableId: string;
 | 
			
		||||
    attributes?: FieldDiffAttribute;
 | 
			
		||||
    oldValue?: string | boolean | DataType;
 | 
			
		||||
    newValue?: string | boolean | DataType;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IndexDiff {
 | 
			
		||||
    object: 'index';
 | 
			
		||||
    type: 'added' | 'removed';
 | 
			
		||||
    indexId: string;
 | 
			
		||||
    tableId: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type ChartDBDiff = TableDiff | FieldDiff | IndexDiff | RelationshipDiff;
 | 
			
		||||
 | 
			
		||||
export type DiffMap = Map<string, ChartDBDiff>;
 | 
			
		||||
 | 
			
		||||
export type DiffObject =
 | 
			
		||||
    | TableDiff['object']
 | 
			
		||||
    | FieldDiff['object']
 | 
			
		||||
    | IndexDiff['object']
 | 
			
		||||
    | RelationshipDiff['object'];
 | 
			
		||||
							
								
								
									
										10
									
								
								src/context/diff-context/use-diff.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/context/diff-context/use-diff.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
import { useContext } from 'react';
 | 
			
		||||
import { diffContext } from './diff-context';
 | 
			
		||||
 | 
			
		||||
export const useDiff = () => {
 | 
			
		||||
    const context = useContext(diffContext);
 | 
			
		||||
    if (context === undefined) {
 | 
			
		||||
        throw new Error('useDiff must be used within an DiffProvider');
 | 
			
		||||
    }
 | 
			
		||||
    return context;
 | 
			
		||||
};
 | 
			
		||||
@@ -7,6 +7,7 @@ export enum KeyboardShortcutAction {
 | 
			
		||||
    SAVE_DIAGRAM = 'save_diagram',
 | 
			
		||||
    TOGGLE_SIDE_PANEL = 'toggle_side_panel',
 | 
			
		||||
    SHOW_ALL = 'show_all',
 | 
			
		||||
    TOGGLE_THEME = 'toggle_theme',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface KeyboardShortcut {
 | 
			
		||||
@@ -63,6 +64,13 @@ export const keyboardShortcuts: Record<
 | 
			
		||||
        keyCombinationMac: 'meta+0',
 | 
			
		||||
        keyCombinationWin: 'ctrl+0',
 | 
			
		||||
    },
 | 
			
		||||
    [KeyboardShortcutAction.TOGGLE_THEME]: {
 | 
			
		||||
        action: KeyboardShortcutAction.TOGGLE_THEME,
 | 
			
		||||
        keyCombinationLabelMac: '⌘M',
 | 
			
		||||
        keyCombinationLabelWin: 'Ctrl+M',
 | 
			
		||||
        keyCombinationMac: 'meta+m',
 | 
			
		||||
        keyCombinationWin: 'ctrl+m',
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface KeyboardShortcutForOS {
 | 
			
		||||
 
 | 
			
		||||
@@ -30,12 +30,6 @@ export interface LocalConfigContext {
 | 
			
		||||
    starUsDialogLastOpen: number;
 | 
			
		||||
    setStarUsDialogLastOpen: (lastOpen: number) => void;
 | 
			
		||||
 | 
			
		||||
    buckleWaitlistOpened: boolean;
 | 
			
		||||
    setBuckleWaitlistOpened: (githubRepoOpened: boolean) => void;
 | 
			
		||||
 | 
			
		||||
    buckleDialogLastOpen: number;
 | 
			
		||||
    setBuckleDialogLastOpen: (lastOpen: number) => void;
 | 
			
		||||
 | 
			
		||||
    showDependenciesOnCanvas: boolean;
 | 
			
		||||
    setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
 | 
			
		||||
 | 
			
		||||
@@ -53,7 +47,7 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
 | 
			
		||||
    schemasFilter: {},
 | 
			
		||||
    setSchemasFilter: emptyFn,
 | 
			
		||||
 | 
			
		||||
    showCardinality: false,
 | 
			
		||||
    showCardinality: true,
 | 
			
		||||
    setShowCardinality: emptyFn,
 | 
			
		||||
 | 
			
		||||
    hideMultiSchemaNotification: false,
 | 
			
		||||
@@ -65,12 +59,6 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
 | 
			
		||||
    starUsDialogLastOpen: 0,
 | 
			
		||||
    setStarUsDialogLastOpen: emptyFn,
 | 
			
		||||
 | 
			
		||||
    buckleWaitlistOpened: false,
 | 
			
		||||
    setBuckleWaitlistOpened: emptyFn,
 | 
			
		||||
 | 
			
		||||
    buckleDialogLastOpen: 0,
 | 
			
		||||
    setBuckleDialogLastOpen: emptyFn,
 | 
			
		||||
 | 
			
		||||
    showDependenciesOnCanvas: false,
 | 
			
		||||
    setShowDependenciesOnCanvas: emptyFn,
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,6 @@ const showCardinalityKey = 'show_cardinality';
 | 
			
		||||
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
 | 
			
		||||
const githubRepoOpenedKey = 'github_repo_opened';
 | 
			
		||||
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
 | 
			
		||||
const buckleWaitlistOpenedKey = 'buckle_waitlist_opened';
 | 
			
		||||
const buckleDialogLastOpenKey = 'buckle_dialog_last_open';
 | 
			
		||||
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
 | 
			
		||||
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
 | 
			
		||||
 | 
			
		||||
@@ -33,7 +31,7 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const [showCardinality, setShowCardinality] = React.useState<boolean>(
 | 
			
		||||
        (localStorage.getItem(showCardinalityKey) || 'false') === 'true'
 | 
			
		||||
        (localStorage.getItem(showCardinalityKey) || 'true') === 'true'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
 | 
			
		||||
@@ -51,17 +49,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
            parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const [buckleWaitlistOpened, setBuckleWaitlistOpened] =
 | 
			
		||||
        React.useState<boolean>(
 | 
			
		||||
            (localStorage.getItem(buckleWaitlistOpenedKey) || 'false') ===
 | 
			
		||||
                'true'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const [buckleDialogLastOpen, setBuckleDialogLastOpen] =
 | 
			
		||||
        React.useState<number>(
 | 
			
		||||
            parseInt(localStorage.getItem(buckleDialogLastOpenKey) || '0')
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
    const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
 | 
			
		||||
        React.useState<boolean>(
 | 
			
		||||
            (localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
 | 
			
		||||
@@ -84,20 +71,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
 | 
			
		||||
    }, [githubRepoOpened]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        localStorage.setItem(
 | 
			
		||||
            buckleDialogLastOpenKey,
 | 
			
		||||
            buckleDialogLastOpen.toString()
 | 
			
		||||
        );
 | 
			
		||||
    }, [buckleDialogLastOpen]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        localStorage.setItem(
 | 
			
		||||
            buckleWaitlistOpenedKey,
 | 
			
		||||
            buckleWaitlistOpened.toString()
 | 
			
		||||
        );
 | 
			
		||||
    }, [buckleWaitlistOpened]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        localStorage.setItem(
 | 
			
		||||
            hideMultiSchemaNotificationKey,
 | 
			
		||||
@@ -154,10 +127,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
                setStarUsDialogLastOpen,
 | 
			
		||||
                showDependenciesOnCanvas,
 | 
			
		||||
                setShowDependenciesOnCanvas,
 | 
			
		||||
                setBuckleDialogLastOpen,
 | 
			
		||||
                buckleDialogLastOpen,
 | 
			
		||||
                buckleWaitlistOpened,
 | 
			
		||||
                setBuckleWaitlistOpened,
 | 
			
		||||
                showMiniMapOnCanvas,
 | 
			
		||||
                setShowMiniMapOnCanvas,
 | 
			
		||||
            }}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,13 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import React, { useEffect, useState, useCallback } from 'react';
 | 
			
		||||
import type { EffectiveTheme } from './theme-context';
 | 
			
		||||
import { ThemeContext } from './theme-context';
 | 
			
		||||
import { useMediaQuery } from 'react-responsive';
 | 
			
		||||
import { useLocalConfig } from '@/hooks/use-local-config';
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook';
 | 
			
		||||
import {
 | 
			
		||||
    KeyboardShortcutAction,
 | 
			
		||||
    keyboardShortcutsForOS,
 | 
			
		||||
} from '../keyboard-shortcuts-context/keyboard-shortcuts';
 | 
			
		||||
 | 
			
		||||
export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
    children,
 | 
			
		||||
@@ -29,6 +34,24 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
 | 
			
		||||
        }
 | 
			
		||||
    }, [effectiveTheme]);
 | 
			
		||||
 | 
			
		||||
    const handleThemeToggle = useCallback(() => {
 | 
			
		||||
        if (theme === 'system') {
 | 
			
		||||
            setTheme(effectiveTheme === 'dark' ? 'light' : 'dark');
 | 
			
		||||
        } else {
 | 
			
		||||
            setTheme(theme === 'dark' ? 'light' : 'dark');
 | 
			
		||||
        }
 | 
			
		||||
    }, [theme, effectiveTheme, setTheme]);
 | 
			
		||||
 | 
			
		||||
    useHotkeys(
 | 
			
		||||
        keyboardShortcutsForOS[KeyboardShortcutAction.TOGGLE_THEME]
 | 
			
		||||
            .keyCombination,
 | 
			
		||||
        handleThemeToggle,
 | 
			
		||||
        {
 | 
			
		||||
            preventDefault: true,
 | 
			
		||||
        },
 | 
			
		||||
        [handleThemeToggle]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}>
 | 
			
		||||
            {children}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,80 +0,0 @@
 | 
			
		||||
import React, { useCallback, useEffect } from 'react';
 | 
			
		||||
import { useDialog } from '@/hooks/use-dialog';
 | 
			
		||||
import {
 | 
			
		||||
    Dialog,
 | 
			
		||||
    DialogClose,
 | 
			
		||||
    DialogContent,
 | 
			
		||||
    DialogDescription,
 | 
			
		||||
    DialogFooter,
 | 
			
		||||
    DialogHeader,
 | 
			
		||||
    DialogTitle,
 | 
			
		||||
} from '@/components/dialog/dialog';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import type { BaseDialogProps } from '../common/base-dialog-props';
 | 
			
		||||
import { useLocalConfig } from '@/hooks/use-local-config';
 | 
			
		||||
import { useTheme } from '@/hooks/use-theme';
 | 
			
		||||
 | 
			
		||||
export interface BuckleDialogProps extends BaseDialogProps {}
 | 
			
		||||
 | 
			
		||||
export const BuckleDialog: React.FC<BuckleDialogProps> = ({ dialog }) => {
 | 
			
		||||
    const { setBuckleWaitlistOpened } = useLocalConfig();
 | 
			
		||||
    const { effectiveTheme } = useTheme();
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!dialog.open) return;
 | 
			
		||||
    }, [dialog.open]);
 | 
			
		||||
    const { closeBuckleDialog } = useDialog();
 | 
			
		||||
 | 
			
		||||
    const handleConfirm = useCallback(() => {
 | 
			
		||||
        setBuckleWaitlistOpened(true);
 | 
			
		||||
        window.open('https://waitlist.buckle.dev', '_blank');
 | 
			
		||||
    }, [setBuckleWaitlistOpened]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Dialog
 | 
			
		||||
            {...dialog}
 | 
			
		||||
            onOpenChange={(open) => {
 | 
			
		||||
                if (!open) {
 | 
			
		||||
                    closeBuckleDialog();
 | 
			
		||||
                }
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <DialogContent
 | 
			
		||||
                className="flex flex-col"
 | 
			
		||||
                showClose={false}
 | 
			
		||||
                onInteractOutside={(e) => {
 | 
			
		||||
                    e.preventDefault();
 | 
			
		||||
                }}
 | 
			
		||||
            >
 | 
			
		||||
                <DialogHeader>
 | 
			
		||||
                    <DialogTitle className="hidden" />
 | 
			
		||||
                    <DialogDescription className="hidden" />
 | 
			
		||||
                </DialogHeader>
 | 
			
		||||
                <div className="flex w-full flex-col items-center">
 | 
			
		||||
                    <img
 | 
			
		||||
                        src={
 | 
			
		||||
                            effectiveTheme === 'light'
 | 
			
		||||
                                ? '/buckle-animated.gif'
 | 
			
		||||
                                : '/buckle.png'
 | 
			
		||||
                        }
 | 
			
		||||
                        className="h-16"
 | 
			
		||||
                    />
 | 
			
		||||
                    <div className="mt-6 text-center text-base">
 | 
			
		||||
                        We've been working on something big -{' '}
 | 
			
		||||
                        <span className="font-semibold">Ready to explore?</span>
 | 
			
		||||
                    </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <DialogFooter className="flex gap-1 md:justify-between">
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
                        <Button variant="secondary">Not now</Button>
 | 
			
		||||
                    </DialogClose>
 | 
			
		||||
                    <DialogClose asChild>
 | 
			
		||||
                        <Button onClick={handleConfirm}>
 | 
			
		||||
                            Try ChartDB v2.0!
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </DialogClose>
 | 
			
		||||
                </DialogFooter>
 | 
			
		||||
            </DialogContent>
 | 
			
		||||
        </Dialog>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										47
									
								
								src/hooks/use-debounce-v2.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/hooks/use-debounce-v2.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
			
		||||
import { useEffect, useRef, useCallback } from 'react';
 | 
			
		||||
import { debounce as utilsDebounce } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
interface DebouncedFunction {
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
    (...args: any[]): void;
 | 
			
		||||
    cancel?: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A hook that returns a debounced version of the provided function.
 | 
			
		||||
 * The debounced function will only be called after the specified delay
 | 
			
		||||
 * has passed without the function being called again.
 | 
			
		||||
 *
 | 
			
		||||
 * @param callback The function to debounce
 | 
			
		||||
 * @param delay The delay in milliseconds
 | 
			
		||||
 * @returns A debounced version of the callback
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
export function useDebounce<T extends (...args: any[]) => any>(
 | 
			
		||||
    callback: T,
 | 
			
		||||
    delay: number
 | 
			
		||||
): (...args: Parameters<T>) => void {
 | 
			
		||||
    // Use a ref to store the debounced function
 | 
			
		||||
    const debouncedFnRef = useRef<DebouncedFunction>();
 | 
			
		||||
 | 
			
		||||
    // Update the debounced function when dependencies change
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        // Create the debounced function
 | 
			
		||||
        debouncedFnRef.current = utilsDebounce(callback, delay);
 | 
			
		||||
 | 
			
		||||
        // Clean up when component unmounts or dependencies change
 | 
			
		||||
        return () => {
 | 
			
		||||
            if (debouncedFnRef.current?.cancel) {
 | 
			
		||||
                debouncedFnRef.current.cancel();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
    }, [callback, delay]);
 | 
			
		||||
 | 
			
		||||
    // Create a stable callback that uses the ref
 | 
			
		||||
    const debouncedCallback = useCallback((...args: Parameters<T>) => {
 | 
			
		||||
        debouncedFnRef.current?.(...args);
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    return debouncedCallback;
 | 
			
		||||
}
 | 
			
		||||
@@ -151,6 +151,8 @@ export const ar: LanguageTranslation = {
 | 
			
		||||
                        comments: 'تعليقات',
 | 
			
		||||
                        no_comments: 'لا يوجد تعليقات',
 | 
			
		||||
                        delete_field: 'حذف الحقل',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'خصائص الفهرس',
 | 
			
		||||
 
 | 
			
		||||
@@ -152,6 +152,8 @@ export const bn: LanguageTranslation = {
 | 
			
		||||
                        comments: 'মন্তব্য',
 | 
			
		||||
                        no_comments: 'কোনো মন্তব্য নেই',
 | 
			
		||||
                        delete_field: 'ফিল্ড মুছুন',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'ইনডেক্স কর্ম',
 | 
			
		||||
 
 | 
			
		||||
@@ -153,6 +153,8 @@ export const de: LanguageTranslation = {
 | 
			
		||||
                        comments: 'Kommentare',
 | 
			
		||||
                        no_comments: 'Keine Kommentare',
 | 
			
		||||
                        delete_field: 'Feld löschen',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'Indexattribute',
 | 
			
		||||
 
 | 
			
		||||
@@ -145,6 +145,7 @@ export const en = {
 | 
			
		||||
                    field_actions: {
 | 
			
		||||
                        title: 'Field Attributes',
 | 
			
		||||
                        unique: 'Unique',
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                        comments: 'Comments',
 | 
			
		||||
                        no_comments: 'No comments',
 | 
			
		||||
                        delete_field: 'Delete Field',
 | 
			
		||||
 
 | 
			
		||||
@@ -142,6 +142,8 @@ export const es: LanguageTranslation = {
 | 
			
		||||
                        comments: 'Comentarios',
 | 
			
		||||
                        no_comments: 'Sin comentarios',
 | 
			
		||||
                        delete_field: 'Eliminar Campo',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'Atributos del Índice',
 | 
			
		||||
 
 | 
			
		||||
@@ -140,6 +140,8 @@ export const fr: LanguageTranslation = {
 | 
			
		||||
                        comments: 'Commentaires',
 | 
			
		||||
                        no_comments: 'Pas de commentaires',
 | 
			
		||||
                        delete_field: 'Supprimer le Champ',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: "Attributs de l'Index",
 | 
			
		||||
 
 | 
			
		||||
@@ -153,6 +153,8 @@ export const gu: LanguageTranslation = {
 | 
			
		||||
                        comments: 'ટિપ્પણીઓ',
 | 
			
		||||
                        no_comments: 'કોઈ ટિપ્પણીઓ નથી',
 | 
			
		||||
                        delete_field: 'ફીલ્ડ કાઢી નાખો',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'ઇન્ડેક્સ લક્ષણો',
 | 
			
		||||
 
 | 
			
		||||
@@ -152,6 +152,8 @@ export const hi: LanguageTranslation = {
 | 
			
		||||
                        comments: 'टिप्पणियाँ',
 | 
			
		||||
                        no_comments: 'कोई टिप्पणी नहीं',
 | 
			
		||||
                        delete_field: 'फ़ील्ड हटाएँ',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'सूचकांक विशेषताएँ',
 | 
			
		||||
 
 | 
			
		||||
@@ -151,6 +151,8 @@ export const id_ID: LanguageTranslation = {
 | 
			
		||||
                        comments: 'Komentar',
 | 
			
		||||
                        no_comments: 'Tidak ada komentar',
 | 
			
		||||
                        delete_field: 'Hapus Kolom',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'Atribut Indeks',
 | 
			
		||||
 
 | 
			
		||||
@@ -155,6 +155,8 @@ export const ja: LanguageTranslation = {
 | 
			
		||||
                        comments: 'コメント',
 | 
			
		||||
                        no_comments: 'コメントがありません',
 | 
			
		||||
                        delete_field: 'フィールドを削除',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'インデックス属性',
 | 
			
		||||
 
 | 
			
		||||
@@ -151,6 +151,8 @@ export const ko_KR: LanguageTranslation = {
 | 
			
		||||
                        comments: '주석',
 | 
			
		||||
                        no_comments: '주석 없음',
 | 
			
		||||
                        delete_field: '필드 삭제',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: '인덱스 속성',
 | 
			
		||||
 
 | 
			
		||||
@@ -154,6 +154,8 @@ export const mr: LanguageTranslation = {
 | 
			
		||||
                        comments: 'टिप्पण्या',
 | 
			
		||||
                        no_comments: 'कोणत्याही टिप्पणी नाहीत',
 | 
			
		||||
                        delete_field: 'फील्ड हटवा',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'इंडेक्स गुणधर्म',
 | 
			
		||||
 
 | 
			
		||||
@@ -152,6 +152,8 @@ export const ne: LanguageTranslation = {
 | 
			
		||||
                        comments: 'टिप्पणीहरू',
 | 
			
		||||
                        no_comments: 'कुनै टिप्पणीहरू छैनन्',
 | 
			
		||||
                        delete_field: 'क्षेत्र हटाउनुहोस्',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'सूचक विशेषताहरू',
 | 
			
		||||
 
 | 
			
		||||
@@ -152,6 +152,8 @@ export const pt_BR: LanguageTranslation = {
 | 
			
		||||
                        comments: 'Comentários',
 | 
			
		||||
                        no_comments: 'Sem comentários',
 | 
			
		||||
                        delete_field: 'Excluir Campo',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'Atributos do Índice',
 | 
			
		||||
 
 | 
			
		||||
@@ -151,6 +151,8 @@ export const ru: LanguageTranslation = {
 | 
			
		||||
                        comments: 'Комментарии',
 | 
			
		||||
                        no_comments: 'Нет комментария',
 | 
			
		||||
                        delete_field: 'Удалить поле',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'Атрибуты индекса',
 | 
			
		||||
 
 | 
			
		||||
@@ -152,6 +152,8 @@ export const te: LanguageTranslation = {
 | 
			
		||||
                        comments: 'వ్యాఖ్యలు',
 | 
			
		||||
                        no_comments: 'వ్యాఖ్యలు లేవు',
 | 
			
		||||
                        delete_field: 'ఫీల్డ్ తొలగించు',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'ఇండెక్స్ గుణాలు',
 | 
			
		||||
 
 | 
			
		||||
@@ -151,6 +151,8 @@ export const tr: LanguageTranslation = {
 | 
			
		||||
                        comments: 'Yorumlar',
 | 
			
		||||
                        no_comments: 'Yorum yok',
 | 
			
		||||
                        delete_field: 'Alanı Sil',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'İndeks Özellikleri',
 | 
			
		||||
 
 | 
			
		||||
@@ -150,6 +150,8 @@ export const uk: LanguageTranslation = {
 | 
			
		||||
                        comments: 'Коментарі',
 | 
			
		||||
                        no_comments: 'Немає коментарів',
 | 
			
		||||
                        delete_field: 'Видалити поле',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'Атрибути індексу',
 | 
			
		||||
 
 | 
			
		||||
@@ -151,6 +151,8 @@ export const vi: LanguageTranslation = {
 | 
			
		||||
                        comments: 'Bình luận',
 | 
			
		||||
                        no_comments: 'Không có bình luận',
 | 
			
		||||
                        delete_field: 'Xóa trường',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: 'Thuộc tính chỉ mục',
 | 
			
		||||
 
 | 
			
		||||
@@ -148,6 +148,8 @@ export const zh_CN: LanguageTranslation = {
 | 
			
		||||
                        comments: '注释',
 | 
			
		||||
                        no_comments: '空',
 | 
			
		||||
                        delete_field: '删除字段',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: '索引属性',
 | 
			
		||||
 
 | 
			
		||||
@@ -148,6 +148,8 @@ export const zh_TW: LanguageTranslation = {
 | 
			
		||||
                        comments: '註解',
 | 
			
		||||
                        no_comments: '無註解',
 | 
			
		||||
                        delete_field: '刪除欄位',
 | 
			
		||||
                        // TODO: Translate
 | 
			
		||||
                        character_length: 'Max Length',
 | 
			
		||||
                    },
 | 
			
		||||
                    index_actions: {
 | 
			
		||||
                        title: '索引屬性',
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import type { DataType } from './data-types';
 | 
			
		||||
import type { DataTypeData } from './data-types';
 | 
			
		||||
 | 
			
		||||
export const clickhouseDataTypes: readonly DataType[] = [
 | 
			
		||||
export const clickhouseDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    // Numeric Types
 | 
			
		||||
    { name: 'uint8', id: 'uint8' },
 | 
			
		||||
    { name: 'uint16', id: 'uint16' },
 | 
			
		||||
@@ -48,25 +48,41 @@ export const clickhouseDataTypes: readonly DataType[] = [
 | 
			
		||||
    { name: 'mediumblob', id: 'mediumblob' },
 | 
			
		||||
    { name: 'tinyblob', id: 'tinyblob' },
 | 
			
		||||
    { name: 'blob', id: 'blob' },
 | 
			
		||||
    { name: 'varchar', id: 'varchar' },
 | 
			
		||||
    { name: 'char', id: 'char' },
 | 
			
		||||
    { name: 'varchar', id: 'varchar', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'char', id: 'char', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'char large object', id: 'char_large_object' },
 | 
			
		||||
    { name: 'char varying', id: 'char_varying' },
 | 
			
		||||
    { name: 'char varying', id: 'char_varying', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'character large object', id: 'character_large_object' },
 | 
			
		||||
    { name: 'character varying', id: 'character_varying' },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'character varying',
 | 
			
		||||
        id: 'character_varying',
 | 
			
		||||
        hasCharMaxLength: true,
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'nchar large object', id: 'nchar_large_object' },
 | 
			
		||||
    { name: 'nchar varying', id: 'nchar_varying' },
 | 
			
		||||
    { name: 'nchar varying', id: 'nchar_varying', hasCharMaxLength: true },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'national character large object',
 | 
			
		||||
        id: 'national_character_large_object',
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'national character varying', id: 'national_character_varying' },
 | 
			
		||||
    { name: 'national char varying', id: 'national_char_varying' },
 | 
			
		||||
    { name: 'national character', id: 'national_character' },
 | 
			
		||||
    { name: 'national char', id: 'national_char' },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'national character varying',
 | 
			
		||||
        id: 'national_character_varying',
 | 
			
		||||
        hasCharMaxLength: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'national char varying',
 | 
			
		||||
        id: 'national_char_varying',
 | 
			
		||||
        hasCharMaxLength: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'national character',
 | 
			
		||||
        id: 'national_character',
 | 
			
		||||
        hasCharMaxLength: true,
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'national char', id: 'national_char', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'binary large object', id: 'binary_large_object' },
 | 
			
		||||
    { name: 'binary varying', id: 'binary_varying' },
 | 
			
		||||
    { name: 'fixedstring', id: 'fixedstring' },
 | 
			
		||||
    { name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'string', id: 'string' },
 | 
			
		||||
 | 
			
		||||
    // Date Types
 | 
			
		||||
 
 | 
			
		||||
@@ -13,12 +13,16 @@ export interface DataType {
 | 
			
		||||
    name: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DataTypeData extends DataType {
 | 
			
		||||
    hasCharMaxLength?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const dataTypeSchema: z.ZodType<DataType> = z.object({
 | 
			
		||||
    id: z.string(),
 | 
			
		||||
    name: z.string(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const dataTypeMap: Record<DatabaseType, readonly DataType[]> = {
 | 
			
		||||
export const dataTypeMap: Record<DatabaseType, readonly DataTypeData[]> = {
 | 
			
		||||
    [DatabaseType.GENERIC]: genericDataTypes,
 | 
			
		||||
    [DatabaseType.POSTGRESQL]: postgresDataTypes,
 | 
			
		||||
    [DatabaseType.MYSQL]: mysqlDataTypes,
 | 
			
		||||
@@ -64,3 +68,21 @@ export function areFieldTypesCompatible(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const dataTypes = Object.values(dataTypeMap).flat();
 | 
			
		||||
 | 
			
		||||
export const dataTypeDataToDataType = (
 | 
			
		||||
    dataTypeData: DataTypeData
 | 
			
		||||
): DataType => ({
 | 
			
		||||
    id: dataTypeData.id,
 | 
			
		||||
    name: dataTypeData.name,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const findDataTypeDataById = (
 | 
			
		||||
    id: string,
 | 
			
		||||
    databaseType?: DatabaseType
 | 
			
		||||
): DataTypeData | undefined => {
 | 
			
		||||
    const dataTypesOptions = databaseType
 | 
			
		||||
        ? dataTypeMap[databaseType]
 | 
			
		||||
        : dataTypes;
 | 
			
		||||
 | 
			
		||||
    return dataTypesOptions.find((dataType) => dataType.id === id);
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import type { DataType } from './data-types';
 | 
			
		||||
import type { DataTypeData } from './data-types';
 | 
			
		||||
 | 
			
		||||
export const genericDataTypes: readonly DataType[] = [
 | 
			
		||||
export const genericDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    { name: 'bigint', id: 'bigint' },
 | 
			
		||||
    { name: 'binary', id: 'binary' },
 | 
			
		||||
    { name: 'binary', id: 'binary', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'blob', id: 'blob' },
 | 
			
		||||
    { name: 'boolean', id: 'boolean' },
 | 
			
		||||
    { name: 'char', id: 'char' },
 | 
			
		||||
    { name: 'char', id: 'char', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'date', id: 'date' },
 | 
			
		||||
    { name: 'datetime', id: 'datetime' },
 | 
			
		||||
    { name: 'decimal', id: 'decimal' },
 | 
			
		||||
@@ -22,6 +22,6 @@ export const genericDataTypes: readonly DataType[] = [
 | 
			
		||||
    { name: 'time', id: 'time' },
 | 
			
		||||
    { name: 'timestamp', id: 'timestamp' },
 | 
			
		||||
    { name: 'uuid', id: 'uuid' },
 | 
			
		||||
    { name: 'varbinary', id: 'varbinary' },
 | 
			
		||||
    { name: 'varchar', id: 'varchar' },
 | 
			
		||||
    { name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'varchar', id: 'varchar', hasCharMaxLength: true },
 | 
			
		||||
] as const;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import type { DataType } from './data-types';
 | 
			
		||||
import type { DataTypeData } from './data-types';
 | 
			
		||||
 | 
			
		||||
export const mariadbDataTypes: readonly DataType[] = [
 | 
			
		||||
export const mariadbDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    // Numeric Types
 | 
			
		||||
    { name: 'tinyint', id: 'tinyint' },
 | 
			
		||||
    { name: 'smallint', id: 'smallint' },
 | 
			
		||||
@@ -23,10 +23,10 @@ export const mariadbDataTypes: readonly DataType[] = [
 | 
			
		||||
    { name: 'year', id: 'year' },
 | 
			
		||||
 | 
			
		||||
    // String Types
 | 
			
		||||
    { name: 'char', id: 'char' },
 | 
			
		||||
    { name: 'varchar', id: 'varchar' },
 | 
			
		||||
    { name: 'binary', id: 'binary' },
 | 
			
		||||
    { name: 'varbinary', id: 'varbinary' },
 | 
			
		||||
    { name: 'char', id: 'char', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'varchar', id: 'varchar', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'binary', id: 'binary', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'tinyblob', id: 'tinyblob' },
 | 
			
		||||
    { name: 'blob', id: 'blob' },
 | 
			
		||||
    { name: 'mediumblob', id: 'mediumblob' },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import type { DataType } from './data-types';
 | 
			
		||||
import type { DataTypeData } from './data-types';
 | 
			
		||||
 | 
			
		||||
export const mysqlDataTypes: readonly DataType[] = [
 | 
			
		||||
export const mysqlDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    // Numeric Types
 | 
			
		||||
    { name: 'tinyint', id: 'tinyint' },
 | 
			
		||||
    { name: 'smallint', id: 'smallint' },
 | 
			
		||||
@@ -23,10 +23,10 @@ export const mysqlDataTypes: readonly DataType[] = [
 | 
			
		||||
    { name: 'year', id: 'year' },
 | 
			
		||||
 | 
			
		||||
    // String Types
 | 
			
		||||
    { name: 'char', id: 'char' },
 | 
			
		||||
    { name: 'varchar', id: 'varchar' },
 | 
			
		||||
    { name: 'binary', id: 'binary' },
 | 
			
		||||
    { name: 'varbinary', id: 'varbinary' },
 | 
			
		||||
    { name: 'char', id: 'char', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'varchar', id: 'varchar', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'binary', id: 'binary', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'tinyblob', id: 'tinyblob' },
 | 
			
		||||
    { name: 'blob', id: 'blob' },
 | 
			
		||||
    { name: 'mediumblob', id: 'mediumblob' },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import type { DataType } from './data-types';
 | 
			
		||||
import type { DataTypeData } from './data-types';
 | 
			
		||||
 | 
			
		||||
export const postgresDataTypes: readonly DataType[] = [
 | 
			
		||||
export const postgresDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    // Numeric Types
 | 
			
		||||
    { name: 'smallint', id: 'smallint' },
 | 
			
		||||
    { name: 'integer', id: 'integer' },
 | 
			
		||||
@@ -15,9 +15,13 @@ export const postgresDataTypes: readonly DataType[] = [
 | 
			
		||||
    { name: 'money', id: 'money' },
 | 
			
		||||
 | 
			
		||||
    // Character Types
 | 
			
		||||
    { name: 'char', id: 'char' },
 | 
			
		||||
    { name: 'varchar', id: 'varchar' },
 | 
			
		||||
    { name: 'character varying', id: 'character_varying' },
 | 
			
		||||
    { name: 'char', id: 'char', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'varchar', id: 'varchar', hasCharMaxLength: true },
 | 
			
		||||
    {
 | 
			
		||||
        name: 'character varying',
 | 
			
		||||
        id: 'character_varying',
 | 
			
		||||
        hasCharMaxLength: true,
 | 
			
		||||
    },
 | 
			
		||||
    { name: 'text', id: 'text' },
 | 
			
		||||
 | 
			
		||||
    // Binary Data Types
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import type { DataType } from './data-types';
 | 
			
		||||
import type { DataTypeData } from './data-types';
 | 
			
		||||
 | 
			
		||||
export const sqlServerDataTypes: readonly DataType[] = [
 | 
			
		||||
export const sqlServerDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    // Exact Numerics
 | 
			
		||||
    { name: 'bigint', id: 'bigint' },
 | 
			
		||||
    { name: 'bit', id: 'bit' },
 | 
			
		||||
@@ -25,18 +25,18 @@ export const sqlServerDataTypes: readonly DataType[] = [
 | 
			
		||||
    { name: 'time', id: 'time' },
 | 
			
		||||
 | 
			
		||||
    // Character Strings
 | 
			
		||||
    { name: 'char', id: 'char' },
 | 
			
		||||
    { name: 'varchar', id: 'varchar' },
 | 
			
		||||
    { name: 'char', id: 'char', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'varchar', id: 'varchar', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'text', id: 'text' },
 | 
			
		||||
 | 
			
		||||
    // Unicode Character Strings
 | 
			
		||||
    { name: 'nchar', id: 'nchar' },
 | 
			
		||||
    { name: 'nvarchar', id: 'nvarchar' },
 | 
			
		||||
    { name: 'nchar', id: 'nchar', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'ntext', id: 'ntext' },
 | 
			
		||||
 | 
			
		||||
    // Binary Strings
 | 
			
		||||
    { name: 'binary', id: 'binary' },
 | 
			
		||||
    { name: 'varbinary', id: 'varbinary' },
 | 
			
		||||
    { name: 'binary', id: 'binary', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'image', id: 'image' },
 | 
			
		||||
 | 
			
		||||
    // Other Data Types
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import type { DataType } from './data-types';
 | 
			
		||||
import type { DataTypeData } from './data-types';
 | 
			
		||||
 | 
			
		||||
export const sqliteDataTypes: readonly DataType[] = [
 | 
			
		||||
export const sqliteDataTypes: readonly DataTypeData[] = [
 | 
			
		||||
    // Numeric Types
 | 
			
		||||
    { name: 'integer', id: 'integer' },
 | 
			
		||||
    { name: 'real', id: 'real' },
 | 
			
		||||
@@ -22,6 +22,6 @@ export const sqliteDataTypes: readonly DataType[] = [
 | 
			
		||||
    { name: 'int', id: 'int' },
 | 
			
		||||
    { name: 'float', id: 'float' },
 | 
			
		||||
    { name: 'boolean', id: 'boolean' },
 | 
			
		||||
    { name: 'varchar', id: 'varchar' },
 | 
			
		||||
    { name: 'varchar', id: 'varchar', hasCharMaxLength: true },
 | 
			
		||||
    { name: 'decimal', id: 'decimal' },
 | 
			
		||||
] as const;
 | 
			
		||||
 
 | 
			
		||||
@@ -115,8 +115,22 @@ export const exportBaseSQL = (diagram: Diagram): string => {
 | 
			
		||||
 | 
			
		||||
                // Remove the type cast part after :: if it exists
 | 
			
		||||
                if (fieldDefault.includes('::')) {
 | 
			
		||||
                    const endedWithParentheses = fieldDefault.endsWith(')');
 | 
			
		||||
                    fieldDefault = fieldDefault.split('::')[0];
 | 
			
		||||
 | 
			
		||||
                    if (
 | 
			
		||||
                        (fieldDefault.startsWith('(') &&
 | 
			
		||||
                            !fieldDefault.endsWith(')')) ||
 | 
			
		||||
                        endedWithParentheses
 | 
			
		||||
                    ) {
 | 
			
		||||
                        fieldDefault += ')';
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (fieldDefault === `('now')`) {
 | 
			
		||||
                    fieldDefault = `now()`;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                sqlScript += ` DEFAULT ${fieldDefault}`;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -95,7 +95,7 @@ export const createFieldsFromMetadata = ({
 | 
			
		||||
            nullable: col.nullable,
 | 
			
		||||
            ...(col.character_maximum_length &&
 | 
			
		||||
            col.character_maximum_length !== 'null'
 | 
			
		||||
                ? { character_maximum_length: col.character_maximum_length }
 | 
			
		||||
                ? { characterMaximumLength: col.character_maximum_length }
 | 
			
		||||
                : {}),
 | 
			
		||||
            ...(col.precision?.precision
 | 
			
		||||
                ? { precision: col.precision.precision }
 | 
			
		||||
 
 | 
			
		||||
@@ -103,10 +103,9 @@ const tableToTableNode = (
 | 
			
		||||
 | 
			
		||||
export interface CanvasProps {
 | 
			
		||||
    initialTables: DBTable[];
 | 
			
		||||
    readonly?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
 | 
			
		||||
export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
 | 
			
		||||
    const { getEdge, getInternalNode, getEdges, getNode } = useReactFlow();
 | 
			
		||||
    const [selectedTableIds, setSelectedTableIds] = useState<string[]>([]);
 | 
			
		||||
    const [selectedRelationshipIds, setSelectedRelationshipIds] = useState<
 | 
			
		||||
@@ -127,6 +126,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
 | 
			
		||||
        filteredSchemas,
 | 
			
		||||
        events,
 | 
			
		||||
        dependencies,
 | 
			
		||||
        readonly,
 | 
			
		||||
    } = useChartDB();
 | 
			
		||||
    const { showSidePanel } = useLayout();
 | 
			
		||||
    const { effectiveTheme } = useTheme();
 | 
			
		||||
@@ -682,7 +682,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <CanvasContextMenu>
 | 
			
		||||
            <div className="relative flex h-full">
 | 
			
		||||
            <div className="relative flex h-full" id="canvas">
 | 
			
		||||
                <ReactFlow
 | 
			
		||||
                    colorMode={effectiveTheme}
 | 
			
		||||
                    className="canvas-cursor-default nodes-animated"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										86
									
								
								src/pages/editor-page/canvas/hooks/use-is-lost-in-canvas.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/pages/editor-page/canvas/hooks/use-is-lost-in-canvas.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
			
		||||
import { useCallback, useState } from 'react';
 | 
			
		||||
import { getTableDimensions } from '../canvas-utils';
 | 
			
		||||
import type { TableNodeType } from '../table-node/table-node';
 | 
			
		||||
import { useOnViewportChange, useReactFlow } from '@xyflow/react';
 | 
			
		||||
import { useDebounce } from '@/hooks/use-debounce-v2';
 | 
			
		||||
 | 
			
		||||
export const useIsLostInCanvas = () => {
 | 
			
		||||
    const { getNodes, getViewport } = useReactFlow();
 | 
			
		||||
    const [noTablesVisible, setNoTablesVisible] = useState<boolean>(false);
 | 
			
		||||
 | 
			
		||||
    // Check if any tables are visible in the current viewport
 | 
			
		||||
    const checkVisibleTables = useCallback(() => {
 | 
			
		||||
        const nodes = getNodes();
 | 
			
		||||
        const viewport = getViewport();
 | 
			
		||||
 | 
			
		||||
        // If there are no nodes at all, don't highlight the button
 | 
			
		||||
        if (nodes.length === 0) {
 | 
			
		||||
            setNoTablesVisible(false);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Count visible (not hidden) nodes
 | 
			
		||||
        const visibleNodes = nodes.filter((node) => !node.hidden);
 | 
			
		||||
 | 
			
		||||
        // If there are no visible nodes at all, don't highlight the button
 | 
			
		||||
        if (visibleNodes.length === 0) {
 | 
			
		||||
            setNoTablesVisible(false);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Calculate viewport boundaries
 | 
			
		||||
        const viewportLeft = -viewport.x / viewport.zoom;
 | 
			
		||||
        const viewportTop = -viewport.y / viewport.zoom;
 | 
			
		||||
 | 
			
		||||
        const width =
 | 
			
		||||
            document.getElementById('canvas')?.clientWidth || window.innerWidth;
 | 
			
		||||
        const height =
 | 
			
		||||
            document.getElementById('canvas')?.clientHeight ||
 | 
			
		||||
            window.innerHeight;
 | 
			
		||||
 | 
			
		||||
        const viewportRight = viewportLeft + width / viewport.zoom;
 | 
			
		||||
        const viewportBottom = viewportTop + height / viewport.zoom;
 | 
			
		||||
 | 
			
		||||
        // Check if any node is visible in the viewport
 | 
			
		||||
        const anyNodeVisible = visibleNodes.some((node) => {
 | 
			
		||||
            let nodeWidth = node.width || 0;
 | 
			
		||||
            let nodeHeight = node.height || 0;
 | 
			
		||||
 | 
			
		||||
            if (node.type === 'table' && node.data?.table) {
 | 
			
		||||
                const tableNodeType = node as TableNodeType;
 | 
			
		||||
                const dimensions = getTableDimensions(tableNodeType.data.table);
 | 
			
		||||
                nodeWidth = dimensions.width;
 | 
			
		||||
                nodeHeight = dimensions.height;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Node boundaries
 | 
			
		||||
            const nodeLeft = node.position.x;
 | 
			
		||||
            const nodeTop = node.position.y;
 | 
			
		||||
            const nodeRight = nodeLeft + nodeWidth;
 | 
			
		||||
            const nodeBottom = nodeTop + nodeHeight;
 | 
			
		||||
 | 
			
		||||
            return (
 | 
			
		||||
                nodeRight >= viewportLeft &&
 | 
			
		||||
                nodeLeft <= viewportRight &&
 | 
			
		||||
                nodeBottom >= viewportTop &&
 | 
			
		||||
                nodeTop <= viewportBottom
 | 
			
		||||
            );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // Only set to true if there are tables but none are visible
 | 
			
		||||
        setNoTablesVisible(!anyNodeVisible);
 | 
			
		||||
    }, [getNodes, getViewport]);
 | 
			
		||||
 | 
			
		||||
    // Create a debounced version of checkVisibleTables
 | 
			
		||||
    const debouncedCheckVisibleTables = useDebounce(checkVisibleTables, 1000);
 | 
			
		||||
 | 
			
		||||
    useOnViewportChange({
 | 
			
		||||
        onEnd: () => {
 | 
			
		||||
            debouncedCheckVisibleTables();
 | 
			
		||||
        },
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        isLostInCanvas: noTablesVisible,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
@@ -7,6 +7,7 @@ import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useLayout } from '@/hooks/use-layout';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import { getCardinalityMarkerId } from './canvas-utils';
 | 
			
		||||
import { useDiff } from '@/context/diff-context/use-diff';
 | 
			
		||||
 | 
			
		||||
export type RelationshipEdgeType = Edge<
 | 
			
		||||
    {
 | 
			
		||||
@@ -29,6 +30,7 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
 | 
			
		||||
}) => {
 | 
			
		||||
    const { getInternalNode, getEdge } = useReactFlow();
 | 
			
		||||
    const { openRelationshipFromSidebar, selectSidebarSection } = useLayout();
 | 
			
		||||
    const { checkIfRelationshipRemoved, checkIfNewRelationship } = useDiff();
 | 
			
		||||
 | 
			
		||||
    const { relationships } = useChartDB();
 | 
			
		||||
 | 
			
		||||
@@ -149,6 +151,25 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
 | 
			
		||||
            }),
 | 
			
		||||
        [relationship?.targetCardinality, selected, targetSide]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const isDiffNewRelationship = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
            relationship?.id
 | 
			
		||||
                ? checkIfNewRelationship({ relationshipId: relationship.id })
 | 
			
		||||
                : false,
 | 
			
		||||
        [checkIfNewRelationship, relationship?.id]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const isDiffRelationshipRemoved = useMemo(
 | 
			
		||||
        () =>
 | 
			
		||||
            relationship?.id
 | 
			
		||||
                ? checkIfRelationshipRemoved({
 | 
			
		||||
                      relationshipId: relationship.id,
 | 
			
		||||
                  })
 | 
			
		||||
                : false,
 | 
			
		||||
        [checkIfRelationshipRemoved, relationship?.id]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <path
 | 
			
		||||
@@ -160,6 +181,10 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
 | 
			
		||||
                className={cn([
 | 
			
		||||
                    'react-flow__edge-path',
 | 
			
		||||
                    `!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
 | 
			
		||||
                    {
 | 
			
		||||
                        '!stroke-green-500': isDiffNewRelationship,
 | 
			
		||||
                        '!stroke-red-500': isDiffRelationshipRemoved,
 | 
			
		||||
                    },
 | 
			
		||||
                ])}
 | 
			
		||||
                onClick={(e) => {
 | 
			
		||||
                    if (e.detail === 2) {
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,15 @@ import {
 | 
			
		||||
    useUpdateNodeInternals,
 | 
			
		||||
} from '@xyflow/react';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { Check, KeyRound, MessageCircleMore, Trash2 } from 'lucide-react';
 | 
			
		||||
import {
 | 
			
		||||
    Check,
 | 
			
		||||
    KeyRound,
 | 
			
		||||
    MessageCircleMore,
 | 
			
		||||
    SquareDot,
 | 
			
		||||
    SquareMinus,
 | 
			
		||||
    SquarePlus,
 | 
			
		||||
    Trash2,
 | 
			
		||||
} from 'lucide-react';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
@@ -23,6 +31,7 @@ import {
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import { useClickAway, useKeyPressEvent } from 'react-use';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { useDiff } from '@/context/diff-context/use-diff';
 | 
			
		||||
 | 
			
		||||
export const LEFT_HANDLE_ID_PREFIX = 'left_rel_';
 | 
			
		||||
export const RIGHT_HANDLE_ID_PREFIX = 'right_rel_';
 | 
			
		||||
@@ -95,6 +104,43 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
        useKeyPressEvent('Enter', editFieldName);
 | 
			
		||||
        useKeyPressEvent('Escape', abortEdit);
 | 
			
		||||
 | 
			
		||||
        const {
 | 
			
		||||
            checkIfFieldRemoved,
 | 
			
		||||
            checkIfNewField,
 | 
			
		||||
            getFieldNewName,
 | 
			
		||||
            getFieldNewType,
 | 
			
		||||
            checkIfFieldHasChange,
 | 
			
		||||
        } = useDiff();
 | 
			
		||||
 | 
			
		||||
        const isDiffFieldRemoved = useMemo(
 | 
			
		||||
            () => checkIfFieldRemoved({ fieldId: field.id }),
 | 
			
		||||
            [checkIfFieldRemoved, field.id]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const isDiffNewField = useMemo(
 | 
			
		||||
            () => checkIfNewField({ fieldId: field.id }),
 | 
			
		||||
            [checkIfNewField, field.id]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const fieldDiffChangedName = useMemo(
 | 
			
		||||
            () => getFieldNewName({ fieldId: field.id }),
 | 
			
		||||
            [getFieldNewName, field.id]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const fieldDiffChangedType = useMemo(
 | 
			
		||||
            () => getFieldNewType({ fieldId: field.id }),
 | 
			
		||||
            [getFieldNewType, field.id]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const isDiffFieldChanged = useMemo(
 | 
			
		||||
            () =>
 | 
			
		||||
                checkIfFieldHasChange({
 | 
			
		||||
                    fieldId: field.id,
 | 
			
		||||
                    tableId: tableNodeId,
 | 
			
		||||
                }),
 | 
			
		||||
            [checkIfFieldHasChange, field.id, tableNodeId]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const enterEditMode = (e: React.MouseEvent) => {
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            setEditMode(true);
 | 
			
		||||
@@ -102,13 +148,23 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div
 | 
			
		||||
                className={`group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800 ${
 | 
			
		||||
                    highlighted ? 'bg-pink-100 dark:bg-pink-900' : ''
 | 
			
		||||
                } transition-all duration-200 ease-in-out ${
 | 
			
		||||
                    visible
 | 
			
		||||
                        ? 'max-h-8 opacity-100'
 | 
			
		||||
                        : 'z-0 max-h-0 overflow-hidden opacity-0'
 | 
			
		||||
                }`}
 | 
			
		||||
                className={cn(
 | 
			
		||||
                    'group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800',
 | 
			
		||||
                    'transition-all duration-200 ease-in-out',
 | 
			
		||||
                    {
 | 
			
		||||
                        'bg-pink-100 dark:bg-pink-900': highlighted,
 | 
			
		||||
                        'max-h-8 opacity-100': visible,
 | 
			
		||||
                        'z-0 max-h-0 overflow-hidden opacity-0': !visible,
 | 
			
		||||
                        'bg-sky-200 dark:bg-sky-800 hover:bg-sky-100 dark:hover:bg-sky-900 border-sky-300 dark:border-sky-700':
 | 
			
		||||
                            isDiffFieldChanged &&
 | 
			
		||||
                            !isDiffFieldRemoved &&
 | 
			
		||||
                            !isDiffNewField,
 | 
			
		||||
                        'bg-red-200 dark:bg-red-800 hover:bg-red-100 dark:hover:bg-red-900 border-red-300 dark:border-red-700':
 | 
			
		||||
                            isDiffFieldRemoved,
 | 
			
		||||
                        'bg-green-200 dark:bg-green-800 hover:bg-green-100 dark:hover:bg-green-900 border-green-300 dark:border-green-700':
 | 
			
		||||
                            isDiffNewField,
 | 
			
		||||
                    }
 | 
			
		||||
                )}
 | 
			
		||||
            >
 | 
			
		||||
                {isConnectable ? (
 | 
			
		||||
                    <>
 | 
			
		||||
@@ -161,7 +217,14 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                        }
 | 
			
		||||
                    )}
 | 
			
		||||
                >
 | 
			
		||||
                    {editMode ? (
 | 
			
		||||
                    {isDiffFieldRemoved ? (
 | 
			
		||||
                        <SquareMinus className="size-3.5 text-red-800 dark:text-red-200" />
 | 
			
		||||
                    ) : isDiffNewField ? (
 | 
			
		||||
                        <SquarePlus className="size-3.5 text-green-800 dark:text-green-200" />
 | 
			
		||||
                    ) : isDiffFieldChanged ? (
 | 
			
		||||
                        <SquareDot className="size-3.5 shrink-0 text-sky-800 dark:text-sky-200" />
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                    {editMode && !readonly ? (
 | 
			
		||||
                        <>
 | 
			
		||||
                            <Input
 | 
			
		||||
                                ref={inputRef}
 | 
			
		||||
@@ -190,10 +253,27 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                        //     {field.name}
 | 
			
		||||
                        // </span>
 | 
			
		||||
                        <span
 | 
			
		||||
                            className="truncate"
 | 
			
		||||
                            className={cn('truncate', {
 | 
			
		||||
                                'text-red-800 font-normal dark:text-red-200':
 | 
			
		||||
                                    isDiffFieldRemoved,
 | 
			
		||||
                                'text-green-800 font-normal dark:text-green-200':
 | 
			
		||||
                                    isDiffNewField,
 | 
			
		||||
                                'text-sky-800 font-normal dark:text-sky-200':
 | 
			
		||||
                                    isDiffFieldChanged &&
 | 
			
		||||
                                    !isDiffFieldRemoved &&
 | 
			
		||||
                                    !isDiffNewField,
 | 
			
		||||
                            })}
 | 
			
		||||
                            onDoubleClick={enterEditMode}
 | 
			
		||||
                        >
 | 
			
		||||
                            {field.name}
 | 
			
		||||
                            {fieldDiffChangedName ? (
 | 
			
		||||
                                <>
 | 
			
		||||
                                    {field.name}{' '}
 | 
			
		||||
                                    <span className="font-medium">→</span>{' '}
 | 
			
		||||
                                    {fieldDiffChangedName}
 | 
			
		||||
                                </>
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                field.name
 | 
			
		||||
                            )}
 | 
			
		||||
                        </span>
 | 
			
		||||
                    )}
 | 
			
		||||
                    {/* <span className="truncate">{field.name}</span> */}
 | 
			
		||||
@@ -214,7 +294,18 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
                            <div
 | 
			
		||||
                                className={cn(
 | 
			
		||||
                                    'text-muted-foreground',
 | 
			
		||||
                                    !readonly ? 'group-hover:hidden' : ''
 | 
			
		||||
                                    !readonly ? 'group-hover:hidden' : '',
 | 
			
		||||
                                    isDiffFieldRemoved
 | 
			
		||||
                                        ? 'text-red-800 dark:text-red-200'
 | 
			
		||||
                                        : '',
 | 
			
		||||
                                    isDiffNewField
 | 
			
		||||
                                        ? 'text-green-800 dark:text-green-200'
 | 
			
		||||
                                        : '',
 | 
			
		||||
                                    isDiffFieldChanged &&
 | 
			
		||||
                                        !isDiffFieldRemoved &&
 | 
			
		||||
                                        !isDiffNewField
 | 
			
		||||
                                        ? 'text-sky-800 dark:text-sky-200'
 | 
			
		||||
                                        : ''
 | 
			
		||||
                                )}
 | 
			
		||||
                            >
 | 
			
		||||
                                <KeyRound size={14} />
 | 
			
		||||
@@ -223,11 +314,31 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
 | 
			
		||||
 | 
			
		||||
                        <div
 | 
			
		||||
                            className={cn(
 | 
			
		||||
                                'content-center truncate text-right text-xs text-muted-foreground shrink-0',
 | 
			
		||||
                                !readonly ? 'group-hover:hidden' : ''
 | 
			
		||||
                                'content-center truncate text-right text-xs text-muted-foreground',
 | 
			
		||||
                                !readonly ? 'group-hover:hidden' : '',
 | 
			
		||||
                                isDiffFieldRemoved
 | 
			
		||||
                                    ? 'text-red-800 dark:text-red-200'
 | 
			
		||||
                                    : '',
 | 
			
		||||
                                isDiffNewField
 | 
			
		||||
                                    ? 'text-green-800 dark:text-green-200'
 | 
			
		||||
                                    : '',
 | 
			
		||||
                                isDiffFieldChanged &&
 | 
			
		||||
                                    !isDiffFieldRemoved &&
 | 
			
		||||
                                    !isDiffNewField
 | 
			
		||||
                                    ? 'text-sky-800 dark:text-sky-200'
 | 
			
		||||
                                    : ''
 | 
			
		||||
                            )}
 | 
			
		||||
                        >
 | 
			
		||||
                            {field.type.name}
 | 
			
		||||
                            {fieldDiffChangedType ? (
 | 
			
		||||
                                <>
 | 
			
		||||
                                    <span className="line-through">
 | 
			
		||||
                                        {field.type.name.split(' ')[0]}
 | 
			
		||||
                                    </span>{' '}
 | 
			
		||||
                                    {fieldDiffChangedType.name.split(' ')[0]}
 | 
			
		||||
                                </>
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                field.type.name.split(' ')[0]
 | 
			
		||||
                            )}
 | 
			
		||||
                            {field.nullable ? '?' : ''}
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {readonly ? null : (
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,35 @@
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
 | 
			
		||||
export interface TableNodeStatusProps {
 | 
			
		||||
    status: 'new' | 'changed' | 'removed' | 'none';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TableNodeStatus: React.FC<TableNodeStatusProps> = ({ status }) => {
 | 
			
		||||
    if (status === 'none') {
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="absolute left-1/2 top-0 z-10 -translate-x-1/2 -translate-y-1/2">
 | 
			
		||||
            <span
 | 
			
		||||
                className={cn(
 | 
			
		||||
                    'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium text-white',
 | 
			
		||||
                    {
 | 
			
		||||
                        'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100':
 | 
			
		||||
                            status === 'new',
 | 
			
		||||
                        'bg-sky-100 text-sky-800 dark:bg-sky-800 dark:text-sky-100':
 | 
			
		||||
                            status === 'changed',
 | 
			
		||||
                        'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100':
 | 
			
		||||
                            status === 'removed',
 | 
			
		||||
                    }
 | 
			
		||||
                )}
 | 
			
		||||
            >
 | 
			
		||||
                {status === 'new'
 | 
			
		||||
                    ? 'New'
 | 
			
		||||
                    : status === 'changed'
 | 
			
		||||
                      ? 'Modified'
 | 
			
		||||
                      : 'Deleted'}
 | 
			
		||||
            </span>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -10,6 +10,9 @@ import {
 | 
			
		||||
    ChevronUp,
 | 
			
		||||
    Check,
 | 
			
		||||
    CircleDotDashed,
 | 
			
		||||
    SquareDot,
 | 
			
		||||
    SquarePlus,
 | 
			
		||||
    SquareMinus,
 | 
			
		||||
} from 'lucide-react';
 | 
			
		||||
import { Label } from '@/components/label/label';
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
@@ -30,6 +33,8 @@ import {
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
    TooltipTrigger,
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import { useDiff } from '@/context/diff-context/use-diff';
 | 
			
		||||
import { TableNodeStatus } from './table-node-status/table-node-status';
 | 
			
		||||
 | 
			
		||||
export type TableNodeType = Node<
 | 
			
		||||
    {
 | 
			
		||||
@@ -61,6 +66,35 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
        const [tableName, setTableName] = useState(table.name);
 | 
			
		||||
        const inputRef = React.useRef<HTMLInputElement>(null);
 | 
			
		||||
 | 
			
		||||
        const {
 | 
			
		||||
            getTableNewName,
 | 
			
		||||
            checkIfTableHasChange,
 | 
			
		||||
            checkIfNewTable,
 | 
			
		||||
            checkIfTableRemoved,
 | 
			
		||||
        } = useDiff();
 | 
			
		||||
 | 
			
		||||
        const fields = useMemo(() => table.fields, [table.fields]);
 | 
			
		||||
 | 
			
		||||
        const tableChangedName = useMemo(
 | 
			
		||||
            () => getTableNewName({ tableId: table.id }),
 | 
			
		||||
            [getTableNewName, table.id]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const isDiffTableChanged = useMemo(
 | 
			
		||||
            () => checkIfTableHasChange({ tableId: table.id }),
 | 
			
		||||
            [checkIfTableHasChange, table.id]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const isDiffNewTable = useMemo(
 | 
			
		||||
            () => checkIfNewTable({ tableId: table.id }),
 | 
			
		||||
            [checkIfNewTable, table.id]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const isDiffTableRemoved = useMemo(
 | 
			
		||||
            () => checkIfTableRemoved({ tableId: table.id }),
 | 
			
		||||
            [checkIfTableRemoved, table.id]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        const selectedRelEdges = edges.filter(
 | 
			
		||||
            (edge) =>
 | 
			
		||||
                (edge.source === id || edge.target === id) &&
 | 
			
		||||
@@ -109,13 +143,13 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
 | 
			
		||||
        const visibleFields = useMemo(() => {
 | 
			
		||||
            if (expanded) {
 | 
			
		||||
                return table.fields;
 | 
			
		||||
                return fields;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const mustDisplayedFields = table.fields.filter((field: DBField) =>
 | 
			
		||||
            const mustDisplayedFields = fields.filter((field: DBField) =>
 | 
			
		||||
                isMustDisplayedField(field)
 | 
			
		||||
            );
 | 
			
		||||
            const nonMustDisplayedFields = table.fields.filter(
 | 
			
		||||
            const nonMustDisplayedFields = fields.filter(
 | 
			
		||||
                (field: DBField) => !isMustDisplayedField(field)
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
@@ -133,8 +167,8 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
            return [
 | 
			
		||||
                ...visibleMustDisplayedFields,
 | 
			
		||||
                ...visibleNonMustDisplayedFields,
 | 
			
		||||
            ].sort((a, b) => table.fields.indexOf(a) - table.fields.indexOf(b));
 | 
			
		||||
        }, [expanded, table.fields, isMustDisplayedField]);
 | 
			
		||||
            ].sort((a, b) => fields.indexOf(a) - fields.indexOf(b));
 | 
			
		||||
        }, [expanded, fields, isMustDisplayedField]);
 | 
			
		||||
 | 
			
		||||
        const editTableName = useCallback(() => {
 | 
			
		||||
            if (!editMode) return;
 | 
			
		||||
@@ -174,6 +208,17 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                            : '',
 | 
			
		||||
                        highlightOverlappingTables && isOverlapping
 | 
			
		||||
                            ? 'animate-scale-2'
 | 
			
		||||
                            : '',
 | 
			
		||||
                        isDiffTableChanged &&
 | 
			
		||||
                            !isDiffNewTable &&
 | 
			
		||||
                            !isDiffTableRemoved
 | 
			
		||||
                            ? 'outline outline-[3px] outline-sky-500 dark:outline-sky-900 outline-offset-[5px]'
 | 
			
		||||
                            : '',
 | 
			
		||||
                        isDiffNewTable
 | 
			
		||||
                            ? 'outline outline-[3px] outline-green-500 dark:outline-green-900 outline-offset-[5px]'
 | 
			
		||||
                            : '',
 | 
			
		||||
                        isDiffTableRemoved
 | 
			
		||||
                            ? 'outline outline-[3px] outline-red-500 dark:outline-red-900 outline-offset-[5px]'
 | 
			
		||||
                            : ''
 | 
			
		||||
                    )}
 | 
			
		||||
                    onClick={(e) => {
 | 
			
		||||
@@ -194,14 +239,87 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                        table={table}
 | 
			
		||||
                        focused={focused}
 | 
			
		||||
                    />
 | 
			
		||||
                    {/* Badge added here */}
 | 
			
		||||
                    <TableNodeStatus
 | 
			
		||||
                        status={
 | 
			
		||||
                            isDiffNewTable
 | 
			
		||||
                                ? 'new'
 | 
			
		||||
                                : isDiffTableRemoved
 | 
			
		||||
                                  ? 'removed'
 | 
			
		||||
                                  : isDiffTableChanged
 | 
			
		||||
                                    ? 'changed'
 | 
			
		||||
                                    : 'none'
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                    <div
 | 
			
		||||
                        className="h-2 rounded-t-[6px]"
 | 
			
		||||
                        style={{ backgroundColor: table.color }}
 | 
			
		||||
                    ></div>
 | 
			
		||||
                    <div className="group flex h-9 items-center justify-between bg-slate-200 px-2 dark:bg-slate-900">
 | 
			
		||||
                        <div className="flex min-w-0 flex-1 items-center gap-2">
 | 
			
		||||
                            {isDiffNewTable ? (
 | 
			
		||||
                                <Tooltip>
 | 
			
		||||
                                    <TooltipTrigger asChild>
 | 
			
		||||
                                        <SquarePlus
 | 
			
		||||
                                            className="size-3.5 shrink-0 text-green-600"
 | 
			
		||||
                                            strokeWidth={2.5}
 | 
			
		||||
                                        />
 | 
			
		||||
                                    </TooltipTrigger>
 | 
			
		||||
                                    <TooltipContent>New Table</TooltipContent>
 | 
			
		||||
                                </Tooltip>
 | 
			
		||||
                            ) : isDiffTableRemoved ? (
 | 
			
		||||
                                <Tooltip>
 | 
			
		||||
                                    <TooltipTrigger asChild>
 | 
			
		||||
                                        <SquareMinus
 | 
			
		||||
                                            className="size-3.5 shrink-0 text-red-600"
 | 
			
		||||
                                            strokeWidth={2.5}
 | 
			
		||||
                                        />
 | 
			
		||||
                                    </TooltipTrigger>
 | 
			
		||||
                                    <TooltipContent>
 | 
			
		||||
                                        Table Removed
 | 
			
		||||
                                    </TooltipContent>
 | 
			
		||||
                                </Tooltip>
 | 
			
		||||
                            ) : isDiffTableChanged ? (
 | 
			
		||||
                                <Tooltip>
 | 
			
		||||
                                    <TooltipTrigger asChild>
 | 
			
		||||
                                        <SquareDot
 | 
			
		||||
                                            className="size-3.5 shrink-0 text-sky-600"
 | 
			
		||||
                                            strokeWidth={2.5}
 | 
			
		||||
                                        />
 | 
			
		||||
                                    </TooltipTrigger>
 | 
			
		||||
                                    <TooltipContent>
 | 
			
		||||
                                        Table Changed
 | 
			
		||||
                                    </TooltipContent>
 | 
			
		||||
                                </Tooltip>
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                <Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
 | 
			
		||||
                            {editMode ? (
 | 
			
		||||
                            )}
 | 
			
		||||
 | 
			
		||||
                            {tableChangedName ? (
 | 
			
		||||
                                <Label className="flex h-5 items-center justify-center truncate rounded-sm bg-sky-200 px-2 py-0.5 text-sm font-normal text-sky-900 dark:bg-sky-800 dark:text-sky-200">
 | 
			
		||||
                                    <span className="truncate">
 | 
			
		||||
                                        {table.name}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                    <span className="mx-1 font-semibold">
 | 
			
		||||
                                        →
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                    <span className="truncate">
 | 
			
		||||
                                        {tableChangedName}
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </Label>
 | 
			
		||||
                            ) : isDiffNewTable ? (
 | 
			
		||||
                                <Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-green-200 px-2 py-0.5 text-sm font-normal text-green-900 dark:bg-green-800 dark:text-green-200">
 | 
			
		||||
                                    {table.name}
 | 
			
		||||
                                </Label>
 | 
			
		||||
                            ) : isDiffTableRemoved ? (
 | 
			
		||||
                                <Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-red-200 px-2 py-0.5 text-sm font-normal text-red-900 dark:bg-red-800 dark:text-red-200">
 | 
			
		||||
                                    {table.name}
 | 
			
		||||
                                </Label>
 | 
			
		||||
                            ) : isDiffTableChanged ? (
 | 
			
		||||
                                <Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-sky-200 px-2 py-0.5 text-sm font-normal text-sky-900 dark:bg-sky-800 dark:text-sky-200">
 | 
			
		||||
                                    {table.name}
 | 
			
		||||
                                </Label>
 | 
			
		||||
                            ) : editMode && !readonly ? (
 | 
			
		||||
                                <>
 | 
			
		||||
                                    <Input
 | 
			
		||||
                                        ref={inputRef}
 | 
			
		||||
@@ -273,11 +391,11 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                        className="transition-[max-height] duration-200 ease-in-out"
 | 
			
		||||
                        style={{
 | 
			
		||||
                            maxHeight: expanded
 | 
			
		||||
                                ? `${table.fields.length * 2}rem` // h-8 per field
 | 
			
		||||
                                ? `${fields.length * 2}rem` // h-8 per field
 | 
			
		||||
                                : `${TABLE_MINIMIZED_FIELDS * 2}rem`, // h-8 per field
 | 
			
		||||
                        }}
 | 
			
		||||
                    >
 | 
			
		||||
                        {table.fields.map((field: DBField) => (
 | 
			
		||||
                        {fields.map((field: DBField) => (
 | 
			
		||||
                            <TableNodeField
 | 
			
		||||
                                key={field.id}
 | 
			
		||||
                                focused={focused}
 | 
			
		||||
@@ -295,7 +413,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
 | 
			
		||||
                            />
 | 
			
		||||
                        ))}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {table.fields.length > TABLE_MINIMIZED_FIELDS && (
 | 
			
		||||
                    {fields.length > TABLE_MINIMIZED_FIELDS && (
 | 
			
		||||
                        <div
 | 
			
		||||
                            className="z-10 flex h-8 cursor-pointer items-center justify-center rounded-b-md border-t text-xs text-muted-foreground transition-colors duration-200 hover:bg-slate-100 dark:hover:bg-slate-800"
 | 
			
		||||
                            onClick={toggleExpand}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,22 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import type { ButtonProps } from '@/components/button/button';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
export const ToolbarButton = React.forwardRef<
 | 
			
		||||
    React.ElementRef<typeof Button>,
 | 
			
		||||
    ButtonProps
 | 
			
		||||
>((props, ref) => {
 | 
			
		||||
    const { className, ...rest } = props;
 | 
			
		||||
    return (
 | 
			
		||||
        <Button
 | 
			
		||||
            ref={ref}
 | 
			
		||||
            variant="ghost"
 | 
			
		||||
            className={'w-[36px] p-2 hover:bg-primary-foreground'}
 | 
			
		||||
            {...props}
 | 
			
		||||
            className={cn(
 | 
			
		||||
                'w-[36px] p-2 hover:bg-primary-foreground',
 | 
			
		||||
                className
 | 
			
		||||
            )}
 | 
			
		||||
            {...rest}
 | 
			
		||||
        />
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { keyboardShortcutsForOS } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
 | 
			
		||||
import { KeyboardShortcutAction } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
 | 
			
		||||
import { useIsLostInCanvas } from '../hooks/use-is-lost-in-canvas';
 | 
			
		||||
 | 
			
		||||
const convertToPercentage = (value: number) => `${Math.round(value * 100)}%`;
 | 
			
		||||
 | 
			
		||||
@@ -28,6 +29,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
 | 
			
		||||
    const { redo, undo, hasRedo, hasUndo } = useHistory();
 | 
			
		||||
    const { getZoom, zoomIn, zoomOut, fitView } = useReactFlow();
 | 
			
		||||
    const [zoom, setZoom] = useState<string>(convertToPercentage(getZoom()));
 | 
			
		||||
    const { isLostInCanvas } = useIsLostInCanvas();
 | 
			
		||||
 | 
			
		||||
    useOnViewportChange({
 | 
			
		||||
        onChange: ({ zoom }) => {
 | 
			
		||||
            setZoom(convertToPercentage(zoom));
 | 
			
		||||
@@ -93,7 +96,14 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
 | 
			
		||||
                    <Tooltip>
 | 
			
		||||
                        <TooltipTrigger asChild>
 | 
			
		||||
                            <span>
 | 
			
		||||
                                <ToolbarButton onClick={showAll}>
 | 
			
		||||
                                <ToolbarButton
 | 
			
		||||
                                    onClick={showAll}
 | 
			
		||||
                                    className={
 | 
			
		||||
                                        isLostInCanvas
 | 
			
		||||
                                            ? 'bg-pink-500 text-white hover:bg-pink-600 hover:text-white'
 | 
			
		||||
                                            : ''
 | 
			
		||||
                                    }
 | 
			
		||||
                                >
 | 
			
		||||
                                    <Scan />
 | 
			
		||||
                                </ToolbarButton>
 | 
			
		||||
                            </span>
 | 
			
		||||
 
 | 
			
		||||
@@ -29,14 +29,11 @@ import { AlertProvider } from '@/context/alert-context/alert-provider';
 | 
			
		||||
import { CanvasProvider } from '@/context/canvas-context/canvas-provider';
 | 
			
		||||
import { HIDE_BUCKLE_DOT_DEV } from '@/lib/env';
 | 
			
		||||
import { useDiagramLoader } from './use-diagram-loader';
 | 
			
		||||
import { DiffProvider } from '@/context/diff-context/diff-provider';
 | 
			
		||||
 | 
			
		||||
const OPEN_STAR_US_AFTER_SECONDS = 30;
 | 
			
		||||
const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1;
 | 
			
		||||
 | 
			
		||||
const OPEN_BUCKLE_AFTER_SECONDS = 60;
 | 
			
		||||
const SHOW_BUCKLE_AGAIN_AFTER_DAYS = 1;
 | 
			
		||||
const SHOW_BUCKLE_AGAIN_OPENED_AFTER_DAYS = 7;
 | 
			
		||||
 | 
			
		||||
export const EditorDesktopLayoutLazy = React.lazy(
 | 
			
		||||
    () => import('./editor-desktop-layout')
 | 
			
		||||
);
 | 
			
		||||
@@ -49,7 +46,7 @@ const EditorPageComponent: React.FC = () => {
 | 
			
		||||
    const { diagramName, currentDiagram, schemas, filteredSchemas } =
 | 
			
		||||
        useChartDB();
 | 
			
		||||
    const { openSelectSchema, showSidePanel } = useLayout();
 | 
			
		||||
    const { openStarUsDialog, openBuckleDialog } = useDialog();
 | 
			
		||||
    const { openStarUsDialog } = useDialog();
 | 
			
		||||
    const { diagramId } = useParams<{ diagramId: string }>();
 | 
			
		||||
    const { isMd: isDesktop } = useBreakpoint('md');
 | 
			
		||||
    const {
 | 
			
		||||
@@ -58,9 +55,6 @@ const EditorPageComponent: React.FC = () => {
 | 
			
		||||
        starUsDialogLastOpen,
 | 
			
		||||
        setStarUsDialogLastOpen,
 | 
			
		||||
        githubRepoOpened,
 | 
			
		||||
        setBuckleDialogLastOpen,
 | 
			
		||||
        buckleDialogLastOpen,
 | 
			
		||||
        buckleWaitlistOpened,
 | 
			
		||||
    } = useLocalConfig();
 | 
			
		||||
    const { toast } = useToast();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
@@ -91,37 +85,6 @@ const EditorPageComponent: React.FC = () => {
 | 
			
		||||
        starUsDialogLastOpen,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (HIDE_BUCKLE_DOT_DEV) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!currentDiagram?.id) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            new Date().getTime() - buckleDialogLastOpen >
 | 
			
		||||
            1000 *
 | 
			
		||||
                60 *
 | 
			
		||||
                60 *
 | 
			
		||||
                24 *
 | 
			
		||||
                (buckleWaitlistOpened
 | 
			
		||||
                    ? SHOW_BUCKLE_AGAIN_OPENED_AFTER_DAYS
 | 
			
		||||
                    : SHOW_BUCKLE_AGAIN_AFTER_DAYS)
 | 
			
		||||
        ) {
 | 
			
		||||
            const lastOpen = new Date().getTime();
 | 
			
		||||
            setBuckleDialogLastOpen(lastOpen);
 | 
			
		||||
            setTimeout(openBuckleDialog, OPEN_BUCKLE_AFTER_SECONDS * 1000);
 | 
			
		||||
        }
 | 
			
		||||
    }, [
 | 
			
		||||
        currentDiagram?.id,
 | 
			
		||||
        buckleWaitlistOpened,
 | 
			
		||||
        openBuckleDialog,
 | 
			
		||||
        setBuckleDialogLastOpen,
 | 
			
		||||
        buckleDialogLastOpen,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    const lastDiagramId = useRef<string>('');
 | 
			
		||||
 | 
			
		||||
    const handleChangeSchema = useCallback(async () => {
 | 
			
		||||
@@ -235,6 +198,7 @@ export const EditorPage: React.FC = () => (
 | 
			
		||||
                    <StorageProvider>
 | 
			
		||||
                        <ConfigProvider>
 | 
			
		||||
                            <RedoUndoStackProvider>
 | 
			
		||||
                                <DiffProvider>
 | 
			
		||||
                                    <ChartDBProvider>
 | 
			
		||||
                                        <HistoryProvider>
 | 
			
		||||
                                            <ReactFlowProvider>
 | 
			
		||||
@@ -252,6 +216,7 @@ export const EditorPage: React.FC = () => (
 | 
			
		||||
                                            </ReactFlowProvider>
 | 
			
		||||
                                        </HistoryProvider>
 | 
			
		||||
                                    </ChartDBProvider>
 | 
			
		||||
                                </DiffProvider>
 | 
			
		||||
                            </RedoUndoStackProvider>
 | 
			
		||||
                        </ConfigProvider>
 | 
			
		||||
                    </StorageProvider>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,162 @@
 | 
			
		||||
import React, { useEffect, useRef } from 'react';
 | 
			
		||||
import { Ellipsis, Trash2 } from 'lucide-react';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { Separator } from '@/components/separator/separator';
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import {
 | 
			
		||||
    Popover,
 | 
			
		||||
    PopoverContent,
 | 
			
		||||
    PopoverTrigger,
 | 
			
		||||
} from '@/components/popover/popover';
 | 
			
		||||
import { Label } from '@/components/label/label';
 | 
			
		||||
import { Checkbox } from '@/components/checkbox/checkbox';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Textarea } from '@/components/textarea/textarea';
 | 
			
		||||
import { debounce } from '@/lib/utils';
 | 
			
		||||
 | 
			
		||||
export interface TableFieldPopoverProps {
 | 
			
		||||
    field: DBField;
 | 
			
		||||
    updateField: (attrs: Partial<DBField>) => void;
 | 
			
		||||
    removeField: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
 | 
			
		||||
    field,
 | 
			
		||||
    updateField,
 | 
			
		||||
    removeField,
 | 
			
		||||
}) => {
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
    const [localField, setLocalField] = React.useState<DBField>(field);
 | 
			
		||||
 | 
			
		||||
    const debouncedUpdateFieldRef = useRef<((value?: DBField) => void) | null>(
 | 
			
		||||
        null
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        debouncedUpdateFieldRef.current = debounce((value?: DBField) => {
 | 
			
		||||
            updateField({
 | 
			
		||||
                comments: value?.comments,
 | 
			
		||||
                characterMaximumLength: value?.characterMaximumLength,
 | 
			
		||||
                unique: value?.unique,
 | 
			
		||||
            });
 | 
			
		||||
        }, 200);
 | 
			
		||||
 | 
			
		||||
        return () => {
 | 
			
		||||
            debouncedUpdateFieldRef.current = null;
 | 
			
		||||
        };
 | 
			
		||||
    }, [updateField]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (debouncedUpdateFieldRef.current) {
 | 
			
		||||
            debouncedUpdateFieldRef.current(localField);
 | 
			
		||||
        }
 | 
			
		||||
    }, [localField]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <Popover
 | 
			
		||||
            onOpenChange={(isOpen) => {
 | 
			
		||||
                if (isOpen) {
 | 
			
		||||
                    setLocalField(field);
 | 
			
		||||
                }
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
            <PopoverTrigger asChild>
 | 
			
		||||
                <Button
 | 
			
		||||
                    variant="ghost"
 | 
			
		||||
                    className="h-8 w-[32px] p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
 | 
			
		||||
                >
 | 
			
		||||
                    <Ellipsis className="size-3.5" />
 | 
			
		||||
                </Button>
 | 
			
		||||
            </PopoverTrigger>
 | 
			
		||||
            <PopoverContent className="w-52">
 | 
			
		||||
                <div className="flex flex-col gap-2">
 | 
			
		||||
                    <div className="text-sm font-semibold">
 | 
			
		||||
                        {t(
 | 
			
		||||
                            'side_panel.tables_section.table.field_actions.title'
 | 
			
		||||
                        )}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <Separator orientation="horizontal" />
 | 
			
		||||
                    <div className="flex flex-col gap-3">
 | 
			
		||||
                        <div className="flex items-center justify-between">
 | 
			
		||||
                            <Label htmlFor="width" className="text-subtitle">
 | 
			
		||||
                                {t(
 | 
			
		||||
                                    'side_panel.tables_section.table.field_actions.unique'
 | 
			
		||||
                                )}
 | 
			
		||||
                            </Label>
 | 
			
		||||
                            <Checkbox
 | 
			
		||||
                                checked={localField.unique}
 | 
			
		||||
                                disabled={field.primaryKey}
 | 
			
		||||
                                onCheckedChange={(value) =>
 | 
			
		||||
                                    setLocalField((current) => ({
 | 
			
		||||
                                        ...current,
 | 
			
		||||
                                        unique: !!value,
 | 
			
		||||
                                    }))
 | 
			
		||||
                                }
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
                        {findDataTypeDataById(field.type.id)
 | 
			
		||||
                            ?.hasCharMaxLength ? (
 | 
			
		||||
                            <div className="flex flex-col gap-2">
 | 
			
		||||
                                <Label
 | 
			
		||||
                                    htmlFor="width"
 | 
			
		||||
                                    className="text-subtitle"
 | 
			
		||||
                                >
 | 
			
		||||
                                    {t(
 | 
			
		||||
                                        'side_panel.tables_section.table.field_actions.character_length'
 | 
			
		||||
                                    )}
 | 
			
		||||
                                </Label>
 | 
			
		||||
                                <Input
 | 
			
		||||
                                    value={
 | 
			
		||||
                                        localField.characterMaximumLength ?? ''
 | 
			
		||||
                                    }
 | 
			
		||||
                                    type="number"
 | 
			
		||||
                                    onChange={(e) =>
 | 
			
		||||
                                        setLocalField((current) => ({
 | 
			
		||||
                                            ...current,
 | 
			
		||||
                                            characterMaximumLength:
 | 
			
		||||
                                                e.target.value,
 | 
			
		||||
                                        }))
 | 
			
		||||
                                    }
 | 
			
		||||
                                    className="w-full rounded-md bg-muted text-sm"
 | 
			
		||||
                                />
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ) : null}
 | 
			
		||||
                        <div className="flex flex-col gap-2">
 | 
			
		||||
                            <Label htmlFor="width" className="text-subtitle">
 | 
			
		||||
                                {t(
 | 
			
		||||
                                    'side_panel.tables_section.table.field_actions.comments'
 | 
			
		||||
                                )}
 | 
			
		||||
                            </Label>
 | 
			
		||||
                            <Textarea
 | 
			
		||||
                                value={localField.comments}
 | 
			
		||||
                                onChange={(e) =>
 | 
			
		||||
                                    setLocalField((current) => ({
 | 
			
		||||
                                        ...current,
 | 
			
		||||
                                        comments: e.target.value,
 | 
			
		||||
                                    }))
 | 
			
		||||
                                }
 | 
			
		||||
                                placeholder={t(
 | 
			
		||||
                                    'side_panel.tables_section.table.field_actions.no_comments'
 | 
			
		||||
                                )}
 | 
			
		||||
                                className="w-full rounded-md bg-muted text-sm"
 | 
			
		||||
                            />
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <Separator orientation="horizontal" />
 | 
			
		||||
                    <Button
 | 
			
		||||
                        variant="outline"
 | 
			
		||||
                        className="flex gap-2 !text-red-700"
 | 
			
		||||
                        onClick={removeField}
 | 
			
		||||
                    >
 | 
			
		||||
                        <Trash2 className="size-3.5 text-red-700" />
 | 
			
		||||
                        {t(
 | 
			
		||||
                            'side_panel.tables_section.table.field_actions.delete_field'
 | 
			
		||||
                        )}
 | 
			
		||||
                    </Button>
 | 
			
		||||
                </div>
 | 
			
		||||
            </PopoverContent>
 | 
			
		||||
        </Popover>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,30 +1,27 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Ellipsis, GripVertical, Trash2, KeyRound } from 'lucide-react';
 | 
			
		||||
import React, { useCallback } from 'react';
 | 
			
		||||
import { GripVertical, KeyRound } from 'lucide-react';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { Separator } from '@/components/separator/separator';
 | 
			
		||||
 | 
			
		||||
import type { DBField } from '@/lib/domain/db-field';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { dataTypeMap } from '@/lib/data/data-types/data-types';
 | 
			
		||||
import {
 | 
			
		||||
    dataTypeDataToDataType,
 | 
			
		||||
    dataTypeMap,
 | 
			
		||||
} from '@/lib/data/data-types/data-types';
 | 
			
		||||
import {
 | 
			
		||||
    Tooltip,
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
    TooltipTrigger,
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import {
 | 
			
		||||
    Popover,
 | 
			
		||||
    PopoverContent,
 | 
			
		||||
    PopoverTrigger,
 | 
			
		||||
} from '@/components/popover/popover';
 | 
			
		||||
import { Label } from '@/components/label/label';
 | 
			
		||||
import { Checkbox } from '@/components/checkbox/checkbox';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { Textarea } from '@/components/textarea/textarea';
 | 
			
		||||
import { TableFieldToggle } from './table-field-toggle';
 | 
			
		||||
import { useSortable } from '@dnd-kit/sortable';
 | 
			
		||||
import { CSS } from '@dnd-kit/utilities';
 | 
			
		||||
import type {
 | 
			
		||||
    SelectBoxOption,
 | 
			
		||||
    SelectBoxProps,
 | 
			
		||||
} from '@/components/select-box/select-box';
 | 
			
		||||
import { SelectBox } from '@/components/select-box/select-box';
 | 
			
		||||
import { TableFieldPopover } from './table-field-modal/table-field-modal';
 | 
			
		||||
 | 
			
		||||
export interface TableFieldProps {
 | 
			
		||||
    field: DBField;
 | 
			
		||||
@@ -39,13 +36,55 @@ export const TableField: React.FC<TableFieldProps> = ({
 | 
			
		||||
}) => {
 | 
			
		||||
    const { databaseType } = useChartDB();
 | 
			
		||||
    const { t } = useTranslation();
 | 
			
		||||
 | 
			
		||||
    const { attributes, listeners, setNodeRef, transform, transition } =
 | 
			
		||||
        useSortable({ id: field.id });
 | 
			
		||||
 | 
			
		||||
    const dataFieldOptions = dataTypeMap[databaseType].map((type) => ({
 | 
			
		||||
    const dataFieldOptions: SelectBoxOption[] = dataTypeMap[databaseType].map(
 | 
			
		||||
        (type) => ({
 | 
			
		||||
            label: type.name,
 | 
			
		||||
            value: type.id,
 | 
			
		||||
    }));
 | 
			
		||||
            regex: type.hasCharMaxLength
 | 
			
		||||
                ? `^${type.name}\\(\\d+\\)$`
 | 
			
		||||
                : undefined,
 | 
			
		||||
            extractRegex: type.hasCharMaxLength ? /\((\d+)\)/ : undefined,
 | 
			
		||||
        })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const onChangeDataType = useCallback<
 | 
			
		||||
        NonNullable<SelectBoxProps['onChange']>
 | 
			
		||||
    >(
 | 
			
		||||
        (value, regexMatches) => {
 | 
			
		||||
            const dataType = dataTypeMap[databaseType].find(
 | 
			
		||||
                (v) => v.id === value
 | 
			
		||||
            ) ?? {
 | 
			
		||||
                id: value as string,
 | 
			
		||||
                name: value as string,
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            let characterMaximumLength: string | undefined = undefined;
 | 
			
		||||
 | 
			
		||||
            if (regexMatches?.length && dataType?.hasCharMaxLength) {
 | 
			
		||||
                characterMaximumLength = regexMatches[1];
 | 
			
		||||
            } else if (
 | 
			
		||||
                field.characterMaximumLength &&
 | 
			
		||||
                dataType?.hasCharMaxLength
 | 
			
		||||
            ) {
 | 
			
		||||
                characterMaximumLength = field.characterMaximumLength;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            updateField({
 | 
			
		||||
                characterMaximumLength,
 | 
			
		||||
                type: dataTypeDataToDataType(
 | 
			
		||||
                    dataType ?? {
 | 
			
		||||
                        id: value as string,
 | 
			
		||||
                        name: value as string,
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
            });
 | 
			
		||||
        },
 | 
			
		||||
        [updateField, databaseType, field.characterMaximumLength]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const style = {
 | 
			
		||||
        transform: CSS.Translate.toString(transform),
 | 
			
		||||
@@ -96,20 +135,39 @@ export const TableField: React.FC<TableFieldProps> = ({
 | 
			
		||||
                                    'side_panel.tables_section.table.field_type'
 | 
			
		||||
                                )}
 | 
			
		||||
                                value={field.type.id}
 | 
			
		||||
                                onChange={(value) =>
 | 
			
		||||
                                    updateField({
 | 
			
		||||
                                        type: dataTypeMap[databaseType].find(
 | 
			
		||||
                                            (v) => v.id === value
 | 
			
		||||
                                        ),
 | 
			
		||||
                                    })
 | 
			
		||||
                                valueSuffix={
 | 
			
		||||
                                    field.characterMaximumLength
 | 
			
		||||
                                        ? `(${field.characterMaximumLength})`
 | 
			
		||||
                                        : ''
 | 
			
		||||
                                }
 | 
			
		||||
                                optionSuffix={(option) => {
 | 
			
		||||
                                    const type = dataTypeMap[databaseType].find(
 | 
			
		||||
                                        (v) => v.id === option.value
 | 
			
		||||
                                    );
 | 
			
		||||
 | 
			
		||||
                                    if (!type) {
 | 
			
		||||
                                        return '';
 | 
			
		||||
                                    }
 | 
			
		||||
 | 
			
		||||
                                    if (type.hasCharMaxLength) {
 | 
			
		||||
                                        return `(${!field.characterMaximumLength ? 'n' : field.characterMaximumLength})`;
 | 
			
		||||
                                    }
 | 
			
		||||
 | 
			
		||||
                                    return '';
 | 
			
		||||
                                }}
 | 
			
		||||
                                onChange={onChangeDataType}
 | 
			
		||||
                                emptyPlaceholder={t(
 | 
			
		||||
                                    'side_panel.tables_section.table.no_types_found'
 | 
			
		||||
                                )}
 | 
			
		||||
                            />
 | 
			
		||||
                        </span>
 | 
			
		||||
                    </TooltipTrigger>
 | 
			
		||||
                    <TooltipContent>{field.type.name}</TooltipContent>
 | 
			
		||||
                    <TooltipContent>
 | 
			
		||||
                        {field.type.name}
 | 
			
		||||
                        {field.characterMaximumLength
 | 
			
		||||
                            ? `(${field.characterMaximumLength})`
 | 
			
		||||
                            : ''}
 | 
			
		||||
                    </TooltipContent>
 | 
			
		||||
                </Tooltip>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="flex w-4/12 justify-end gap-1 overflow-hidden">
 | 
			
		||||
@@ -152,81 +210,12 @@ export const TableField: React.FC<TableFieldProps> = ({
 | 
			
		||||
                        {t('side_panel.tables_section.table.primary_key')}
 | 
			
		||||
                    </TooltipContent>
 | 
			
		||||
                </Tooltip>
 | 
			
		||||
                <Popover>
 | 
			
		||||
                    <PopoverTrigger asChild>
 | 
			
		||||
                        <Button
 | 
			
		||||
                            variant="ghost"
 | 
			
		||||
                            className="h-8 w-[32px] p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
 | 
			
		||||
                        >
 | 
			
		||||
                            <Ellipsis className="size-3.5" />
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    </PopoverTrigger>
 | 
			
		||||
                    <PopoverContent className="w-52">
 | 
			
		||||
                        <div className="flex flex-col gap-2">
 | 
			
		||||
                            <div className="text-sm font-semibold">
 | 
			
		||||
                                {t(
 | 
			
		||||
                                    'side_panel.tables_section.table.field_actions.title'
 | 
			
		||||
                                )}
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <Separator orientation="horizontal" />
 | 
			
		||||
                            <div className="flex flex-col gap-3">
 | 
			
		||||
                                <div className="flex items-center justify-between">
 | 
			
		||||
                                    <Label
 | 
			
		||||
                                        htmlFor="width"
 | 
			
		||||
                                        className="text-subtitle"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {t(
 | 
			
		||||
                                            'side_panel.tables_section.table.field_actions.unique'
 | 
			
		||||
                                        )}
 | 
			
		||||
                                    </Label>
 | 
			
		||||
                                    <Checkbox
 | 
			
		||||
                                        checked={field.unique}
 | 
			
		||||
                                        disabled={field.primaryKey}
 | 
			
		||||
                                        onCheckedChange={(value) =>
 | 
			
		||||
                                            updateField({
 | 
			
		||||
                                                unique: !!value,
 | 
			
		||||
                                            })
 | 
			
		||||
                                        }
 | 
			
		||||
                <TableFieldPopover
 | 
			
		||||
                    field={field}
 | 
			
		||||
                    updateField={updateField}
 | 
			
		||||
                    removeField={removeField}
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
                                <div className="flex flex-col gap-2">
 | 
			
		||||
                                    <Label
 | 
			
		||||
                                        htmlFor="width"
 | 
			
		||||
                                        className="text-subtitle"
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {t(
 | 
			
		||||
                                            'side_panel.tables_section.table.field_actions.comments'
 | 
			
		||||
                                        )}
 | 
			
		||||
                                    </Label>
 | 
			
		||||
                                    <Textarea
 | 
			
		||||
                                        value={field.comments}
 | 
			
		||||
                                        onChange={(e) =>
 | 
			
		||||
                                            updateField({
 | 
			
		||||
                                                comments: e.target.value,
 | 
			
		||||
                                            })
 | 
			
		||||
                                        }
 | 
			
		||||
                                        placeholder={t(
 | 
			
		||||
                                            'side_panel.tables_section.table.field_actions.no_comments'
 | 
			
		||||
                                        )}
 | 
			
		||||
                                        className="w-full rounded-md bg-muted text-sm"
 | 
			
		||||
                                    />
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            <Separator orientation="horizontal" />
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant="outline"
 | 
			
		||||
                                className="flex gap-2 !text-red-700"
 | 
			
		||||
                                onClick={removeField}
 | 
			
		||||
                            >
 | 
			
		||||
                                <Trash2 className="size-3.5 text-red-700" />
 | 
			
		||||
                                {t(
 | 
			
		||||
                                    'side_panel.tables_section.table.field_actions.delete_field'
 | 
			
		||||
                                )}
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </PopoverContent>
 | 
			
		||||
                </Popover>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -470,8 +470,16 @@ export const Menu: React.FC<MenuProps> = () => {
 | 
			
		||||
                    </MenubarSub>
 | 
			
		||||
                    <MenubarSeparator />
 | 
			
		||||
                    <MenubarSub>
 | 
			
		||||
                        <MenubarSubTrigger>
 | 
			
		||||
                            {t('menu.view.theme')}
 | 
			
		||||
                        <MenubarSubTrigger className="flex items-center gap-1">
 | 
			
		||||
                            <span>{t('menu.view.theme')}</span>
 | 
			
		||||
                            <div className="flex-1" />
 | 
			
		||||
                            <MenubarShortcut>
 | 
			
		||||
                                {
 | 
			
		||||
                                    keyboardShortcutsForOS[
 | 
			
		||||
                                        KeyboardShortcutAction.TOGGLE_THEME
 | 
			
		||||
                                    ].keyCombinationLabel
 | 
			
		||||
                                }
 | 
			
		||||
                            </MenubarShortcut>
 | 
			
		||||
                        </MenubarSubTrigger>
 | 
			
		||||
                        <MenubarSubContent>
 | 
			
		||||
                            <MenubarCheckboxItem
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ import { DiagramName } from './diagram-name';
 | 
			
		||||
import { LastSaved } from './last-saved';
 | 
			
		||||
import { LanguageNav } from './language-nav/language-nav';
 | 
			
		||||
import { Menu } from './menu/menu';
 | 
			
		||||
import { HIDE_BUCKLE_DOT_DEV } from '@/lib/env';
 | 
			
		||||
 | 
			
		||||
export interface TopNavbarProps {}
 | 
			
		||||
 | 
			
		||||
@@ -26,27 +25,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
 | 
			
		||||
        );
 | 
			
		||||
    }, [isDesktop]);
 | 
			
		||||
 | 
			
		||||
    const openBuckleWaitlist = useCallback(() => {
 | 
			
		||||
        window.open('https://waitlist.buckle.dev', '_blank');
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const renderGetBuckleButton = useCallback(() => {
 | 
			
		||||
        if (HIDE_BUCKLE_DOT_DEV) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <button
 | 
			
		||||
                className="gradient-background relative inline-flex items-center justify-center overflow-hidden rounded-lg p-0.5 text-base text-gray-700 focus:outline-none focus:ring-0"
 | 
			
		||||
                onClick={openBuckleWaitlist}
 | 
			
		||||
            >
 | 
			
		||||
                <span className="relative inline-flex items-center justify-center whitespace-nowrap rounded-md bg-background px-2 py-0.5 font-primary text-xs font-semibold text-foreground md:text-sm">
 | 
			
		||||
                    ChartDB v2.0 🔥
 | 
			
		||||
                </span>
 | 
			
		||||
            </button>
 | 
			
		||||
        );
 | 
			
		||||
    }, [openBuckleWaitlist]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <nav className="flex flex-col justify-between border-b px-3 md:h-12 md:flex-row md:items-center md:px-4">
 | 
			
		||||
            <div className="flex flex-1 flex-col justify-between gap-x-1 md:flex-row md:justify-normal">
 | 
			
		||||
@@ -68,7 +46,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
 | 
			
		||||
                    </a>
 | 
			
		||||
                    {!isDesktop ? (
 | 
			
		||||
                        <div className="flex items-center gap-2">
 | 
			
		||||
                            {renderGetBuckleButton()}
 | 
			
		||||
                            {renderStars()}
 | 
			
		||||
                            <LanguageNav />
 | 
			
		||||
                        </div>
 | 
			
		||||
@@ -80,7 +57,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
 | 
			
		||||
                <>
 | 
			
		||||
                    <DiagramName />
 | 
			
		||||
                    <div className="hidden flex-1 items-center justify-end gap-2 sm:flex">
 | 
			
		||||
                        {renderGetBuckleButton()}
 | 
			
		||||
                        <LastSaved />
 | 
			
		||||
                        {renderStars()}
 | 
			
		||||
                        <LanguageNav />
 | 
			
		||||
 
 | 
			
		||||
@@ -289,7 +289,6 @@ const TemplatePageComponent: React.FC = () => {
 | 
			
		||||
                                        readonly
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <Canvas
 | 
			
		||||
                                            readonly
 | 
			
		||||
                                            initialTables={
 | 
			
		||||
                                                template.diagram.tables ?? []
 | 
			
		||||
                                            }
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user