mirror of
https://github.com/chartdb/chartdb.git
synced 2025-10-29 02:53:56 +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,89 +53,119 @@ 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) => {
|
||||||
setEditMode(true);
|
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 (
|
return (
|
||||||
<div
|
<AreaNodeContextMenu area={area} onEditName={enterEditMode}>
|
||||||
className={cn(
|
<div
|
||||||
'flex h-full flex-col rounded-md border-2 shadow-sm',
|
className={cn(
|
||||||
selected ? 'border-pink-600' : 'border-transparent'
|
'flex h-full flex-col rounded-md border-2 shadow-sm',
|
||||||
)}
|
selected ? 'border-pink-600' : 'border-transparent'
|
||||||
style={{
|
)}
|
||||||
backgroundColor: `${area.color}15`,
|
style={{
|
||||||
borderColor: selected ? undefined : area.color,
|
backgroundColor: `${area.color}15`,
|
||||||
}}
|
borderColor: selected ? undefined : area.color,
|
||||||
onClick={(e) => {
|
}}
|
||||||
if (e.detail === 2) {
|
onClick={(e) => {
|
||||||
openAreaInEditor();
|
if (e.detail === 2) {
|
||||||
}
|
openAreaInEditor();
|
||||||
}}
|
}
|
||||||
>
|
}}
|
||||||
{!readonly ? (
|
>
|
||||||
<NodeResizer
|
{!readonly ? (
|
||||||
isVisible={focused}
|
<NodeResizer
|
||||||
lineClassName="!border-4 !border-transparent"
|
isVisible={focused}
|
||||||
handleClassName="!h-[10px] !w-[10px] !rounded-full !bg-pink-600"
|
lineClassName="!border-4 !border-transparent"
|
||||||
minHeight={100}
|
handleClassName="!h-[10px] !w-[10px] !rounded-full !bg-pink-600"
|
||||||
minWidth={100}
|
minHeight={100}
|
||||||
/>
|
minWidth={100}
|
||||||
) : null}
|
/>
|
||||||
<div className="group flex h-8 items-center justify-between rounded-t-md px-2">
|
) : null}
|
||||||
<div className="flex w-full items-center gap-1">
|
<div className="group flex h-8 items-center justify-between rounded-t-md px-2">
|
||||||
<GripVertical className="size-4 shrink-0 text-slate-700 opacity-60 dark:text-slate-300" />
|
<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 ? (
|
{editMode && !readonly ? (
|
||||||
<div className="flex w-full items-center">
|
<div className="flex w-full items-center">
|
||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
autoFocus
|
autoFocus
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={area.name}
|
placeholder={area.name}
|
||||||
value={areaName}
|
value={areaName}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setAreaName(e.target.value)
|
setAreaName(e.target.value)
|
||||||
}
|
}
|
||||||
className="h-6 bg-white/70 focus-visible:ring-0 dark:bg-slate-900/70"
|
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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="ml-1 size-6 p-0 hover:bg-white/20"
|
className="ml-auto size-5 p-0 opacity-0 transition-opacity hover:bg-white/20 group-hover:opacity-100"
|
||||||
onClick={editAreaName}
|
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>
|
</Button>
|
||||||
</div>
|
)}
|
||||||
) : !readonly ? (
|
</div>
|
||||||
<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>
|
||||||
<div className="flex-1" />
|
</AreaNodeContextMenu>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
event.stopPropagation();
|
useKeyPressEvent('Escape', abortEdit);
|
||||||
setEditMode(true);
|
|
||||||
};
|
const enterEditMode = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setRelationshipName(relationship.name);
|
||||||
|
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.stopPropagation();
|
(e: React.MouseEvent) => {
|
||||||
setEditMode(true);
|
e.stopPropagation();
|
||||||
};
|
setTableName(table.name);
|
||||||
|
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