fix: manipulate schema directly from the canvas (#947)

This commit is contained in:
Guy Ben-Aharon
2025-10-16 17:37:20 +03:00
committed by GitHub
parent 34475add32
commit 7ad0e7712d
2 changed files with 202 additions and 74 deletions

View File

@@ -14,14 +14,14 @@ import { Table, Workflow, Group, View } from 'lucide-react';
import { useDiagramFilter } from '@/context/diagram-filter-context/use-diagram-filter';
import { useLocalConfig } from '@/hooks/use-local-config';
import { useCanvas } from '@/hooks/use-canvas';
import type { DBTable } from '@/lib/domain';
import { defaultSchemas } from '@/lib/data/default-schemas';
export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const { createTable, readonly, createArea } = useChartDB();
const { createTable, readonly, createArea, databaseType } = useChartDB();
const { schemasDisplayed } = useDiagramFilter();
const { openCreateRelationshipDialog, openTableSchemaDialog } = useDialog();
const { openCreateRelationshipDialog } = useDialog();
const { screenToFlowPosition } = useReactFlow();
const { t } = useTranslation();
const { showDBViews } = useLocalConfig();
@@ -36,31 +36,24 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
y: event.clientY,
});
let newTable: DBTable | null = null;
if (schemasDisplayed.length > 1) {
openTableSchemaDialog({
onConfirm: async ({ schema }) => {
newTable = await createTable({
x: position.x,
y: position.y,
schema: schema.name,
});
},
schemas: schemasDisplayed,
});
} else {
const schema =
schemasDisplayed?.length === 1
? schemasDisplayed[0]?.name
: undefined;
newTable = await createTable({
x: position.x,
y: position.y,
schema,
});
// Auto-select schema with priority: default schema > first displayed schema > undefined
let schema: string | undefined = undefined;
if (schemasDisplayed.length > 0) {
const defaultSchemaName = defaultSchemas[databaseType];
const defaultSchemaInList = schemasDisplayed.find(
(s) => s.name === defaultSchemaName
);
schema = defaultSchemaInList
? defaultSchemaInList.name
: schemasDisplayed[0]?.name;
}
const newTable = await createTable({
x: position.x,
y: position.y,
schema,
});
if (newTable) {
setEditTableModeTable({ tableId: newTable.id });
}
@@ -68,9 +61,9 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
[
createTable,
screenToFlowPosition,
openTableSchemaDialog,
schemasDisplayed,
setEditTableModeTable,
databaseType,
]
);
@@ -81,33 +74,25 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
y: event.clientY,
});
let newView: DBTable | null = null;
if (schemasDisplayed.length > 1) {
openTableSchemaDialog({
onConfirm: async ({ schema }) => {
newView = await createTable({
x: position.x,
y: position.y,
schema: schema.name,
isView: true,
});
},
schemas: schemasDisplayed,
});
} else {
const schema =
schemasDisplayed?.length === 1
? schemasDisplayed[0]?.name
: undefined;
newView = await createTable({
x: position.x,
y: position.y,
schema,
isView: true,
});
// Auto-select schema with priority: default schema > first displayed schema > undefined
let schema: string | undefined = undefined;
if (schemasDisplayed.length > 0) {
const defaultSchemaName = defaultSchemas[databaseType];
const defaultSchemaInList = schemasDisplayed.find(
(s) => s.name === defaultSchemaName
);
schema = defaultSchemaInList
? defaultSchemaInList.name
: schemasDisplayed[0]?.name;
}
const newView = await createTable({
x: position.x,
y: position.y,
schema,
isView: true,
});
if (newView) {
setEditTableModeTable({ tableId: newView.id });
}
@@ -115,9 +100,9 @@ export const CanvasContextMenu: React.FC<React.PropsWithChildren> = ({
[
createTable,
screenToFlowPosition,
openTableSchemaDialog,
schemasDisplayed,
setEditTableModeTable,
databaseType,
]
);

View File

@@ -1,7 +1,13 @@
import { Input } from '@/components/input/input';
import type { DBTable } from '@/lib/domain';
import { FileType2, X } from 'lucide-react';
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { FileType2, X, SquarePlus } from 'lucide-react';
import React, {
useEffect,
useState,
useRef,
useCallback,
useMemo,
} from 'react';
import { TableEditModeField } from './table-edit-mode-field';
import { cn } from '@/lib/utils';
import { ScrollArea } from '@/components/scroll-area/scroll-area';
@@ -11,6 +17,14 @@ import { Separator } from '@/components/separator/separator';
import { useChartDB } from '@/hooks/use-chartdb';
import { useUpdateTable } from '@/hooks/use-update-table';
import { useTranslation } from 'react-i18next';
import { SelectBox } from '@/components/select-box/select-box';
import type { SelectBoxOption } from '@/components/select-box/select-box';
import {
databasesWithSchemas,
schemaNameToSchemaId,
} from '@/lib/domain/db-schema';
import type { DBSchema } from '@/lib/domain/db-schema';
import { defaultSchemas } from '@/lib/data/default-schemas';
export interface TableEditModeProps {
table: DBTable;
@@ -25,7 +39,8 @@ export const TableEditMode: React.FC<TableEditModeProps> = React.memo(
const scrollAreaRef = useRef<HTMLDivElement>(null);
const fieldRefs = useRef<Map<string, HTMLDivElement>>(new Map());
const [isVisible, setIsVisible] = useState(false);
const { createField, updateTable } = useChartDB();
const { createField, updateTable, schemas, databaseType } =
useChartDB();
const { t } = useTranslation();
const { tableName, handleTableNameChange } = useUpdateTable(table);
const [focusFieldId, setFocusFieldId] = useState<string | undefined>(
@@ -33,6 +48,39 @@ export const TableEditMode: React.FC<TableEditModeProps> = React.memo(
);
const inputRef = useRef<HTMLInputElement>(null);
// Schema-related state
const [isCreatingNewSchema, setIsCreatingNewSchema] = useState(false);
const [newSchemaName, setNewSchemaName] = useState('');
const [selectedSchemaId, setSelectedSchemaId] = useState<string>(() =>
table.schema ? schemaNameToSchemaId(table.schema) : ''
);
// Sync selectedSchemaId when table.schema changes
useEffect(() => {
setSelectedSchemaId(
table.schema ? schemaNameToSchemaId(table.schema) : ''
);
}, [table.schema]);
const supportsSchemas = useMemo(
() => databasesWithSchemas.includes(databaseType),
[databaseType]
);
const defaultSchemaName = useMemo(
() => defaultSchemas?.[databaseType],
[databaseType]
);
const schemaOptions: SelectBoxOption[] = useMemo(
() =>
schemas.map((schema) => ({
value: schema.id,
label: schema.name,
})),
[schemas]
);
useEffect(() => {
setFocusFieldId(focusFieldIdProp);
if (!focusFieldIdProp) {
@@ -115,6 +163,43 @@ export const TableEditMode: React.FC<TableEditModeProps> = React.memo(
[updateTable, table.id]
);
const handleSchemaChange = useCallback(
(schemaId: string) => {
const schema = schemas.find((s) => s.id === schemaId);
if (schema) {
updateTable(table.id, { schema: schema.name });
setSelectedSchemaId(schemaId);
}
},
[schemas, updateTable, table.id]
);
const handleCreateNewSchema = useCallback(() => {
if (newSchemaName.trim()) {
const trimmedName = newSchemaName.trim();
const newSchema: DBSchema = {
id: schemaNameToSchemaId(trimmedName),
name: trimmedName,
tableCount: 0,
};
updateTable(table.id, { schema: newSchema.name });
setSelectedSchemaId(newSchema.id);
setIsCreatingNewSchema(false);
setNewSchemaName('');
}
}, [newSchemaName, updateTable, table.id]);
const handleToggleSchemaMode = useCallback(() => {
if (isCreatingNewSchema && newSchemaName.trim()) {
// If we're leaving create mode with a value, create the schema
handleCreateNewSchema();
} else {
// Otherwise just toggle modes
setIsCreatingNewSchema(!isCreatingNewSchema);
setNewSchemaName('');
}
}, [isCreatingNewSchema, newSchemaName, handleCreateNewSchema]);
return (
<div
ref={containerRef}
@@ -134,18 +219,60 @@ export const TableEditMode: React.FC<TableEditModeProps> = React.memo(
onClick={(e) => e.stopPropagation()}
>
<div
className="h-2 rounded-t-[6px]"
className="h-2 cursor-move rounded-t-[6px]"
style={{ backgroundColor: color }}
></div>
<div className="group flex h-9 items-center justify-between gap-2 bg-slate-200 px-2 dark:bg-slate-900">
<div className="group flex h-9 cursor-move items-center justify-between gap-2 bg-slate-200 px-2 dark:bg-slate-900">
<div className="flex min-w-0 flex-1 items-center gap-2">
<ColorPicker
color={color}
onChange={handleColorChange}
disabled={table.isView}
popoverOnMouseDown={(e) => e.stopPropagation()}
popoverOnClick={(e) => e.stopPropagation()}
/>
{supportsSchemas && !isCreatingNewSchema && (
<SelectBox
options={schemaOptions}
value={selectedSchemaId}
onChange={(value) =>
handleSchemaChange(value as string)
}
placeholder={
defaultSchemaName || 'Select schema'
}
className="h-6 min-h-6 w-20 shrink-0 rounded-sm border-slate-600 bg-background py-0 pl-2 pr-0.5 text-sm"
popoverClassName="w-[200px]"
commandOnMouseDown={(e) => e.stopPropagation()}
commandOnClick={(e) => e.stopPropagation()}
footerButtons={
<Button
variant="ghost"
size="sm"
className="w-full justify-center rounded-none text-xs"
onClick={(e) => {
e.stopPropagation();
handleToggleSchemaMode();
}}
>
<SquarePlus className="!size-3.5" />
Create new schema
</Button>
}
/>
)}
{supportsSchemas && isCreatingNewSchema && (
<Input
value={newSchemaName}
onChange={(e) =>
setNewSchemaName(e.target.value)
}
placeholder={`Enter schema name${defaultSchemaName ? ` (e.g. ${defaultSchemaName})` : ''}`}
className="h-6 w-28 shrink-0 rounded-sm border-slate-600 bg-background text-sm"
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleCreateNewSchema();
} else if (e.key === 'Escape') {
handleToggleSchemaMode();
}
}}
onBlur={handleToggleSchemaMode}
autoFocus
/>
)}
<Input
ref={inputRef}
className="h-6 flex-1 rounded-sm border-slate-600 bg-background text-sm"
@@ -179,15 +306,31 @@ export const TableEditMode: React.FC<TableEditModeProps> = React.memo(
</ScrollArea>
<Separator />
<div className="flex items-center justify-between p-2">
<Button
variant="outline"
className="h-8 p-2 text-xs"
onClick={handleAddField}
>
<FileType2 className="mr-1 h-4" />
{t('side_panel.tables_section.table.add_field')}
</Button>
<div className="flex cursor-move items-center justify-between p-2">
<div className="flex items-center gap-2">
{!table.isView ? (
<>
<ColorPicker
color={color}
onChange={handleColorChange}
popoverOnMouseDown={(e) =>
e.stopPropagation()
}
popoverOnClick={(e) => e.stopPropagation()}
/>
</>
) : (
<div />
)}
<Button
variant="outline"
className="h-8 p-2 text-xs"
onClick={handleAddField}
>
<FileType2 className="mr-1 h-4" />
{t('side_panel.tables_section.table.add_field')}
</Button>
</div>
<span className="text-xs font-medium text-muted-foreground">
{table.fields.length}{' '}
{t('side_panel.tables_section.table.fields')}