fix(ui): improve host tree inline group rename interactions (#1367)
* fix(ui): improve host tree inline group rename interactions Cancel rename when clicking another tree row and prevent parent drag from blocking text selection in the rename input. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): block group toggle keyboard while inline renaming Co-authored-by: Cursor <cursoragent@cursor.com> * fix(ui): cancel host inline rename when clicking other tree rows Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { CheckSquare, ChevronRight, Edit2, FileSymlink, Folder, FolderOpen, Server, Square, Expand, Minimize2 } from 'lucide-react';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useI18n } from '../application/i18n/I18nProvider';
|
||||
import {
|
||||
hostTreeInlineGroupEditStore,
|
||||
@@ -171,7 +171,13 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
||||
return (
|
||||
<div>
|
||||
{/* Group Node */}
|
||||
<Collapsible open={isExpanded} onOpenChange={() => onToggle(node.path)}>
|
||||
<Collapsible
|
||||
open={isExpanded}
|
||||
onOpenChange={() => {
|
||||
if (isInlineEditing) return;
|
||||
onToggle(node.path);
|
||||
}}
|
||||
>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>
|
||||
<CollapsibleTrigger asChild>
|
||||
@@ -182,8 +188,14 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
||||
getDropTargetClasses?.(node.path),
|
||||
)}
|
||||
style={{ paddingLeft }}
|
||||
draggable
|
||||
onDragStart={(e) => e.dataTransfer.setData("group-path", node.path)}
|
||||
data-section="host-tree-row"
|
||||
data-row-type="group"
|
||||
data-group-path={node.path}
|
||||
draggable={!isInlineEditing}
|
||||
onDragStart={(e) => {
|
||||
if (isInlineEditing) return;
|
||||
e.dataTransfer.setData("group-path", node.path);
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -394,6 +406,9 @@ const HostTreeItem: React.FC<HostTreeItemProps> = ({
|
||||
isSelected ? "bg-primary/10" : "",
|
||||
)}
|
||||
style={{ paddingLeft }}
|
||||
data-section="host-tree-row"
|
||||
data-row-type="host"
|
||||
data-host-id={host.id}
|
||||
draggable={!isMultiSelectMode}
|
||||
onDragStart={(e) => e.dataTransfer.setData("host-id", host.id)}
|
||||
onClick={() => {
|
||||
@@ -496,6 +511,20 @@ export const HostTreeView: React.FC<HostTreeViewProps> = ({
|
||||
groupConfigs = [],
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const inlineEdit = useHostTreeInlineGroupEdit();
|
||||
const vaultTreeActions = useVaultHostTreeActions();
|
||||
const cancelRename = cancelInlineGroupEdit ?? vaultTreeActions?.cancelInlineGroupEdit;
|
||||
|
||||
const handleTreePointerDownCapture = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (!inlineEdit?.groupPath || !cancelRename) return;
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) return;
|
||||
if (target.closest('[data-inline-group-edit="true"]')) return;
|
||||
const row = target.closest('[data-section="host-tree-row"]');
|
||||
if (!row) return;
|
||||
if (row.getAttribute('data-group-path') === inlineEdit.groupPath) return;
|
||||
cancelRename();
|
||||
}, [cancelRename, inlineEdit?.groupPath]);
|
||||
|
||||
// Use external state if provided, otherwise use local persistent state
|
||||
const localTreeState = useTreeExpandedState(STORAGE_KEY_VAULT_HOSTS_TREE_EXPANDED);
|
||||
@@ -567,7 +596,7 @@ export const HostTreeView: React.FC<HostTreeViewProps> = ({
|
||||
}, [groupTree, sortMode]);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-1" onPointerDownCapture={handleTreePointerDownCapture}>
|
||||
{/* Expand/Collapse controls */}
|
||||
{groupTree.length > 0 && (
|
||||
<div className="flex items-center gap-2 mb-3 pb-2 border-b border-border/30">
|
||||
|
||||
@@ -43,11 +43,23 @@ export const HostTreeGroupInlineRenameInput: React.FC<HostTreeGroupInlineRenameI
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
data-inline-group-edit="true"
|
||||
value={value}
|
||||
draggable={false}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={commit}
|
||||
onBlur={() => {
|
||||
queueMicrotask(() => {
|
||||
commit();
|
||||
});
|
||||
}}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onDoubleClick={(event) => event.stopPropagation()}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onDragStart={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation();
|
||||
if (event.key === 'Enter') {
|
||||
@@ -60,7 +72,7 @@ export const HostTreeGroupInlineRenameInput: React.FC<HostTreeGroupInlineRenameI
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'min-w-0 flex-1 truncate rounded-sm border border-primary/50 bg-background/80 px-1 py-0 text-sm font-medium outline-none ring-1 ring-primary/30',
|
||||
'min-w-0 flex-1 truncate select-text rounded-sm border border-primary/50 bg-background/80 px-1 py-0 text-sm font-medium outline-none ring-1 ring-primary/30',
|
||||
className,
|
||||
)}
|
||||
style={style}
|
||||
|
||||
@@ -420,9 +420,9 @@ const HostTreeFlatRowItem = memo<HostTreeFlatRowProps>(({
|
||||
color: theme.termFg,
|
||||
backgroundColor: isDragOver ? theme.rowDropBg : undefined,
|
||||
}}
|
||||
draggable={canDrag}
|
||||
draggable={canDrag && !isInlineEditing}
|
||||
onDragStart={(event) => {
|
||||
if (!canDrag) return;
|
||||
if (!canDrag || isInlineEditing) return;
|
||||
event.dataTransfer.setData(HOST_TREE_DRAG_GROUP_PATH, node.path);
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
}}
|
||||
@@ -445,8 +445,12 @@ const HostTreeFlatRowItem = memo<HostTreeFlatRowProps>(({
|
||||
onMouseLeave={(event) => {
|
||||
if (!isDragOver) event.currentTarget.style.backgroundColor = '';
|
||||
}}
|
||||
onClick={() => onTogglePath(node.path)}
|
||||
onClick={() => {
|
||||
if (isInlineEditing) return;
|
||||
onTogglePath(node.path);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (isInlineEditing) return;
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
onTogglePath(node.path);
|
||||
@@ -711,6 +715,24 @@ const TerminalHostTreeSidebarInner: React.FC<TerminalHostTreeSidebarProps> = ({
|
||||
handleDropToParent(null, event.dataTransfer);
|
||||
}, [canDrag, handleDropToParent]);
|
||||
|
||||
const handleListPointerDownCapture = useCallback((event: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (!menuActions) return;
|
||||
const editingGroupPath = inlineEdit?.groupPath;
|
||||
const editingHostId = inlineHostEdit?.hostId;
|
||||
if (!editingGroupPath && !editingHostId) return;
|
||||
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element)) return;
|
||||
if (target.closest('[data-inline-group-edit="true"]')) return;
|
||||
const row = target.closest('[data-section="terminal-host-tree-sidebar-row"]');
|
||||
if (!row) return;
|
||||
if (editingGroupPath && row.getAttribute('data-group-path') === editingGroupPath) return;
|
||||
if (editingHostId && row.getAttribute('data-host-id') === editingHostId) return;
|
||||
|
||||
if (editingGroupPath) menuActions.cancelInlineGroupEdit();
|
||||
if (editingHostId) menuActions.cancelInlineHostEdit();
|
||||
}, [inlineEdit?.groupPath, inlineHostEdit?.hostId, menuActions]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inlineEdit?.shouldScrollIntoView || !inlineEdit.isNew) return;
|
||||
const index = flatRows.findIndex(
|
||||
@@ -971,6 +993,7 @@ const TerminalHostTreeSidebarInner: React.FC<TerminalHostTreeSidebarProps> = ({
|
||||
className="flex-1 min-h-0 py-1"
|
||||
data-section="terminal-host-tree-sidebar-content"
|
||||
style={dragOverTarget?.kind === 'root' ? { backgroundColor: theme.rowDropBg } : undefined}
|
||||
onPointerDownCapture={handleListPointerDownCapture}
|
||||
onDragOver={handleRootDragOver}
|
||||
onDragLeave={handleRootDragLeave}
|
||||
onDrop={handleRootDrop}
|
||||
|
||||
Reference in New Issue
Block a user