mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-11-04 05:53:15 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			v1.17.0
			...
			jf/edit-cl
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					4fdc2ccd91 | 
							
								
								
									
										50
									
								
								src/hooks/use-click-outside.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/hooks/use-click-outside.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
			
		||||
import { useEffect, useCallback, type RefObject } from 'react';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Custom hook that handles click outside detection with capture phase
 | 
			
		||||
 * to work properly with React Flow canvas and other event-stopping elements
 | 
			
		||||
 */
 | 
			
		||||
export function useClickOutside(
 | 
			
		||||
    ref: RefObject<HTMLElement>,
 | 
			
		||||
    handler: () => void,
 | 
			
		||||
    isActive = true
 | 
			
		||||
) {
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!isActive) return;
 | 
			
		||||
 | 
			
		||||
        const handleClickOutside = (event: MouseEvent) => {
 | 
			
		||||
            if (ref.current && !ref.current.contains(event.target as Node)) {
 | 
			
		||||
                handler();
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Use capture phase to catch events before React Flow or other libraries can stop them
 | 
			
		||||
        document.addEventListener('mousedown', handleClickOutside, true);
 | 
			
		||||
 | 
			
		||||
        return () => {
 | 
			
		||||
            document.removeEventListener('mousedown', handleClickOutside, true);
 | 
			
		||||
        };
 | 
			
		||||
    }, [ref, handler, isActive]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Specialized version of useClickOutside for edit mode inputs
 | 
			
		||||
 * Adds a small delay to prevent race conditions with blur events
 | 
			
		||||
 */
 | 
			
		||||
export function useEditClickOutside(
 | 
			
		||||
    inputRef: RefObject<HTMLElement>,
 | 
			
		||||
    editMode: boolean,
 | 
			
		||||
    onSave: () => void,
 | 
			
		||||
    delay = 100
 | 
			
		||||
) {
 | 
			
		||||
    const handleClickOutside = useCallback(() => {
 | 
			
		||||
        if (editMode) {
 | 
			
		||||
            // Small delay to ensure any pending state updates are processed
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
                onSave();
 | 
			
		||||
            }, delay);
 | 
			
		||||
        }
 | 
			
		||||
    }, [editMode, onSave, delay]);
 | 
			
		||||
 | 
			
		||||
    useClickOutside(inputRef, handleClickOutside, editMode);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,54 @@
 | 
			
		||||
import {
 | 
			
		||||
    ContextMenu,
 | 
			
		||||
    ContextMenuContent,
 | 
			
		||||
    ContextMenuItem,
 | 
			
		||||
    ContextMenuTrigger,
 | 
			
		||||
} from '@/components/context-menu/context-menu';
 | 
			
		||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import { Pencil, Trash2 } from 'lucide-react';
 | 
			
		||||
import React, { useCallback } from 'react';
 | 
			
		||||
 | 
			
		||||
export interface AreaNodeContextMenuProps {
 | 
			
		||||
    area: Area;
 | 
			
		||||
    onEditName?: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AreaNodeContextMenu: React.FC<
 | 
			
		||||
    React.PropsWithChildren<AreaNodeContextMenuProps>
 | 
			
