mirror of
				https://github.com/chartdb/chartdb.git
				synced 2025-10-30 19:43:54 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			jf/add_rea
			...
			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 type { NodeProps, Node } from '@xyflow/react'; | ||||||
| import { NodeResizer } from '@xyflow/react'; | import { NodeResizer } from '@xyflow/react'; | ||||||
| import type { Area } from '@/lib/domain/area'; | import type { Area } from '@/lib/domain/area'; | ||||||
| import { useChartDB } from '@/hooks/use-chartdb'; | import { useChartDB } from '@/hooks/use-chartdb'; | ||||||
| import { Input } from '@/components/input/input'; | 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 { | import { | ||||||
|     Tooltip, |     Tooltip, | ||||||
|     TooltipContent, |     TooltipContent, | ||||||
| @@ -12,9 +13,10 @@ import { | |||||||
| } from '@/components/tooltip/tooltip'; | } from '@/components/tooltip/tooltip'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { cn } from '@/lib/utils'; | 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 { Button } from '@/components/button/button'; | ||||||
| import { useLayout } from '@/hooks/use-layout'; | import { useLayout } from '@/hooks/use-layout'; | ||||||
|  | import { AreaNodeContextMenu } from './area-node-context-menu'; | ||||||
|  |  | ||||||
| export type AreaNodeType = Node< | export type AreaNodeType = Node< | ||||||
|     { |     { | ||||||
| @@ -35,12 +37,11 @@ export const AreaNode: React.FC<NodeProps<AreaNodeType>> = React.memo( | |||||||
|         const focused = !!selected && !dragging; |         const focused = !!selected && !dragging; | ||||||
|  |  | ||||||
|         const editAreaName = useCallback(() => { |         const editAreaName = useCallback(() => { | ||||||
|             if (!editMode) return; |  | ||||||
|             if (areaName.trim()) { |             if (areaName.trim()) { | ||||||
|                 updateArea(area.id, { name: areaName.trim() }); |                 updateArea(area.id, { name: areaName.trim() }); | ||||||
|             } |             } | ||||||
|             setEditMode(false); |             setEditMode(false); | ||||||
|         }, [areaName, area.id, updateArea, editMode]); |         }, [areaName, area.id, updateArea]); | ||||||
|  |  | ||||||
|         const abortEdit = useCallback(() => { |         const abortEdit = useCallback(() => { | ||||||
|             setEditMode(false); |             setEditMode(false); | ||||||
| @@ -52,16 +53,36 @@ export const AreaNode: React.FC<NodeProps<AreaNodeType>> = React.memo( | |||||||
|             openAreaFromSidebar(area.id); |             openAreaFromSidebar(area.id); | ||||||
|         }, [selectSidebarSection, 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('Enter', editAreaName); | ||||||
|         useKeyPressEvent('Escape', abortEdit); |         useKeyPressEvent('Escape', abortEdit); | ||||||
|  |  | ||||||
|         const enterEditMode = (e: React.MouseEvent) => { |         const enterEditMode = useCallback( | ||||||
|             e.stopPropagation(); |             (e?: React.MouseEvent) => { | ||||||
|  |                 e?.stopPropagation(); | ||||||
|  |                 setAreaName(area.name); | ||||||
|                 setEditMode(true); |                 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 ( |         return ( | ||||||
|  |             <AreaNodeContextMenu area={area} onEditName={enterEditMode}> | ||||||
|                 <div |                 <div | ||||||
|                     className={cn( |                     className={cn( | ||||||
|                         'flex h-full flex-col rounded-md border-2 shadow-sm', |                         'flex h-full flex-col rounded-md border-2 shadow-sm', | ||||||
| @@ -116,7 +137,7 @@ export const AreaNode: React.FC<NodeProps<AreaNodeType>> = React.memo( | |||||||
|                                 <Tooltip> |                                 <Tooltip> | ||||||
|                                     <TooltipTrigger asChild> |                                     <TooltipTrigger asChild> | ||||||
|                                         <div |                                         <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" |                                             className="text-editable truncate px-1 py-0.5 text-base font-semibold text-slate-700 dark:text-slate-300" | ||||||
|                                             onDoubleClick={enterEditMode} |                                             onDoubleClick={enterEditMode} | ||||||
|                                         > |                                         > | ||||||
|                                             {area.name} |                                             {area.name} | ||||||
| @@ -131,10 +152,20 @@ export const AreaNode: React.FC<NodeProps<AreaNodeType>> = React.memo( | |||||||
|                                     {area.name} |                                     {area.name} | ||||||
|                                 </div> |                                 </div> | ||||||
|                             )} |                             )} | ||||||
|  |                             {!editMode && !readonly && ( | ||||||
|  |                                 <Button | ||||||
|  |                                     variant="ghost" | ||||||
|  |                                     className="ml-auto size-5 p-0 opacity-0 transition-opacity hover:bg-white/20 group-hover:opacity-100" | ||||||
|  |                                     onClick={enterEditMode} | ||||||
|  |                                 > | ||||||
|  |                                     <Pencil className="size-3 text-slate-700 dark:text-slate-300" /> | ||||||
|  |                                 </Button> | ||||||
|  |                             )} | ||||||
|                         </div> |                         </div> | ||||||
|                     </div> |                     </div> | ||||||
|                     <div className="flex-1" /> |                     <div className="flex-1" /> | ||||||
|                 </div> |                 </div> | ||||||
|  |             </AreaNodeContextMenu> | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
| ); | ); | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ import { Separator } from '@/components/separator/separator'; | |||||||
| import { useChartDB } from '@/hooks/use-chartdb'; | import { useChartDB } from '@/hooks/use-chartdb'; | ||||||
| import { useUpdateTable } from '@/hooks/use-update-table'; | import { useUpdateTable } from '@/hooks/use-update-table'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
|  | import { useClickOutside } from '@/hooks/use-click-outside'; | ||||||
|  |  | ||||||
| export interface TableEditModeProps { | export interface TableEditModeProps { | ||||||
|     table: DBTable; |     table: DBTable; | ||||||
| @@ -108,6 +109,9 @@ export const TableEditMode: React.FC<TableEditModeProps> = React.memo( | |||||||
|             } |             } | ||||||
|         }, [createField, table.id]); |         }, [createField, table.id]); | ||||||
|  |  | ||||||
|  |         // Close edit mode when clicking outside | ||||||
|  |         useClickOutside(containerRef, onClose, isVisible); | ||||||
|  |  | ||||||
|         const handleColorChange = useCallback( |         const handleColorChange = useCallback( | ||||||
|             (newColor: string) => { |             (newColor: string) => { | ||||||
|                 updateTable(table.id, { color: newColor }); |                 updateTable(table.id, { color: newColor }); | ||||||
|   | |||||||
| @@ -11,7 +11,8 @@ import type { DBRelationship } from '@/lib/domain/db-relationship'; | |||||||
| import { useReactFlow } from '@xyflow/react'; | import { useReactFlow } from '@xyflow/react'; | ||||||
| import { useChartDB } from '@/hooks/use-chartdb'; | import { useChartDB } from '@/hooks/use-chartdb'; | ||||||
| import { useFocusOn } from '@/hooks/use-focus-on'; | 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 { | import { | ||||||
|     DropdownMenu, |     DropdownMenu, | ||||||
|     DropdownMenuContent, |     DropdownMenuContent, | ||||||
| @@ -42,31 +43,37 @@ export const RelationshipListItemHeader: React.FC< | |||||||
|     const inputRef = React.useRef<HTMLInputElement>(null); |     const inputRef = React.useRef<HTMLInputElement>(null); | ||||||
|  |  | ||||||
|     const editRelationshipName = useCallback(() => { |     const editRelationshipName = useCallback(() => { | ||||||
|         if (!editMode) return; |  | ||||||
|         if (relationshipName.trim() && relationshipName !== relationship.name) { |         if (relationshipName.trim() && relationshipName !== relationship.name) { | ||||||
|             updateRelationship(relationship.id, { |             updateRelationship(relationship.id, { | ||||||
|                 name: relationshipName.trim(), |                 name: relationshipName.trim(), | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         setEditMode(false); |         setEditMode(false); | ||||||
|     }, [ |     }, [ | ||||||
|         relationshipName, |         relationshipName, | ||||||
|         relationship.id, |         relationship.id, | ||||||
|         updateRelationship, |         updateRelationship, | ||||||
|         editMode, |  | ||||||
|         relationship.name, |         relationship.name, | ||||||
|     ]); |     ]); | ||||||
|  |  | ||||||
|     useClickAway(inputRef, editRelationshipName); |     const abortEdit = useCallback(() => { | ||||||
|     useKeyPressEvent('Enter', editRelationshipName); |         setEditMode(false); | ||||||
|  |         setRelationshipName(relationship.name); | ||||||
|  |     }, [relationship.name]); | ||||||
|  |  | ||||||
|     const enterEditMode = ( |     // Handle click outside to save and exit edit mode | ||||||
|         event: React.MouseEvent<HTMLButtonElement, MouseEvent> |     useEditClickOutside(inputRef, editMode, editRelationshipName); | ||||||
|     ) => { |     useKeyPressEvent('Enter', editRelationshipName); | ||||||
|  |     useKeyPressEvent('Escape', abortEdit); | ||||||
|  |  | ||||||
|  |     const enterEditMode = useCallback( | ||||||
|  |         (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { | ||||||
|             event.stopPropagation(); |             event.stopPropagation(); | ||||||
|  |             setRelationshipName(relationship.name); | ||||||
|             setEditMode(true); |             setEditMode(true); | ||||||
|     }; |         }, | ||||||
|  |         [relationship.name] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     const handleFocusOnRelationship = useCallback( |     const handleFocusOnRelationship = useCallback( | ||||||
|         (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { |         (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 type { DBTable } from '@/lib/domain/db-table'; | ||||||
| import { Input } from '@/components/input/input'; | import { Input } from '@/components/input/input'; | ||||||
| import { useChartDB } from '@/hooks/use-chartdb'; | 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 { useSortable } from '@dnd-kit/sortable'; | ||||||
| import { | import { | ||||||
|     DropdownMenu, |     DropdownMenu, | ||||||
| @@ -67,27 +68,30 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({ | |||||||
|     const { listeners } = useSortable({ id: table.id }); |     const { listeners } = useSortable({ id: table.id }); | ||||||
|  |  | ||||||
|     const editTableName = useCallback(() => { |     const editTableName = useCallback(() => { | ||||||
|         if (!editMode) return; |  | ||||||
|         if (tableName.trim()) { |         if (tableName.trim()) { | ||||||
|             updateTable(table.id, { name: tableName.trim() }); |             updateTable(table.id, { name: tableName.trim() }); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         setEditMode(false); |         setEditMode(false); | ||||||
|     }, [tableName, table.id, updateTable, editMode]); |     }, [tableName, table.id, updateTable]); | ||||||
|  |  | ||||||
|     const abortEdit = useCallback(() => { |     const abortEdit = useCallback(() => { | ||||||
|         setEditMode(false); |         setEditMode(false); | ||||||
|         setTableName(table.name); |         setTableName(table.name); | ||||||
|     }, [table.name]); |     }, [table.name]); | ||||||
|  |  | ||||||
|     useClickAway(inputRef, editTableName); |     // Handle click outside to save and exit edit mode | ||||||
|  |     useEditClickOutside(inputRef, editMode, editTableName); | ||||||
|     useKeyPressEvent('Enter', editTableName); |     useKeyPressEvent('Enter', editTableName); | ||||||
|     useKeyPressEvent('Escape', abortEdit); |     useKeyPressEvent('Escape', abortEdit); | ||||||
|  |  | ||||||
|     const enterEditMode = (e: React.MouseEvent) => { |     const enterEditMode = useCallback( | ||||||
|  |         (e: React.MouseEvent) => { | ||||||
|             e.stopPropagation(); |             e.stopPropagation(); | ||||||
|  |             setTableName(table.name); | ||||||
|             setEditMode(true); |             setEditMode(true); | ||||||
|     }; |         }, | ||||||
|  |         [table.name] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     const handleFocusOnTable = useCallback( |     const handleFocusOnTable = useCallback( | ||||||
|         (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { |         (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { | ||||||
| @@ -249,6 +253,20 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({ | |||||||
|         } |         } | ||||||
|     }, [table.name]); |     }, [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 ( |     return ( | ||||||
|         <div className="group flex h-11 flex-1 items-center justify-between gap-1 overflow-hidden"> |         <div className="group flex h-11 flex-1 items-center justify-between gap-1 overflow-hidden"> | ||||||
|             {!readonly ? ( |             {!readonly ? ( | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
| import React, { useCallback, useEffect, useState } from 'react'; | import React, { useCallback, useEffect, useState } from 'react'; | ||||||
|  | import { useEditClickOutside } from '@/hooks/use-click-outside'; | ||||||
| import { Button } from '@/components/button/button'; | import { Button } from '@/components/button/button'; | ||||||
| import { Check } from 'lucide-react'; | import { Check, Pencil } from 'lucide-react'; | ||||||
| import { Input } from '@/components/input/input'; | import { Input } from '@/components/input/input'; | ||||||
| import { useChartDB } from '@/hooks/use-chartdb'; | 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 { DiagramIcon } from '@/components/diagram-icon/diagram-icon'; | ||||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||||
| import { cn } from '@/lib/utils'; | import { cn } from '@/lib/utils'; | ||||||
| @@ -31,23 +32,45 @@ export const DiagramName: React.FC<DiagramNameProps> = () => { | |||||||
|         setEditedDiagramName(diagramName); |         setEditedDiagramName(diagramName); | ||||||
|     }, [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(() => { |     const editDiagramName = useCallback(() => { | ||||||
|         if (!editMode) return; |  | ||||||
|         if (editedDiagramName.trim()) { |         if (editedDiagramName.trim()) { | ||||||
|             updateDiagramName(editedDiagramName.trim()); |             updateDiagramName(editedDiagramName.trim()); | ||||||
|         } |         } | ||||||
|         setEditMode(false); |         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('Enter', editDiagramName); | ||||||
|  |     useKeyPressEvent('Escape', abortEdit); | ||||||
|  |  | ||||||
|     const enterEditMode = ( |     const enterEditMode = useCallback( | ||||||
|         event: React.MouseEvent<HTMLHeadingElement, MouseEvent> |         (event: React.MouseEvent<HTMLElement, MouseEvent>) => { | ||||||
|     ) => { |  | ||||||
|             event.stopPropagation(); |             event.stopPropagation(); | ||||||
|  |             setEditedDiagramName(diagramName); | ||||||
|             setEditMode(true); |             setEditMode(true); | ||||||
|     }; |         }, | ||||||
|  |         [diagramName] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return ( |     return ( | ||||||
|         <div className="group"> |         <div className="group"> | ||||||
| @@ -81,10 +104,16 @@ export const DiagramName: React.FC<DiagramNameProps> = () => { | |||||||
|                                     setEditedDiagramName(e.target.value) |                                     setEditedDiagramName(e.target.value) | ||||||
|                                 } |                                 } | ||||||
|                                 className="ml-1 h-7 focus-visible:ring-0" |                                 className="ml-1 h-7 focus-visible:ring-0" | ||||||
|  |                                 style={{ | ||||||
|  |                                     width: `${Math.max( | ||||||
|  |                                         editedDiagramName.length * 8 + 20, | ||||||
|  |                                         100 | ||||||
|  |                                     )}px`, | ||||||
|  |                                 }} | ||||||
|                             /> |                             /> | ||||||
|                             <Button |                             <Button | ||||||
|                                 variant="ghost" |                                 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} |                                 onClick={editDiagramName} | ||||||
|                             > |                             > | ||||||
|                                 <Check /> |                                 <Check /> | ||||||
| @@ -110,6 +139,13 @@ export const DiagramName: React.FC<DiagramNameProps> = () => { | |||||||
|                                     {t('tool_tips.double_click_to_edit')} |                                     {t('tool_tips.double_click_to_edit')} | ||||||
|                                 </TooltipContent> |                                 </TooltipContent> | ||||||
|                             </Tooltip> |                             </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> |                 </div> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user