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
121 lines
4.3 KiB
TypeScript
121 lines
4.3 KiB
TypeScript
import React, { useMemo } from 'react';
|
|
import { AlertCircle, Loader2 } from 'lucide-react';
|
|
import { TreeNode, TREE_ROW_HEIGHT } from './SftpPaneTreeNode';
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
type SftpPaneTreeRowsProps = Record<string, any>;
|
|
|
|
export function useSftpPaneTreeRows(props: SftpPaneTreeRowsProps) {
|
|
const {
|
|
nodeDescriptors, scrollTop, viewportHeight, tRef, columnTemplate, visibleColumns, selectedPaths, dragOverNodePath,
|
|
toggleExpand, handleNodeClick, stableOnOpenEntry, stableOnDragStart, stableOnDragEnd,
|
|
handleNodeDragOver, handleNodeDrop, handleNodeDragLeave, handleNodeContextMenu,
|
|
} = props;
|
|
|
|
const { totalHeight, visibleRange } = useMemo(() => {
|
|
const totalCount = nodeDescriptors.length;
|
|
const total = totalCount * TREE_ROW_HEIGHT;
|
|
const shouldVirtualize = viewportHeight > 0 && totalCount > 50;
|
|
|
|
if (!shouldVirtualize) {
|
|
return { totalHeight: 0, visibleRange: { start: 0, end: totalCount - 1, virtualized: false } };
|
|
}
|
|
|
|
const overscan = 6;
|
|
const start = Math.max(0, Math.floor(scrollTop / TREE_ROW_HEIGHT) - overscan);
|
|
const end = Math.min(totalCount - 1, Math.ceil((scrollTop + viewportHeight) / TREE_ROW_HEIGHT) + overscan);
|
|
return { totalHeight: total, visibleRange: { start, end, virtualized: true } };
|
|
}, [nodeDescriptors.length, scrollTop, viewportHeight]);
|
|
|
|
// ── Render visible rows ──────────────────────────────────────────
|
|
const treeRows = useMemo(() => {
|
|
const { start, end, virtualized } = visibleRange;
|
|
const rows: React.ReactNode[] = [];
|
|
|
|
for (let i = start; i <= end; i++) {
|
|
const descriptor = nodeDescriptors[i];
|
|
if (!descriptor) continue;
|
|
|
|
let content: React.ReactNode;
|
|
if (descriptor.type === 'loading') {
|
|
content = (
|
|
<div
|
|
style={{ paddingLeft: (descriptor.depth + 1) * 16 + 8, height: TREE_ROW_HEIGHT }}
|
|
className="text-xs text-muted-foreground flex items-center gap-1"
|
|
>
|
|
<Loader2 size={12} className="animate-spin" /> {tRef.current('sftp.tree.loading')}
|
|
</div>
|
|
);
|
|
} else if (descriptor.type === 'error') {
|
|
content = (
|
|
<div
|
|
style={{ paddingLeft: (descriptor.depth + 1) * 16 + 8, height: TREE_ROW_HEIGHT }}
|
|
className="text-xs text-destructive flex items-center gap-1"
|
|
>
|
|
<AlertCircle size={12} /> {tRef.current('sftp.tree.loadError')}
|
|
</div>
|
|
);
|
|
} else {
|
|
content = (
|
|
<TreeNode
|
|
entry={descriptor.entry}
|
|
entryPath={descriptor.entryPath}
|
|
depth={descriptor.depth}
|
|
columnTemplate={columnTemplate}
|
|
visibleColumns={visibleColumns}
|
|
isSelected={selectedPaths.has(descriptor.entryPath)}
|
|
isExpanded={descriptor.isExpanded}
|
|
isLoading={descriptor.isLoading}
|
|
isDragOver={dragOverNodePath === descriptor.entryPath}
|
|
onToggleExpand={toggleExpand}
|
|
onNodeClick={handleNodeClick}
|
|
onOpenEntry={stableOnOpenEntry}
|
|
onDragStart={stableOnDragStart}
|
|
onDragEnd={stableOnDragEnd}
|
|
onDragOverEntry={handleNodeDragOver}
|
|
onDropEntry={handleNodeDrop}
|
|
onDragLeaveEntry={handleNodeDragLeave}
|
|
onContextMenu={handleNodeContextMenu}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const key = descriptor.type === 'node' ? descriptor.entryPath : descriptor.key;
|
|
if (virtualized) {
|
|
rows.push(
|
|
<div
|
|
key={key}
|
|
className="absolute left-0 right-0"
|
|
style={{ top: i * TREE_ROW_HEIGHT, height: TREE_ROW_HEIGHT }}
|
|
>
|
|
{content}
|
|
</div>,
|
|
);
|
|
} else {
|
|
rows.push(<React.Fragment key={key}>{content}</React.Fragment>);
|
|
}
|
|
}
|
|
|
|
return rows;
|
|
}, [
|
|
visibleRange,
|
|
nodeDescriptors,
|
|
columnTemplate,
|
|
visibleColumns,
|
|
selectedPaths,
|
|
dragOverNodePath,
|
|
toggleExpand,
|
|
handleNodeClick,
|
|
stableOnOpenEntry,
|
|
stableOnDragStart,
|
|
stableOnDragEnd,
|
|
handleNodeDragOver,
|
|
handleNodeDrop,
|
|
handleNodeDragLeave,
|
|
handleNodeContextMenu,
|
|
tRef,
|
|
]);
|
|
|
|
return { totalHeight, treeRows, visibleRange };
|
|
}
|