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:
陈大猫
2026-06-10 14:38:33 +08:00
committed by GitHub
parent c9d84c7ce3
commit 068730c53c
3 changed files with 74 additions and 10 deletions

View File

@@ -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">

View File

@@ -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}

View File

@@ -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}