		||||
> = ({ children, area, onEditName }) => {
 | 
			
		||||
    const { removeArea, readonly } = useChartDB();
 | 
			
		||||
    const { isMd: isDesktop } = useBreakpoint('md');
 | 
			
		||||
 | 
			
		||||
    const removeAreaHandler = useCallback(() => {
 | 
			
		||||
        removeArea(area.id);
 | 
			
		||||
    }, [removeArea, area.id]);
 | 
			
		||||
 | 
			
		||||
    if (!isDesktop || readonly) {
 | 
			
		||||
        return <>{children}</>;
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
        <ContextMenu>
 | 
			
		||||
            <ContextMenuTrigger>{children}</ContextMenuTrigger>
 | 
			
		||||
            <ContextMenuContent>
 | 
			
		||||
                {onEditName && (
 | 
			
		||||
                    <ContextMenuItem
 | 
			
		||||
                        onClick={onEditName}
 | 
			
		||||
                        className="flex justify-between gap-3"
 | 
			
		||||
                    >
 | 
			
		||||
                        <span>Edit Area Name</span>
 | 
			
		||||
                        <Pencil className="size-3.5" />
 | 
			
		||||
                    </ContextMenuItem>
 | 
			
		||||
                )}
 | 
			
		||||
                <ContextMenuItem
 | 
			
		||||
                    onClick={removeAreaHandler}
 | 
			
		||||
                    className="flex justify-between gap-3"
 | 
			
		||||
                >
 | 
			
		||||
                    <span>Delete Area</span>
 | 
			
		||||
                    <Trash2 className="size-3.5 text-red-700" />
 | 
			
		||||
                </ContextMenuItem>
 | 
			
		||||
            </ContextMenuContent>
 | 
			
		||||
        </ContextMenu>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
import React, { useCallback, useState } from 'react';
 | 
			
		||||
import React, { useCallback, useEffect, useState } from 'react';
 | 
			
		||||
import type { NodeProps, Node } from '@xyflow/react';
 | 
			
		||||
import { NodeResizer } from '@xyflow/react';
 | 
			
		||||
import type { Area } from '@/lib/domain/area';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { useClickAway, useKeyPressEvent } from 'react-use';
 | 
			
		||||
import { useEditClickOutside } from '@/hooks/use-click-outside';
 | 
			
		||||
import { useKeyPressEvent } from 'react-use';
 | 
			
		||||
import {
 | 
			
		||||
    Tooltip,
 | 
			
		||||
    TooltipContent,
 | 
			
		||||
@@ -12,9 +13,10 @@ import {
 | 
			
		||||
} from '@/components/tooltip/tooltip';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
import { Check, GripVertical } from 'lucide-react';
 | 
			
		||||
import { Check, GripVertical, Pencil } from 'lucide-react';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { useLayout } from '@/hooks/use-layout';
 | 
			
		||||
import { AreaNodeContextMenu } from './area-node-context-menu';
 | 
			
		||||
 | 
			
		||||
export type AreaNodeType = Node<
 | 
			
		||||
    {
 | 
			
		||||
@@ -35,12 +37,11 @@ export const AreaNode: React.FC<NodeProps<AreaNodeType>> = React.memo(
 | 
			
		||||
        const focused = !!selected && !dragging;
 | 
			
		||||
 | 
			
		||||
        const editAreaName = useCallback(() => {
 | 
			
		||||
            if (!editMode) return;
 | 
			
		||||
            if (areaName.trim()) {
 | 
			
		||||
                updateArea(area.id, { name: areaName.trim() });
 | 
			
		||||
            }
 | 
			
		||||
            setEditMode(false);
 | 
			
		||||
        }, [areaName, area.id, updateArea, editMode]);
 | 
			
		||||
        }, [areaName, area.id, updateArea]);
 | 
			
		||||
 | 
			
		||||
        const abortEdit = useCallback(() => {
 | 
			
		||||
            setEditMode(false);
 | 
			
		||||
@@ -52,89 +53,119 @@ export const AreaNode: React.FC<NodeProps<AreaNodeType>> = React.memo(
 | 
			
		||||
            openAreaFromSidebar(area.id);
 | 
			
		||||
        }, [selectSidebarSection, openAreaFromSidebar, area.id]);
 | 
			
		||||
 | 
			
		||||
        useClickAway(inputRef, editAreaName);
 | 
			
		||||
        // Handle click outside to save and exit edit mode
 | 
			
		||||
        useEditClickOutside(inputRef, editMode, editAreaName);
 | 
			
		||||
        useKeyPressEvent('Enter', editAreaName);
 | 
			
		||||
        useKeyPressEvent('Escape', abortEdit);
 | 
			
		||||
 | 
			
		||||
        const enterEditMode = (e: React.MouseEvent) => {
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            setEditMode(true);
 | 
			
		||||
        };
 | 
			
		||||
        const enterEditMode = useCallback(
 | 
			
		||||
            (e?: React.MouseEvent) => {
 | 
			
		||||
                e?.stopPropagation();
 | 
			
		||||
                setAreaName(area.name);
 | 
			
		||||
                setEditMode(true);
 | 
			
		||||
            },
 | 
			
		||||
            [area.name]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        useEffect(() => {
 | 
			
		||||
            if (editMode) {
 | 
			
		||||
                // Small delay to ensure the input is rendered
 | 
			
		||||
                const timeoutId = setTimeout(() => {
 | 
			
		||||
                    if (inputRef.current) {
 | 
			
		||||
                        inputRef.current.focus();
 | 
			
		||||
                        inputRef.current.select();
 | 
			
		||||
                    }
 | 
			
		||||
                }, 50);
 | 
			
		||||
 | 
			
		||||
                return () => clearTimeout(timeoutId);
 | 
			
		||||
            }
 | 
			
		||||
        }, [editMode]);
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <div
 | 
			
		||||
                className={cn(
 | 
			
		||||
                    'flex h-full flex-col rounded-md border-2 shadow-sm',
 | 
			
		||||
                    selected ? 'border-pink-600' : 'border-transparent'
 | 
			
		||||
                )}
 | 
			
		||||
                style={{
 | 
			
		||||
                    backgroundColor: `${area.color}15`,
 | 
			
		||||
                    borderColor: selected ? undefined : area.color,
 | 
			
		||||
                }}
 | 
			
		||||
                onClick={(e) => {
 | 
			
		||||
                    if (e.detail === 2) {
 | 
			
		||||
                        openAreaInEditor();
 | 
			
		||||
                    }
 | 
			
		||||
                }}
 | 
			
		||||
            >
 | 
			
		||||
                {!readonly ? (
 | 
			
		||||
                    <NodeResizer
 | 
			
		||||
                        isVisible={focused}
 | 
			
		||||
                        lineClassName="!border-4 !border-transparent"
 | 
			
		||||
                        handleClassName="!h-[10px] !w-[10px] !rounded-full !bg-pink-600"
 | 
			
		||||
                        minHeight={100}
 | 
			
		||||
                        minWidth={100}
 | 
			
		||||
                    />
 | 
			
		||||
                ) : null}
 | 
			
		||||
                <div className="group flex h-8 items-center justify-between rounded-t-md px-2">
 | 
			
		||||
                    <div className="flex w-full items-center gap-1">
 | 
			
		||||
                        <GripVertical className="size-4 shrink-0 text-slate-700 opacity-60 dark:text-slate-300" />
 | 
			
		||||
            <AreaNodeContextMenu area={area} onEditName={enterEditMode}>
 | 
			
		||||
                <div
 | 
			
		||||
                    className={cn(
 | 
			
		||||
                        'flex h-full flex-col rounded-md border-2 shadow-sm',
 | 
			
		||||
                        selected ? 'border-pink-600' : 'border-transparent'
 | 
			
		||||
                    )}
 | 
			
		||||
                    style={{
 | 
			
		||||
                        backgroundColor: `${area.color}15`,
 | 
			
		||||
                        borderColor: selected ? undefined : area.color,
 | 
			
		||||
                    }}
 | 
			
		||||
                    onClick={(e) => {
 | 
			
		||||
                        if (e.detail === 2) {
 | 
			
		||||
                            openAreaInEditor();
 | 
			
		||||
                        }
 | 
			
		||||
                    }}
 | 
			
		||||
                >
 | 
			
		||||
                    {!readonly ? (
 | 
			
		||||
                        <NodeResizer
 | 
			
		||||
                            isVisible={focused}
 | 
			
		||||
                            lineClassName="!border-4 !border-transparent"
 | 
			
		||||
                            handleClassName="!h-[10px] !w-[10px] !rounded-full !bg-pink-600"
 | 
			
		||||
                            minHeight={100}
 | 
			
		||||
                            minWidth={100}
 | 
			
		||||
                        />
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                    <div className="group flex h-8 items-center justify-between rounded-t-md px-2">
 | 
			
		||||
                        <div className="flex w-full items-center gap-1">
 | 
			
		||||
                            <GripVertical className="size-4 shrink-0 text-slate-700 opacity-60 dark:text-slate-300" />
 | 
			
		||||
 | 
			
		||||
                        {editMode && !readonly ? (
 | 
			
		||||
                            <div className="flex w-full items-center">
 | 
			
		||||
                                <Input
 | 
			
		||||
                                    ref={inputRef}
 | 
			
		||||
                                    autoFocus
 | 
			
		||||
                                    type="text"
 | 
			
		||||
                                    placeholder={area.name}
 | 
			
		||||
                                    value={areaName}
 | 
			
		||||
                                    onClick={(e) => e.stopPropagation()}
 | 
			
		||||
                                    onChange={(e) =>
 | 
			
		||||
                                        setAreaName(e.target.value)
 | 
			
		||||
                                    }
 | 
			
		||||
                                    className="h-6 bg-white/70 focus-visible:ring-0 dark:bg-slate-900/70"
 | 
			
		||||
                                />
 | 
			
		||||
                            {editMode && !readonly ? (
 | 
			
		||||
                                <div className="flex w-full items-center">
 | 
			
		||||
                                    <Input
 | 
			
		||||
                                        ref={inputRef}
 | 
			
		||||
                                        autoFocus
 | 
			
		||||
                                        type="text"
 | 
			
		||||
                                        placeholder={area.name}
 | 
			
		||||
                                        value={areaName}
 | 
			
		||||
                                        onClick={(e) => e.stopPropagation()}
 | 
			
		||||
                                        onChange={(e) =>
 | 
			
		||||
                                            setAreaName(e.target.value)
 | 
			
		||||
                                        }
 | 
			
		||||
                                        className="h-6 bg-white/70 focus-visible:ring-0 dark:bg-slate-900/70"
 | 
			
		||||
                                    />
 | 
			
		||||
                                    <Button
 | 
			
		||||
                                        variant="ghost"
 | 
			
		||||
                                        className="ml-1 size-6 p-0 hover:bg-white/20"
 | 
			
		||||
                                        onClick={editAreaName}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        <Check className="size-3.5 text-slate-700 dark:text-slate-300" />
 | 
			
		||||
                                    </Button>
 | 
			
		||||
                                </div>
 | 
			
		||||
                            ) : !readonly ? (
 | 
			
		||||
                                <Tooltip>
 | 
			
		||||
                                    <TooltipTrigger asChild>
 | 
			
		||||
                                        <div
 | 
			
		||||
                                            className="text-editable truncate px-1 py-0.5 text-base font-semibold text-slate-700 dark:text-slate-300"
 | 
			
		||||
                                            onDoubleClick={enterEditMode}
 | 
			
		||||
                                        >
 | 
			
		||||
                                            {area.name}
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    </TooltipTrigger>
 | 
			
		||||
                                    <TooltipContent>
 | 
			
		||||
                                        {t('tool_tips.double_click_to_edit')}
 | 
			
		||||
                                    </TooltipContent>
 | 
			
		||||
                                </Tooltip>
 | 
			
		||||
                            ) : (
 | 
			
		||||
                                <div className="truncate px-1 py-0.5 text-base font-semibold text-slate-700 dark:text-slate-300">
 | 
			
		||||
                                    {area.name}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            )}
 | 
			
		||||
                            {!editMode && !readonly && (
 | 
			
		||||
                                <Button
 | 
			
		||||
                                    variant="ghost"
 | 
			
		||||
                                    className="ml-1 size-6 p-0 hover:bg-white/20"
 | 
			
		||||
                                    onClick={editAreaName}
 | 
			
		||||
                                    className="ml-auto size-5 p-0 opacity-0 transition-opacity hover:bg-white/20 group-hover:opacity-100"
 | 
			
		||||
                                    onClick={enterEditMode}
 | 
			
		||||
                                >
 | 
			
		||||
                                    <Check className="size-3.5 text-slate-700 dark:text-slate-300" />
 | 
			
		||||
                                    <Pencil className="size-3 text-slate-700 dark:text-slate-300" />
 | 
			
		||||
                                </Button>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        ) : !readonly ? (
 | 
			
		||||
                            <Tooltip>
 | 
			
		||||
                                <TooltipTrigger asChild>
 | 
			
		||||
                                    <div
 | 
			
		||||
                                        className="text-editable max-w-[200px] cursor-text truncate px-1 py-0.5 text-base font-semibold text-slate-700 dark:text-slate-300"
 | 
			
		||||
                                        onDoubleClick={enterEditMode}
 | 
			
		||||
                                    >
 | 
			
		||||
                                        {area.name}
 | 
			
		||||
                                    </div>
 | 
			
		||||
                                </TooltipTrigger>
 | 
			
		||||
                                <TooltipContent>
 | 
			
		||||
                                    {t('tool_tips.double_click_to_edit')}
 | 
			
		||||
                                </TooltipContent>
 | 
			
		||||
                            </Tooltip>
 | 
			
		||||
                        ) : (
 | 
			
		||||
                            <div className="truncate px-1 py-0.5 text-base font-semibold text-slate-700 dark:text-slate-300">
 | 
			
		||||
                                {area.name}
 | 
			
		||||
                            </div>
 | 
			
		||||
                        )}
 | 
			
		||||
                            )}
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="flex-1" />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="flex-1" />
 | 
			
		||||
            </div>
 | 
			
		||||
            </AreaNodeContextMenu>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ 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 { useClickOutside } from '@/hooks/use-click-outside';
 | 
			
		||||
 | 
			
		||||
export interface TableEditModeProps {
 | 
			
		||||
    table: DBTable;
 | 
			
		||||
@@ -108,6 +109,9 @@ export const TableEditMode: React.FC<TableEditModeProps> = React.memo(
 | 
			
		||||
            }
 | 
			
		||||
        }, [createField, table.id]);
 | 
			
		||||
 | 
			
		||||
        // Close edit mode when clicking outside
 | 
			
		||||
        useClickOutside(containerRef, onClose, isVisible);
 | 
			
		||||
 | 
			
		||||
        const handleColorChange = useCallback(
 | 
			
		||||
            (newColor: string) => {
 | 
			
		||||
                updateTable(table.id, { color: newColor });
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,8 @@ import type { DBRelationship } from '@/lib/domain/db-relationship';
 | 
			
		||||
import { useReactFlow } from '@xyflow/react';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useFocusOn } from '@/hooks/use-focus-on';
 | 
			
		||||
import { useClickAway, useKeyPressEvent } from 'react-use';
 | 
			
		||||
import { useEditClickOutside } from '@/hooks/use-click-outside';
 | 
			
		||||
import { useKeyPressEvent } from 'react-use';
 | 
			
		||||
import {
 | 
			
		||||
    DropdownMenu,
 | 
			
		||||
    DropdownMenuContent,
 | 
			
		||||
@@ -42,31 +43,37 @@ export const RelationshipListItemHeader: React.FC<
 | 
			
		||||
    const inputRef = React.useRef<HTMLInputElement>(null);
 | 
			
		||||
 | 
			
		||||
    const editRelationshipName = useCallback(() => {
 | 
			
		||||
        if (!editMode) return;
 | 
			
		||||
        if (relationshipName.trim() && relationshipName !== relationship.name) {
 | 
			
		||||
            updateRelationship(relationship.id, {
 | 
			
		||||
                name: relationshipName.trim(),
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setEditMode(false);
 | 
			
		||||
    }, [
 | 
			
		||||
        relationshipName,
 | 
			
		||||
        relationship.id,
 | 
			
		||||
        updateRelationship,
 | 
			
		||||
        editMode,
 | 
			
		||||
        relationship.name,
 | 
			
		||||
    ]);
 | 
			
		||||
 | 
			
		||||
    useClickAway(inputRef, editRelationshipName);
 | 
			
		||||
    useKeyPressEvent('Enter', editRelationshipName);
 | 
			
		||||
    const abortEdit = useCallback(() => {
 | 
			
		||||
        setEditMode(false);
 | 
			
		||||
        setRelationshipName(relationship.name);
 | 
			
		||||
    }, [relationship.name]);
 | 
			
		||||
 | 
			
		||||
    const enterEditMode = (
 | 
			
		||||
        event: React.MouseEvent<HTMLButtonElement, MouseEvent>
 | 
			
		||||
    ) => {
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
        setEditMode(true);
 | 
			
		||||
    };
 | 
			
		||||
    // Handle click outside to save and exit edit mode
 | 
			
		||||
    useEditClickOutside(inputRef, editMode, editRelationshipName);
 | 
			
		||||
    useKeyPressEvent('Enter', editRelationshipName);
 | 
			
		||||
    useKeyPressEvent('Escape', abortEdit);
 | 
			
		||||
 | 
			
		||||
    const enterEditMode = useCallback(
 | 
			
		||||
        (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
 | 
			
		||||
            event.stopPropagation();
 | 
			
		||||
            setRelationshipName(relationship.name);
 | 
			
		||||
            setEditMode(true);
 | 
			
		||||
        },
 | 
			
		||||
        [relationship.name]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const handleFocusOnRelationship = useCallback(
 | 
			
		||||
        (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,8 @@ import { ListItemHeaderButton } from '@/pages/editor-page/side-panel/list-item-h
 | 
			
		||||
import type { DBTable } from '@/lib/domain/db-table';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useClickAway, useKeyPressEvent } from 'react-use';
 | 
			
		||||
import { useEditClickOutside } from '@/hooks/use-click-outside';
 | 
			
		||||
import { useKeyPressEvent } from 'react-use';
 | 
			
		||||
import { useSortable } from '@dnd-kit/sortable';
 | 
			
		||||
import {
 | 
			
		||||
    DropdownMenu,
 | 
			
		||||
@@ -67,27 +68,30 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
 | 
			
		||||
    const { listeners } = useSortable({ id: table.id });
 | 
			
		||||
 | 
			
		||||
    const editTableName = useCallback(() => {
 | 
			
		||||
        if (!editMode) return;
 | 
			
		||||
        if (tableName.trim()) {
 | 
			
		||||
            updateTable(table.id, { name: tableName.trim() });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setEditMode(false);
 | 
			
		||||
    }, [tableName, table.id, updateTable, editMode]);
 | 
			
		||||
    }, [tableName, table.id, updateTable]);
 | 
			
		||||
 | 
			
		||||
    const abortEdit = useCallback(() => {
 | 
			
		||||
        setEditMode(false);
 | 
			
		||||
        setTableName(table.name);
 | 
			
		||||
    }, [table.name]);
 | 
			
		||||
 | 
			
		||||
    useClickAway(inputRef, editTableName);
 | 
			
		||||
    // Handle click outside to save and exit edit mode
 | 
			
		||||
    useEditClickOutside(inputRef, editMode, editTableName);
 | 
			
		||||
    useKeyPressEvent('Enter', editTableName);
 | 
			
		||||
    useKeyPressEvent('Escape', abortEdit);
 | 
			
		||||
 | 
			
		||||
    const enterEditMode = (e: React.MouseEvent) => {
 | 
			
		||||
        e.stopPropagation();
 | 
			
		||||
        setEditMode(true);
 | 
			
		||||
    };
 | 
			
		||||
    const enterEditMode = useCallback(
 | 
			
		||||
        (e: React.MouseEvent) => {
 | 
			
		||||
            e.stopPropagation();
 | 
			
		||||
            setTableName(table.name);
 | 
			
		||||
            setEditMode(true);
 | 
			
		||||
        },
 | 
			
		||||
        [table.name]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const handleFocusOnTable = useCallback(
 | 
			
		||||
        (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
 | 
			
		||||
@@ -249,6 +253,20 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
 | 
			
		||||
        }
 | 
			
		||||
    }, [table.name]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (editMode) {
 | 
			
		||||
            // Small delay to ensure the input is rendered
 | 
			
		||||
            const timeoutId = setTimeout(() => {
 | 
			
		||||
                if (inputRef.current) {
 | 
			
		||||
                    inputRef.current.focus();
 | 
			
		||||
                    inputRef.current.select();
 | 
			
		||||
                }
 | 
			
		||||
            }, 50);
 | 
			
		||||
 | 
			
		||||
            return () => clearTimeout(timeoutId);
 | 
			
		||||
        }
 | 
			
		||||
    }, [editMode]);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="group flex h-11 flex-1 items-center justify-between gap-1 overflow-hidden">
 | 
			
		||||
            {!readonly ? (
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
import React, { useCallback, useEffect, useState } from 'react';
 | 
			
		||||
import { useEditClickOutside } from '@/hooks/use-click-outside';
 | 
			
		||||
import { Button } from '@/components/button/button';
 | 
			
		||||
import { Check } from 'lucide-react';
 | 
			
		||||
import { Check, Pencil } from 'lucide-react';
 | 
			
		||||
import { Input } from '@/components/input/input';
 | 
			
		||||
import { useChartDB } from '@/hooks/use-chartdb';
 | 
			
		||||
import { useClickAway, useKeyPressEvent } from 'react-use';
 | 
			
		||||
import { useKeyPressEvent } from 'react-use';
 | 
			
		||||
import { DiagramIcon } from '@/components/diagram-icon/diagram-icon';
 | 
			
		||||
import { useTranslation } from 'react-i18next';
 | 
			
		||||
import { cn } from '@/lib/utils';
 | 
			
		||||
@@ -31,23 +32,45 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
 | 
			
		||||
        setEditedDiagramName(diagramName);
 | 
			
		||||
    }, [diagramName]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (editMode) {
 | 
			
		||||
            // Small delay to ensure the input is rendered
 | 
			
		||||
            const timeoutId = setTimeout(() => {
 | 
			
		||||
                if (inputRef.current) {
 | 
			
		||||
                    inputRef.current.focus();
 | 
			
		||||
                    inputRef.current.select();
 | 
			
		||||
                }
 | 
			
		||||
            }, 50); // Slightly longer delay to ensure DOM is ready
 | 
			
		||||
 | 
			
		||||
            return () => clearTimeout(timeoutId);
 | 
			
		||||
        }
 | 
			
		||||
    }, [editMode]);
 | 
			
		||||
 | 
			
		||||
    const editDiagramName = useCallback(() => {
 | 
			
		||||
        if (!editMode) return;
 | 
			
		||||
        if (editedDiagramName.trim()) {
 | 
			
		||||
            updateDiagramName(editedDiagramName.trim());
 | 
			
		||||
        }
 | 
			
		||||
        setEditMode(false);
 | 
			
		||||
    }, [editedDiagramName, updateDiagramName, editMode]);
 | 
			
		||||
    }, [editedDiagramName, updateDiagramName]);
 | 
			
		||||
 | 
			
		||||
    useClickAway(inputRef, editDiagramName);
 | 
			
		||||
    const abortEdit = useCallback(() => {
 | 
			
		||||
        setEditMode(false);
 | 
			
		||||
        setEditedDiagramName(diagramName);
 | 
			
		||||
    }, [diagramName]);
 | 
			
		||||
 | 
			
		||||
    // Handle click outside to save and exit edit mode
 | 
			
		||||
    useEditClickOutside(inputRef, editMode, editDiagramName);
 | 
			
		||||
    useKeyPressEvent('Enter', editDiagramName);
 | 
			
		||||
    useKeyPressEvent('Escape', abortEdit);
 | 
			
		||||
 | 
			
		||||
    const enterEditMode = (
 | 
			
		||||
        event: React.MouseEvent<HTMLHeadingElement, MouseEvent>
 | 
			
		||||
    ) => {
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
        setEditMode(true);
 | 
			
		||||
    };
 | 
			
		||||
    const enterEditMode = useCallback(
 | 
			
		||||
        (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
 | 
			
		||||
            event.stopPropagation();
 | 
			
		||||
            setEditedDiagramName(diagramName);
 | 
			
		||||
            setEditMode(true);
 | 
			
		||||
        },
 | 
			
		||||
        [diagramName]
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div className="group">
 | 
			
		||||
@@ -81,10 +104,16 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
 | 
			
		||||
                                    setEditedDiagramName(e.target.value)
 | 
			
		||||
                                }
 | 
			
		||||
                                className="ml-1 h-7 focus-visible:ring-0"
 | 
			
		||||
                                style={{
 | 
			
		||||
                                    width: `${Math.max(
 | 
			
		||||
                                        editedDiagramName.length * 8 + 20,
 | 
			
		||||
                                        100
 | 
			
		||||
                                    )}px`,
 | 
			
		||||
                                }}
 | 
			
		||||
                            />
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant="ghost"
 | 
			
		||||
                                className="flex size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300"
 | 
			
		||||
                                className="ml-1 flex size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300"
 | 
			
		||||
                                onClick={editDiagramName}
 | 
			
		||||
                            >
 | 
			
		||||
                                <Check />
 | 
			
		||||
@@ -110,6 +139,13 @@ export const DiagramName: React.FC<DiagramNameProps> = () => {
 | 
			
		||||
                                    {t('tool_tips.double_click_to_edit')}
 | 
			
		||||
                                </TooltipContent>
 | 
			
		||||
                            </Tooltip>
 | 
			
		||||
                            <Button
 | 
			
		||||
                                variant="ghost"
 | 
			
		||||
                                className="ml-1 size-5 p-0 opacity-0 transition-opacity hover:bg-primary-foreground group-hover:opacity-100"
 | 
			
		||||
                                onClick={enterEditMode}
 | 
			
		||||
                            >
 | 
			
		||||
                                <Pencil className="size-3 text-slate-500 dark:text-slate-400" />
 | 
			
		||||
                            </Button>
 | 
			
		||||
                        </>
 | 
			
		||||
                    )}
 | 
			
		||||
                </div>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user