[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:
476
components/sftp/SftpTransferQueue.tsx
Normal file
476
components/sftp/SftpTransferQueue.tsx
Normal file
@@ -0,0 +1,476 @@
|
||||
import { GripHorizontal } from "lucide-react";
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { useI18n } from "../../application/i18n/I18nProvider";
|
||||
import { sftpTransferCenterStore } from "../../application/state/sftpTransferCenterStore";
|
||||
import { useStoredNumber } from "../../application/state/useStoredNumber";
|
||||
import type { useSftpState } from "../../application/state/useSftpState";
|
||||
import {
|
||||
STORAGE_KEY_SFTP_TRANSFER_CHILD_NAME_WIDTH,
|
||||
STORAGE_KEY_SFTP_TRANSFER_PANEL_HEIGHT,
|
||||
} from "../../infrastructure/config/storageKeys";
|
||||
import type { TransferTask } from "../../types";
|
||||
import { Button } from "../ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
|
||||
import { SftpTransferItem } from "./SftpTransferItem";
|
||||
|
||||
/** Same control path as the global transfer center — never a second pause implementation. */
|
||||
const pauseViaCenter = (taskId: string) => {
|
||||
void sftpTransferCenterStore.pause(taskId);
|
||||
};
|
||||
const resumeViaCenter = (taskId: string) => {
|
||||
void sftpTransferCenterStore.resume(taskId);
|
||||
};
|
||||
|
||||
type SftpState = ReturnType<typeof useSftpState>;
|
||||
|
||||
interface SftpTransferQueueProps {
|
||||
sftp: SftpState;
|
||||
visibleTransfers: SftpState["transfers"];
|
||||
allTransfers: SftpState["transfers"];
|
||||
canRevealTransferTarget?: (task: TransferTask) => boolean;
|
||||
onRevealTransferTarget?: (task: TransferTask) => void | Promise<void>;
|
||||
canCopyTransferTargetPath?: (task: TransferTask) => boolean;
|
||||
onCopyTransferTargetPath?: (task: TransferTask) => void | Promise<void>;
|
||||
}
|
||||
|
||||
const MIN_PANEL_HEIGHT = 112;
|
||||
const MAX_PANEL_HEIGHT = 480;
|
||||
const HEADER_HEIGHT = 42;
|
||||
const MIN_CHILD_NAME_WIDTH = 160;
|
||||
const MAX_CHILD_NAME_WIDTH = 480;
|
||||
const CHILD_ROW_HEIGHT = 28;
|
||||
const CHILD_VIRTUALIZE_THRESHOLD = 80;
|
||||
const CHILD_OVERSCAN = 8;
|
||||
const childListIdForTask = (taskId: string) => `sftp-transfer-children-${taskId.replace(/[^A-Za-z0-9_-]/g, "-")}`;
|
||||
|
||||
interface TransferChildListProps {
|
||||
childTasks: TransferTask[];
|
||||
childListId: string;
|
||||
childNameWidth: number;
|
||||
onResizeNameColumn: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
scrollContainerRef: React.RefObject<HTMLDivElement>;
|
||||
scrollTop: number;
|
||||
viewportHeight: number;
|
||||
onCancel: (taskId: string) => void;
|
||||
onPause: (taskId: string) => void;
|
||||
onResume: (taskId: string) => void;
|
||||
onRetry: (taskId: string) => Promise<void>;
|
||||
onDismiss: (taskId: string) => void;
|
||||
onSetNameColumnWidth: (width: number) => void;
|
||||
}
|
||||
|
||||
const TransferChildList: React.FC<TransferChildListProps> = ({
|
||||
childTasks,
|
||||
childListId,
|
||||
childNameWidth,
|
||||
onResizeNameColumn,
|
||||
scrollContainerRef,
|
||||
scrollTop,
|
||||
viewportHeight,
|
||||
onCancel,
|
||||
onPause,
|
||||
onResume,
|
||||
onRetry,
|
||||
onDismiss,
|
||||
onSetNameColumnWidth,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [contentTop, setContentTop] = useState(0);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current;
|
||||
const scrollContainer = scrollContainerRef.current;
|
||||
if (!container || !scrollContainer) return;
|
||||
|
||||
const nextTop =
|
||||
container.getBoundingClientRect().top -
|
||||
scrollContainer.getBoundingClientRect().top +
|
||||
scrollTop;
|
||||
|
||||
if (Math.abs(nextTop - contentTop) > 1) {
|
||||
setContentTop(nextTop);
|
||||
}
|
||||
}, [childTasks.length, contentTop, scrollContainerRef, scrollTop, viewportHeight]);
|
||||
|
||||
const needsVirtualization = childTasks.length > CHILD_VIRTUALIZE_THRESHOLD;
|
||||
// Use a fallback viewport height when not yet measured to avoid rendering
|
||||
// all children on the first frame. This caps the initial render to ~15 rows
|
||||
// instead of potentially thousands.
|
||||
const effectiveViewportHeight = viewportHeight > 0 ? viewportHeight : MAX_PANEL_HEIGHT;
|
||||
const shouldVirtualize = needsVirtualization;
|
||||
|
||||
const { startIndex, visibleTasks } = useMemo(() => {
|
||||
if (!shouldVirtualize) {
|
||||
return {
|
||||
startIndex: 0,
|
||||
visibleTasks: childTasks,
|
||||
};
|
||||
}
|
||||
|
||||
const relativeTop = Math.max(0, scrollTop - contentTop);
|
||||
const relativeBottom = Math.max(0, scrollTop + effectiveViewportHeight - contentTop);
|
||||
const start = Math.max(0, Math.floor(relativeTop / CHILD_ROW_HEIGHT) - CHILD_OVERSCAN);
|
||||
const end = Math.min(
|
||||
childTasks.length - 1,
|
||||
Math.ceil(relativeBottom / CHILD_ROW_HEIGHT) + CHILD_OVERSCAN,
|
||||
);
|
||||
|
||||
return {
|
||||
startIndex: start,
|
||||
visibleTasks: childTasks.slice(start, end + 1),
|
||||
};
|
||||
}, [childTasks, contentTop, effectiveViewportHeight, scrollTop, shouldVirtualize]);
|
||||
|
||||
return (
|
||||
<div
|
||||
id={childListId}
|
||||
ref={containerRef}
|
||||
className="border-t border-border/30 bg-background/30"
|
||||
>
|
||||
<div
|
||||
className={shouldVirtualize ? "relative" : undefined}
|
||||
style={shouldVirtualize ? { height: childTasks.length * CHILD_ROW_HEIGHT } : undefined}
|
||||
>
|
||||
{visibleTasks.map((child, visibleIndex) => {
|
||||
const index = shouldVirtualize ? startIndex + visibleIndex : visibleIndex;
|
||||
return (
|
||||
<div
|
||||
key={child.id}
|
||||
className={shouldVirtualize ? "absolute left-0 right-0" : undefined}
|
||||
style={shouldVirtualize ? { top: index * CHILD_ROW_HEIGHT } : undefined}
|
||||
>
|
||||
<SftpTransferItem
|
||||
task={child}
|
||||
isChild
|
||||
childNameColumnWidth={childNameWidth}
|
||||
childNameColumnMinWidth={MIN_CHILD_NAME_WIDTH}
|
||||
childNameColumnMaxWidth={MAX_CHILD_NAME_WIDTH}
|
||||
onResizeNameColumn={onResizeNameColumn}
|
||||
onSetNameColumnWidth={onSetNameColumnWidth}
|
||||
resizeHandleTabIndex={visibleIndex === 0 ? 0 : -1}
|
||||
onCancel={() => onCancel(child.id)}
|
||||
onPause={() => onPause(child.id)}
|
||||
onResume={() => onResume(child.id)}
|
||||
onRetry={() => onRetry(child.id)}
|
||||
onDismiss={() => onDismiss(child.id)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SftpTransferQueue: React.FC<SftpTransferQueueProps> = ({
|
||||
sftp,
|
||||
visibleTransfers,
|
||||
allTransfers,
|
||||
canRevealTransferTarget,
|
||||
onRevealTransferTarget,
|
||||
canCopyTransferTargetPath,
|
||||
onCopyTransferTargetPath,
|
||||
}) => {
|
||||
const { t } = useI18n();
|
||||
const [expandedParents, setExpandedParents] = useState<Record<string, boolean>>({});
|
||||
const [panelHeight, setPanelHeight, persistPanelHeight] = useStoredNumber(
|
||||
STORAGE_KEY_SFTP_TRANSFER_PANEL_HEIGHT,
|
||||
220,
|
||||
{ min: MIN_PANEL_HEIGHT, max: MAX_PANEL_HEIGHT },
|
||||
);
|
||||
const [childNameWidth, setChildNameWidth, persistChildNameWidth] = useStoredNumber(
|
||||
STORAGE_KEY_SFTP_TRANSFER_CHILD_NAME_WIDTH,
|
||||
260,
|
||||
{ min: MIN_CHILD_NAME_WIDTH, max: MAX_CHILD_NAME_WIDTH },
|
||||
);
|
||||
const panelHeightRef = useRef(panelHeight);
|
||||
const childNameWidthRef = useRef(childNameWidth);
|
||||
const dragStateRef = useRef<{ startY: number; startHeight: number } | null>(null);
|
||||
const childColumnDragRef = useRef<{ startX: number; startWidth: number } | null>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [scrollTop, setScrollTop] = useState(0);
|
||||
const [viewportHeight, setViewportHeight] = useState(0);
|
||||
const scrollFrameRef = useRef<number | null>(null);
|
||||
|
||||
panelHeightRef.current = panelHeight;
|
||||
childNameWidthRef.current = childNameWidth;
|
||||
|
||||
const childrenByParent = useMemo(() => {
|
||||
const map = new Map<string, TransferTask[]>();
|
||||
for (const task of allTransfers) {
|
||||
if (task.parentTaskId && task.status !== "cancelled") {
|
||||
const children = map.get(task.parentTaskId) || [];
|
||||
children.push(task);
|
||||
map.set(task.parentTaskId, children);
|
||||
}
|
||||
}
|
||||
for (const [parentId, children] of map) {
|
||||
map.set(
|
||||
parentId,
|
||||
[...children].sort((a, b) => b.startTime - a.startTime),
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}, [allTransfers]);
|
||||
|
||||
const topLevelTransfers = useMemo(
|
||||
() => visibleTransfers.filter((task) => !task.parentTaskId),
|
||||
[visibleTransfers],
|
||||
);
|
||||
|
||||
const clampPanelHeight = useCallback((height: number) => {
|
||||
if (typeof window === "undefined") {
|
||||
return Math.max(MIN_PANEL_HEIGHT, Math.min(MAX_PANEL_HEIGHT, height));
|
||||
}
|
||||
const viewportMax = Math.floor(window.innerHeight * 0.6);
|
||||
return Math.max(MIN_PANEL_HEIGHT, Math.min(Math.min(MAX_PANEL_HEIGHT, viewportMax), height));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedParents((prev) => {
|
||||
const next: Record<string, boolean> = {};
|
||||
let changed = false;
|
||||
|
||||
for (const task of topLevelTransfers) {
|
||||
const hasChildren = (childrenByParent.get(task.id)?.length ?? 0) > 0;
|
||||
if (!hasChildren) continue;
|
||||
next[task.id] = prev[task.id] ?? true;
|
||||
if (next[task.id] !== prev[task.id]) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed && Object.keys(prev).length === Object.keys(next).length) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
}, [childrenByParent, topLevelTransfers]);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollContainer = scrollContainerRef.current;
|
||||
if (!scrollContainer) return;
|
||||
|
||||
const updateViewport = () => setViewportHeight(scrollContainer.clientHeight);
|
||||
updateViewport();
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateViewport);
|
||||
resizeObserver.observe(scrollContainer);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (scrollFrameRef.current !== null) {
|
||||
window.cancelAnimationFrame(scrollFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (dragStateRef.current) {
|
||||
const deltaY = dragStateRef.current.startY - event.clientY;
|
||||
setPanelHeight(clampPanelHeight(dragStateRef.current.startHeight + deltaY));
|
||||
}
|
||||
if (childColumnDragRef.current) {
|
||||
const deltaX = event.clientX - childColumnDragRef.current.startX;
|
||||
const nextWidth = Math.max(
|
||||
MIN_CHILD_NAME_WIDTH,
|
||||
Math.min(MAX_CHILD_NAME_WIDTH, childColumnDragRef.current.startWidth + deltaX),
|
||||
);
|
||||
setChildNameWidth(nextWidth);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
const hadPanelDrag = !!dragStateRef.current;
|
||||
const hadChildColumnDrag = !!childColumnDragRef.current;
|
||||
dragStateRef.current = null;
|
||||
childColumnDragRef.current = null;
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
if (hadPanelDrag) {
|
||||
persistPanelHeight(panelHeightRef.current);
|
||||
}
|
||||
if (hadChildColumnDrag) {
|
||||
persistChildNameWidth(childNameWidthRef.current);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("mousemove", handleMouseMove);
|
||||
window.addEventListener("mouseup", handleMouseUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleMouseMove);
|
||||
window.removeEventListener("mouseup", handleMouseUp);
|
||||
document.body.style.cursor = "";
|
||||
document.body.style.userSelect = "";
|
||||
};
|
||||
}, [clampPanelHeight, panelHeight, persistChildNameWidth, persistPanelHeight, setChildNameWidth, setPanelHeight]);
|
||||
|
||||
const handleResizeStart = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
|
||||
dragStateRef.current = {
|
||||
startY: event.clientY,
|
||||
startHeight: panelHeight,
|
||||
};
|
||||
document.body.style.cursor = "row-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
}, [panelHeight]);
|
||||
|
||||
const handleChildColumnResizeStart = useCallback((event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
childColumnDragRef.current = {
|
||||
startX: event.clientX,
|
||||
startWidth: childNameWidth,
|
||||
};
|
||||
document.body.style.cursor = "col-resize";
|
||||
document.body.style.userSelect = "none";
|
||||
}, [childNameWidth]);
|
||||
|
||||
const handleChildColumnWidthSet = useCallback((width: number) => {
|
||||
const nextWidth = Math.max(MIN_CHILD_NAME_WIDTH, Math.min(MAX_CHILD_NAME_WIDTH, width));
|
||||
setChildNameWidth(nextWidth);
|
||||
persistChildNameWidth(nextWidth);
|
||||
}, [persistChildNameWidth, setChildNameWidth]);
|
||||
|
||||
const toggleExpanded = useCallback((taskId: string) => {
|
||||
setExpandedParents((prev) => ({
|
||||
...prev,
|
||||
[taskId]: !(prev[taskId] ?? true),
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleScroll = useCallback((event: React.UIEvent<HTMLDivElement>) => {
|
||||
const nextTop = event.currentTarget.scrollTop;
|
||||
if (scrollFrameRef.current !== null) return;
|
||||
scrollFrameRef.current = window.requestAnimationFrame(() => {
|
||||
scrollFrameRef.current = null;
|
||||
setScrollTop(nextTop);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (topLevelTransfers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="border-t border-border/70 bg-secondary/80 supports-[backdrop-filter]:backdrop-blur-sm shrink-0"
|
||||
data-section="terminal-sftp-transfer-queue"
|
||||
style={{ height: clampPanelHeight(panelHeight) }}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className="group flex h-3 cursor-row-resize items-center justify-center border-b border-border/30 text-muted-foreground/70"
|
||||
onMouseDown={handleResizeStart}
|
||||
>
|
||||
<GripHorizontal size={14} className="transition-colors group-hover:text-foreground/80" />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{t("sftp.transfers.dragToResize")}</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div
|
||||
className="flex items-center justify-between border-b border-border/40 px-3 py-1.5 text-[11px] text-muted-foreground"
|
||||
data-section="terminal-sftp-transfer-queue-header"
|
||||
>
|
||||
<span className="font-medium">
|
||||
{t("sftp.transfers")}
|
||||
{sftp.activeTransfersCount > 0 && (
|
||||
<span className="ml-2 text-primary">
|
||||
({t("sftp.transfers.active", { count: sftp.activeTransfersCount })})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
{sftp.transfers.some(
|
||||
(transfer) => transfer.status === "completed" || transfer.status === "cancelled",
|
||||
) && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-5 px-1.5 text-[11px]"
|
||||
onClick={sftp.clearCompletedTransfers}
|
||||
>
|
||||
{t("sftp.transfers.clearCompleted")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="overflow-auto"
|
||||
data-section="terminal-sftp-transfer-list"
|
||||
style={{ height: `calc(100% - ${HEADER_HEIGHT}px)` }}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
{topLevelTransfers.map((task) => {
|
||||
const childTasks = childrenByParent.get(task.id) ?? [];
|
||||
const isExpanded = expandedParents[task.id] ?? true;
|
||||
const childListId = childListIdForTask(task.id);
|
||||
|
||||
return (
|
||||
<React.Fragment key={task.id}>
|
||||
<SftpTransferItem
|
||||
task={task}
|
||||
canToggleChildren={childTasks.length > 0}
|
||||
isExpanded={isExpanded}
|
||||
visibleChildCount={childTasks.length}
|
||||
childListId={childListId}
|
||||
onToggleChildren={() => toggleExpanded(task.id)}
|
||||
onCancel={() => {
|
||||
void sftpTransferCenterStore.cancel(task.id);
|
||||
}}
|
||||
onPause={() => pauseViaCenter(task.id)}
|
||||
onResume={() => resumeViaCenter(task.id)}
|
||||
onRetry={() => { void sftpTransferCenterStore.retry(task.id); }}
|
||||
onDismiss={() => sftpTransferCenterStore.dismiss(task.id)}
|
||||
canRevealTarget={canRevealTransferTarget?.(task) ?? false}
|
||||
onRevealTarget={
|
||||
onRevealTransferTarget
|
||||
? () => {
|
||||
void onRevealTransferTarget(task);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
canCopyTargetPath={canCopyTransferTargetPath?.(task) ?? false}
|
||||
onCopyTargetPath={
|
||||
onCopyTransferTargetPath
|
||||
? () => {
|
||||
void onCopyTransferTargetPath(task);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{isExpanded && childTasks.length > 0 && (
|
||||
<TransferChildList
|
||||
childTasks={childTasks}
|
||||
childListId={childListId}
|
||||
childNameWidth={childNameWidth}
|
||||
onResizeNameColumn={handleChildColumnResizeStart}
|
||||
onSetNameColumnWidth={handleChildColumnWidthSet}
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
scrollTop={scrollTop}
|
||||
viewportHeight={viewportHeight}
|
||||
onCancel={(taskId) => { void sftpTransferCenterStore.cancel(taskId); }}
|
||||
onPause={(taskId) => pauseViaCenter(taskId)}
|
||||
onResume={(taskId) => resumeViaCenter(taskId)}
|
||||
onRetry={(taskId) => { void sftpTransferCenterStore.retry(taskId); }}
|
||||
onDismiss={(taskId) => sftpTransferCenterStore.dismiss(taskId)}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user