[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
130
components/sftp/SftpPaneTreeNode.tsx
Normal file
130
components/sftp/SftpPaneTreeNode.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React from 'react';
|
||||
import { ChevronRight, CornerUpLeft, Folder, FolderOpen, Loader2 } from 'lucide-react';
|
||||
import type { SftpFileEntry } from '../../types';
|
||||
import { formatBytes, formatDate, getFileIcon, isNavigableDirectory, type SftpColumnVisibility } from './utils';
|
||||
import { useI18n } from '../../application/i18n/I18nProvider';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
export type NodeDescriptor =
|
||||
| { type: 'node'; entry: SftpFileEntry; entryPath: string; depth: number; isExpanded: boolean; isLoading: boolean }
|
||||
| { type: 'loading' | 'error'; key: string; depth: number };
|
||||
|
||||
// ── Simplified TreeNode (no per-node ContextMenu) ────────────────────
|
||||
|
||||
interface TreeNodeProps {
|
||||
entry: SftpFileEntry;
|
||||
entryPath: string;
|
||||
depth: number;
|
||||
columnTemplate: string;
|
||||
visibleColumns: SftpColumnVisibility;
|
||||
isSelected: boolean;
|
||||
isExpanded: boolean;
|
||||
isLoading: boolean;
|
||||
isDragOver: boolean;
|
||||
onToggleExpand: (entry: SftpFileEntry, entryPath: string) => void;
|
||||
onNodeClick: (entry: SftpFileEntry, entryPath: string, e: React.MouseEvent) => void;
|
||||
onOpenEntry: (entry: SftpFileEntry, entryPath: string) => void;
|
||||
onDragStart: (entry: SftpFileEntry, entryPath: string, isDir: boolean, e: React.DragEvent) => void;
|
||||
onDragEnd: () => void;
|
||||
onDragOverEntry: (entryPath: string, e: React.DragEvent) => void;
|
||||
onDropEntry: (entryPath: string, e: React.DragEvent) => void;
|
||||
onDragLeaveEntry: () => void;
|
||||
onContextMenu: (entry: SftpFileEntry, entryPath: string, e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
export const TREE_ROW_HEIGHT = 28;
|
||||
|
||||
export const TreeNode = React.memo<TreeNodeProps>(({
|
||||
entry, entryPath, depth, columnTemplate, visibleColumns, isSelected,
|
||||
isExpanded, isLoading, isDragOver,
|
||||
onToggleExpand, onNodeClick, onOpenEntry, onDragStart, onDragEnd,
|
||||
onDragOverEntry, onDropEntry, onDragLeaveEntry,
|
||||
onContextMenu,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const isParentEntry = entry.name === '..';
|
||||
const isDir = isNavigableDirectory(entry);
|
||||
const icon = isDir
|
||||
? (isExpanded
|
||||
? <FolderOpen size={14} className="shrink-0 text-yellow-500" />
|
||||
: <Folder size={14} className="shrink-0 text-yellow-500" />)
|
||||
: getFileIcon(entry);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-section="terminal-sftp-tree-row"
|
||||
data-entry-name={entry.name}
|
||||
data-entry-type={isDir ? 'directory' : entry.type}
|
||||
data-selected={isSelected ? 'true' : 'false'}
|
||||
data-expanded={isDir ? (isExpanded ? 'true' : 'false') : undefined}
|
||||
data-drag-over={isDragOver ? 'true' : 'false'}
|
||||
className={cn(
|
||||
'grid items-center gap-x-1 px-2 cursor-pointer select-none text-sm',
|
||||
isSelected
|
||||
? 'bg-accent text-accent-foreground hover:bg-accent'
|
||||
: 'hover:bg-accent/50',
|
||||
isDragOver && 'ring-2 ring-primary/50 ring-inset bg-primary/10',
|
||||
)}
|
||||
style={{ gridTemplateColumns: columnTemplate, height: TREE_ROW_HEIGHT }}
|
||||
onClick={e => onNodeClick(entry, entryPath, e)}
|
||||
onDoubleClick={() => onOpenEntry(entry, entryPath)}
|
||||
onContextMenu={e => {
|
||||
if (!isParentEntry) {
|
||||
onContextMenu(entry, entryPath, e);
|
||||
}
|
||||
}}
|
||||
draggable={!isParentEntry}
|
||||
onDragStart={e => { if (!isParentEntry) onDragStart(entry, entryPath, isDir, e); }}
|
||||
onDragEnd={onDragEnd}
|
||||
onDragOver={e => onDragOverEntry(entryPath, e)}
|
||||
onDrop={e => onDropEntry(entryPath, e)}
|
||||
onDragLeave={onDragLeaveEntry}
|
||||
>
|
||||
<div
|
||||
className="flex min-w-0 items-center gap-1"
|
||||
style={{ paddingLeft: depth * 16 + 8 }}
|
||||
>
|
||||
<span className="shrink-0 w-4 flex items-center justify-center">
|
||||
{isParentEntry ? (
|
||||
<CornerUpLeft size={14} className="text-muted-foreground" />
|
||||
) : isDir ? (
|
||||
isLoading ? (
|
||||
<Loader2 size={12} className="animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight
|
||||
size={14}
|
||||
className={cn('transition-transform text-muted-foreground', isExpanded && 'rotate-90')}
|
||||
onClick={e => { e.stopPropagation(); void onToggleExpand(entry, entryPath); }}
|
||||
/>
|
||||
)
|
||||
) : null}
|
||||
</span>
|
||||
{!isParentEntry && <span className="shrink-0">{icon}</span>}
|
||||
<span className="min-w-0 flex-1 truncate">{entry.name}</span>
|
||||
</div>
|
||||
{visibleColumns.modified && (
|
||||
<span className="min-w-0 text-muted-foreground text-xs truncate">
|
||||
{isParentEntry ? '' : formatDate(entry.lastModified)}
|
||||
</span>
|
||||
)}
|
||||
{visibleColumns.size && (
|
||||
<span className="min-w-0 text-right text-muted-foreground text-xs truncate">
|
||||
{isParentEntry ? '' : (isDir ? '--' : formatBytes(entry.size ?? 0))}
|
||||
</span>
|
||||
)}
|
||||
{visibleColumns.type && (
|
||||
<span className="min-w-0 text-right text-muted-foreground text-xs truncate">
|
||||
{isParentEntry ? '' : (isDir ? t('sftp.kind.folder') : (entry.name.split('.').pop()?.toUpperCase() ?? '--'))}
|
||||
</span>
|
||||
)}
|
||||
{visibleColumns.owner && (
|
||||
<span className="min-w-0 text-right text-muted-foreground text-xs truncate">
|
||||
{isParentEntry ? '' : (entry.owner || '--')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
TreeNode.displayName = 'TreeNode';
|
||||
|
||||
// ── Tree paths reducer (unchanged) ──────────────────────────────────
|
||||
Reference in New Issue
Block a user