fix(canvas edit): add option to edit names in canvas (#536)

* fix(canvas edit): add option to edit names in canvas

* fix(canvas edit): add option to edit field names in canvas

* some few fixes + style

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
This commit is contained in:
Jonathan Fishner
2025-01-27 14:26:04 +02:00
committed by GitHub
parent ff3269ec05
commit 0dcc9b9568
3 changed files with 201 additions and 52 deletions
@@ -1,4 +1,10 @@
import React, { useEffect, useMemo, useRef } from 'react';
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import {
Handle,
Position,
@@ -6,7 +12,7 @@ import {
useUpdateNodeInternals,
} from '@xyflow/react';
import { Button } from '@/components/button/button';
import { KeyRound, MessageCircleMore, Trash2 } from 'lucide-react';
import { Check, KeyRound, MessageCircleMore, Trash2 } from 'lucide-react';
import type { DBField } from '@/lib/domain/db-field';
import { useChartDB } from '@/hooks/use-chartdb';
import { cn } from '@/lib/utils';
@@ -15,6 +21,8 @@ import {
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { useClickAway, useKeyPressEvent } from 'react-use';
import { Input } from '@/components/input/input';
export const LEFT_HANDLE_ID_PREFIX = 'left_rel_';
export const RIGHT_HANDLE_ID_PREFIX = 'right_rel_';
@@ -31,7 +39,12 @@ export interface TableNodeFieldProps {
export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
({ field, focused, tableNodeId, highlighted, visible, isConnectable }) => {
const { removeField, relationships, readonly } = useChartDB();
const { removeField, relationships, readonly, updateField } =
useChartDB();
const [editMode, setEditMode] = useState(false);
const [fieldName, setFieldName] = useState(field.name);
const inputRef = React.useRef<HTMLInputElement>(null);
const updateNodeInternals = useUpdateNodeInternals();
const connection = useConnection();
const isTarget = useMemo(
@@ -65,6 +78,28 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
}
}, [tableNodeId, updateNodeInternals, numberOfEdgesToField]);
const editFieldName = useCallback(() => {
if (!editMode) return;
if (fieldName.trim()) {
updateField(tableNodeId, field.id, { name: fieldName.trim() });
}
setEditMode(false);
}, [fieldName, field.id, updateField, editMode, tableNodeId]);
const abortEdit = useCallback(() => {
setEditMode(false);
setFieldName(field.name);
}, [field.name]);
useClickAway(inputRef, editFieldName);
useKeyPressEvent('Enter', editFieldName);
useKeyPressEvent('Escape', abortEdit);
const enterEditMode = (e: React.MouseEvent) => {
e.stopPropagation();
setEditMode(true);
};
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 ${
@@ -122,11 +157,47 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
'flex items-center gap-1 truncate text-left',
{
'font-semibold': field.primaryKey || field.unique,
'w-full': editMode,
}
)}
>
<span className="truncate">{field.name}</span>
{field.comments ? (
{editMode ? (
<>
<Input
ref={inputRef}
onBlur={editFieldName}
placeholder={field.name}
autoFocus
type="text"
value={fieldName}
onClick={(e) => e.stopPropagation()}
onChange={(e) => setFieldName(e.target.value)}
className="h-5 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900"
/>
<Button
variant="ghost"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
onClick={editFieldName}
>
<Check className="size-4" />
</Button>
</>
) : (
// <span
// className="truncate"
// onClick={readonly ? undefined : enterEditMode}
// >
// {field.name}
// </span>
<span
className="truncate"
onDoubleClick={enterEditMode}
>
{field.name}
</span>
)}
{/* <span className="truncate">{field.name}</span> */}
{field.comments && !editMode ? (
<Tooltip>
<TooltipTrigger asChild>
<div className="shrink-0 cursor-pointer text-muted-foreground">
@@ -137,42 +208,44 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
</Tooltip>
) : null}
</div>
<div className="flex max-w-[35%] justify-end gap-1.5 truncate hover:shrink-0">
{field.primaryKey ? (
{editMode ? null : (
<div className="flex max-w-[35%] justify-end gap-1.5 truncate hover:shrink-0">
{field.primaryKey ? (
<div
className={cn(
'text-muted-foreground',
!readonly ? 'group-hover:hidden' : ''
)}
>
<KeyRound size={14} />
</div>
) : null}
<div
className={cn(
'text-muted-foreground',
'content-center truncate text-right text-xs text-muted-foreground shrink-0',
!readonly ? 'group-hover:hidden' : ''
)}
>
<KeyRound size={14} />
{field.type.name}
{field.nullable ? '?' : ''}
</div>
) : null}
<div
className={cn(
'content-center truncate text-right text-xs text-muted-foreground shrink-0',
!readonly ? 'group-hover:hidden' : ''
{readonly ? null : (
<div className="hidden flex-row group-hover:flex">
<Button
variant="ghost"
className="size-6 p-0 hover:bg-primary-foreground"
onClick={(e) => {
e.stopPropagation();
removeField(tableNodeId, field.id);
}}
>
<Trash2 className="size-3.5 text-red-700" />
</Button>
</div>
)}
>
{field.type.name}
{field.nullable ? '?' : ''}
</div>
{readonly ? null : (
<div className="hidden flex-row group-hover:flex">
<Button
variant="ghost"
className="size-6 p-0 hover:bg-primary-foreground"
onClick={(e) => {
e.stopPropagation();
removeField(tableNodeId, field.id);
}}
>
<Trash2 className="size-3.5 text-red-700" />
</Button>
</div>
)}
</div>
)}
</div>
);
}
@@ -9,6 +9,7 @@ import {
Table2,
ChevronDown,
ChevronUp,
Check,
} from 'lucide-react';
import { Label } from '@/components/label/label';
import type { DBTable } from '@/lib/domain/db-table';
@@ -22,6 +23,13 @@ import { TableNodeContextMenu } from './table-node-context-menu';
import { cn } from '@/lib/utils';
import { TableNodeDependencyIndicator } from './table-node-dependency-indicator';
import type { EdgeType } from '../canvas';
import { Input } from '@/components/input/input';
import { useClickAway, useKeyPressEvent } from 'react-use';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
export type TableNodeType = Node<
{
@@ -49,6 +57,9 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
const { openTableFromSidebar, selectSidebarSection } = useLayout();
const [expanded, setExpanded] = useState(false);
const { t } = useTranslation();
const [editMode, setEditMode] = useState(false);
const [tableName, setTableName] = useState(table.name);
const inputRef = React.useRef<HTMLInputElement>(null);
const selectedRelEdges = edges.filter(
(edge) =>
@@ -125,6 +136,28 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
].sort((a, b) => table.fields.indexOf(a) - table.fields.indexOf(b));
}, [expanded, table.fields, isMustDisplayedField]);
const editTableName = useCallback(() => {
if (!editMode) return;
if (tableName.trim()) {
updateTable(table.id, { name: tableName.trim() });
}
setEditMode(false);
}, [tableName, table.id, updateTable, editMode]);
const abortEdit = useCallback(() => {
setEditMode(false);
setTableName(table.name);
}, [table.name]);
useClickAway(inputRef, editTableName);
useKeyPressEvent('Enter', editTableName);
useKeyPressEvent('Escape', abortEdit);
const enterEditMode = (e: React.MouseEvent) => {
e.stopPropagation();
setEditMode(true);
};
return (
<TableNodeContextMenu table={table}>
<div
@@ -168,12 +201,47 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
<div className="group flex h-9 items-center justify-between bg-slate-200 px-2 dark:bg-slate-900">
<div className="flex min-w-0 flex-1 items-center gap-2">
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
<Label className="truncate text-sm font-bold">
{table.name}
</Label>
{editMode ? (
<>
<Input
ref={inputRef}
onBlur={editTableName}
placeholder={table.name}
autoFocus
type="text"
value={tableName}
onClick={(e) => e.stopPropagation()}
onChange={(e) =>
setTableName(e.target.value)
}
className="h-6 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900"
/>
<Button
variant="ghost"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
onClick={editTableName}
>
<Check className="size-4" />
</Button>
</>
) : (
<Tooltip>
<TooltipTrigger asChild>
<Label
className="text-editable truncate px-2 py-0.5 text-sm font-bold"
onDoubleClick={enterEditMode}
>
{table.name}
</Label>
</TooltipTrigger>
<TooltipContent>
{t('tool_tips.double_click_to_edit')}
</TooltipContent>
</Tooltip>
)}
</div>
<div className="hidden shrink-0 flex-row group-hover:flex">
{readonly ? null : (
{readonly || editMode ? null : (
<Button
variant="ghost"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
@@ -182,21 +250,23 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
<Pencil className="size-4" />
</Button>
)}
<Button
variant="ghost"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
onClick={
table.width !== MAX_TABLE_SIZE
? expandTable
: shrinkTable
}
>
{table.width !== MAX_TABLE_SIZE ? (
<ChevronsLeftRight className="size-4" />
) : (
<ChevronsRightLeft className="size-4" />
)}
</Button>
{editMode ? null : (
<Button
variant="ghost"
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
onClick={
table.width !== MAX_TABLE_SIZE
? expandTable
: shrinkTable
}
>
{table.width !== MAX_TABLE_SIZE ? (
<ChevronsLeftRight className="size-4" />
) : (
<ChevronsRightLeft className="size-4" />
)}
</Button>
)}
</div>
</div>
<div
@@ -73,8 +73,14 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
setEditMode(false);
}, [tableName, table.id, updateTable, editMode]);
const abortEdit = useCallback(() => {
setEditMode(false);
setTableName(table.name);
}, [table.name]);
useClickAway(inputRef, editTableName);
useKeyPressEvent('Enter', editTableName);
useKeyPressEvent('Escape', abortEdit);
const enterEditMode = (e: React.MouseEvent) => {
e.stopPropagation();