Files
NetMesh/components/sftp/SftpPaneView.tsx

789 lines
28 KiB
TypeScript
Raw Normal View History

import React, { memo, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, useTransition } from "react";
import { useI18n } from "../../application/i18n/I18nProvider";
import { logger } from "../../lib/logger";
import { useRenderTracker } from "../../lib/useRenderTracker";
import { cn } from "../../lib/utils";
import { SftpPaneDialogs } from "./SftpPaneDialogs";
import { SftpClipboardUploadDialog } from "./SftpClipboardUploadDialog";
import { SftpPaneEmptyState } from "./SftpPaneEmptyState";
import { SftpPaneFileList } from "./SftpPaneFileList";
import { SftpPaneToolbar } from "./SftpPaneToolbar";
import { SftpPaneTreeView } from "./SftpPaneTreeView";
import {
useActiveTabId,
useSftpConnectedHosts,
useSftpDrag,
useSftpHosts,
useSftpPaneCallbacks,
useSftpUpdateHosts,
useSftpWritableHosts,
} from "./SftpContext";
import type { SftpPane } from "../../application/state/sftp/types";
import { joinPath, getParentPath } from "../../application/state/sftp/utils";
import type { Host, SftpBookmark } from "../../domain/models";
import { useSftpPaneDialogs } from "./hooks/useSftpPaneDialogs";
import { useSftpPaneDragAndSelect } from "./hooks/useSftpPaneDragAndSelect";
import { useSftpPaneFiles } from "./hooks/useSftpPaneFiles";
import { useSftpPanePath } from "./hooks/useSftpPanePath";
import { useSftpPaneSorting, type UseSftpPaneSortingResult } from "../../application/state/sftp/useSftpPaneSorting";
import { useSftpPaneVirtualList } from "./hooks/useSftpPaneVirtualList";
import { sftpPaneViewModeStore } from "../../application/state/sftp/sftpPaneViewModeStore";
import { useSftpDialogActionHandler } from "../../application/state/sftp/sftpDialogActionStore";
import { useSftpBookmarks } from "./hooks/useSftpBookmarks";
import { useLocalSftpBookmarks } from "../../application/state/sftp/localSftpBookmarks";
import { useGlobalSftpBookmarks } from "./hooks/useGlobalSftpBookmarks";
import { useSftpHostViewMode } from "../../application/state/sftp/sftpHostViewModeStore";
import { useSftpListDensity } from "../../application/state/sftp/sftpListDensityStore";
import { sftpListOrderStore } from "./hooks/useSftpListOrderStore";
import { sftpTreeSelectionStore } from "../../application/state/sftp/sftpTreeSelectionStore";
import { sftpClipboardUploadStore } from "./clipboardUpload";
interface TreeReloadRequest {
token: number;
paths?: string[];
full?: boolean;
}
interface SftpPaneWrapperProps {
side: "left" | "right";
paneId: string;
isFirstPane: boolean;
children: React.ReactNode;
}
const SftpPaneWrapper = memo<SftpPaneWrapperProps>(({ side, paneId, isFirstPane, children }) => {
const activeTabId = useActiveTabId(side);
const isActive = activeTabId ? paneId === activeTabId : isFirstPane;
const containerStyle: React.CSSProperties = isActive
? {}
: { visibility: "hidden", pointerEvents: "none" };
return (
<div
className={cn("absolute inset-0", isActive ? "z-10" : "z-0")}
style={containerStyle}
>
{children}
</div>
);
});
SftpPaneWrapper.displayName = "SftpPaneWrapper";
interface SftpPaneViewProps {
side: "left" | "right";
pane: SftpPane;
dialogActionScopeId: string;
isPaneFocused: boolean;
sftpDefaultViewMode: 'list' | 'tree';
showHeader?: boolean;
showEmptyHeader?: boolean;
onToggleShowHiddenFiles?: () => void;
onGoToTerminalCwd?: () => void;
onLocatePathInTerminal?: () => void;
followTerminalCwd?: boolean;
onToggleFollowTerminalCwd?: () => void;
/** When true, treat this pane as always active (used by SftpSidePanel which manages visibility itself) */
forceActive?: boolean;
}
const SftpPaneViewInner: React.FC<SftpPaneViewProps> = ({
side,
pane,
dialogActionScopeId,
isPaneFocused,
sftpDefaultViewMode,
showHeader = true,
showEmptyHeader = true,
onToggleShowHiddenFiles,
onGoToTerminalCwd,
onLocatePathInTerminal,
followTerminalCwd,
onToggleFollowTerminalCwd,
forceActive,
}) => {
const activeTabId = useActiveTabId(side);
const isActive = forceActive || (activeTabId ? pane.id === activeTabId : true);
const callbacks = useSftpPaneCallbacks(side);
const { draggedFiles, onDragStart, onDragEnd } = useSftpDrag();
const hosts = useSftpHosts();
const connectedHosts = useSftpConnectedHosts();
const writableHosts = useSftpWritableHosts();
const { t } = useI18n();
const hostId = pane.connection?.hostId;
const { hostViewMode, setHostViewMode: saveHostViewMode } = useSftpHostViewMode(hostId);
const [, startTransition] = useTransition();
const [showFilterBar, setShowFilterBar] = useState(false);
const initialViewMode = hostViewMode ?? sftpDefaultViewMode ?? 'list';
const [viewMode, setViewMode] = useState<'list' | 'tree'>(initialViewMode);
const { density: listDensity, setDensity: setListDensity } = useSftpListDensity();
const [treeReloadRequest, setTreeReloadRequest] = useState<TreeReloadRequest>({ token: 0, full: true });
// Lazy-mount: only render the tree component once tree mode has been activated
const [treeEverMounted, setTreeEverMounted] = useState(initialViewMode === 'tree');
useEffect(() => {
if (viewMode === 'tree' && !treeEverMounted) setTreeEverMounted(true);
}, [viewMode, treeEverMounted]);
const filterInputRef = useRef<HTMLInputElement>(null);
const clipboardUploadRequestSnapshot = useSyncExternalStore(
sftpClipboardUploadStore.subscribe,
sftpClipboardUploadStore.getSnapshot,
sftpClipboardUploadStore.getSnapshot,
);
const clipboardUploadRequest =
clipboardUploadRequestSnapshot?.scopeId === dialogActionScopeId
&& clipboardUploadRequestSnapshot.side === side
&& isActive
? clipboardUploadRequestSnapshot
: null;
const requestTreeReload = useCallback((paths?: string[], full = false) => {
setTreeReloadRequest((prev) => ({
token: prev.token + 1,
paths,
full,
}));
}, []);
const requestNestedTreeReload = useCallback((paths?: string[]) => {
const targets = Array.from(new Set((paths ?? []).filter(Boolean)));
if (targets.length > 0) {
requestTreeReload(targets);
}
}, [requestTreeReload]);
useRenderTracker(`SftpPaneView[${side}]`, {
side,
paneId: pane.id,
paneConnected: pane.connected,
panePath: pane.currentPath,
showHeader,
draggedFilesCount: draggedFiles?.length ?? 0,
});
const {
sortField,
sortOrder,
directoriesFirst,
columnWidths,
visibleColumns,
handleSort,
handleResizeStart,
toggleColumnVisibility,
toggleDirectoriesFirst,
} = useSftpPaneSorting();
// Bookmark support
const updateHosts = useSftpUpdateHosts();
const currentHost = useMemo(
() => writableHosts.find((h) => h.id === pane.connection?.hostId),
[writableHosts, pane.connection?.hostId],
);
const onUpdateHost = useCallback(
(updated: Host) => updateHosts(writableHosts.map((h) => (h.id === updated.id ? updated : h))),
[updateHosts, writableHosts],
);
const remoteBookmarks = useSftpBookmarks({
host: currentHost,
currentPath: pane.connection?.currentPath,
onUpdateHost,
});
const localBookmarks = useLocalSftpBookmarks({
currentPath: pane.connection?.currentPath,
});
const globalBookmarks = useGlobalSftpBookmarks({
currentPath: pane.connection?.currentPath,
});
const hostBookmarks = pane.connection?.isLocal ? localBookmarks : remoteBookmarks;
const mergedBookmarks = useMemo(
() => [
...globalBookmarks.bookmarks.map((b) => ({ ...b, global: true as const })),
...hostBookmarks.bookmarks.map((b) => ({ ...b, global: false as const })),
],
[hostBookmarks.bookmarks, globalBookmarks.bookmarks],
);
const isCurrentPathBookmarked = hostBookmarks.isCurrentPathBookmarked || globalBookmarks.isCurrentPathBookmarked;
const toggleBookmark = useCallback(() => {
if (globalBookmarks.isCurrentPathBookmarked && !hostBookmarks.isCurrentPathBookmarked) {
const currentPath = pane.connection?.currentPath;
if (currentPath) {
const bm = globalBookmarks.bookmarks.find((b) => b.path === currentPath);
if (bm) globalBookmarks.deleteBookmark(bm.id);
}
} else {
hostBookmarks.toggleBookmark();
}
}, [hostBookmarks, globalBookmarks, pane.connection?.currentPath]);
const deleteBookmark = useCallback(
(bookmark: SftpBookmark) => {
if (bookmark.global) {
globalBookmarks.deleteBookmark(bookmark.id);
} else {
hostBookmarks.deleteBookmark(bookmark.id);
}
},
[hostBookmarks, globalBookmarks],
);
const reorderBookmark = useCallback(
(from: SftpBookmark, to: SftpBookmark) => {
if (from.global && to.global) {
globalBookmarks.reorderBookmark(from.id, to.id);
} else if (!from.global && !to.global) {
hostBookmarks.reorderBookmark(from.id, to.id);
}
},
[hostBookmarks, globalBookmarks],
);
const renameBookmark = useCallback(
(bookmark: SftpBookmark, label: string) => {
if (bookmark.global) {
globalBookmarks.renameBookmark(bookmark.id, label);
} else {
hostBookmarks.renameBookmark(bookmark.id, label);
}
},
[hostBookmarks, globalBookmarks],
);
const { sortedDisplayFiles } = useSftpPaneFiles({
files: pane.files,
filter: pane.filter,
connection: pane.connection,
showHiddenFiles: pane.showHiddenFiles,
enableListView: viewMode === 'list',
sortField,
sortOrder,
directoriesFirst,
});
const {
isEditingPath,
editingPathValue,
showPathSuggestions,
pathSuggestionIndex,
pathInputRef,
pathDropdownRef,
pathSuggestions,
setEditingPathValue,
setShowPathSuggestions,
setPathSuggestionIndex,
handlePathBlur,
handlePathKeyDown,
handlePathDoubleClick,
handlePathSubmit,
} = useSftpPanePath({
connection: pane.connection,
files: pane.files,
showHiddenFiles: pane.showHiddenFiles,
onNavigateTo: callbacks.onNavigateTo,
});
const {
showHostPicker,
hostSearch,
showNewFolderDialog,
newFolderName,
showNewFileDialog,
newFileName,
fileNameError,
showOverwriteConfirm,
overwriteTarget,
showRenameDialog,
renameTarget: _renameTarget,
renameName,
showDeleteConfirm,
deleteTargets,
isCreating,
isCreatingFile,
isRenaming,
isDeleting,
setShowHostPicker,
setHostSearch,
setShowNewFolderDialog,
setNewFolderName,
setShowNewFileDialog,
setNewFileName,
setFileNameError,
setShowOverwriteConfirm,
setShowRenameDialog,
setRenameName,
setShowDeleteConfirm,
handleCreateFolder,
handleCreateFile,
handleConfirmOverwrite,
handleRename,
handleDelete,
openNewFolderDialogAtPath,
openNewFileDialogAtPath,
openRenameDialog,
openDeleteConfirm,
getNextUntitledName,
} = useSftpPaneDialogs({
t,
pane,
onCreateDirectory: callbacks.onCreateDirectory,
onCreateDirectoryAtPath: callbacks.onCreateDirectoryAtPath,
onCreateFile: callbacks.onCreateFile,
onCreateFileAtPath: callbacks.onCreateFileAtPath,
onRenameFileAtPath: callbacks.onRenameFileAtPath,
onDeleteFilesAtPath: callbacks.onDeleteFilesAtPath,
onClearSelection: callbacks.onClearSelection,
onMutateSuccess: (paths?: string[]) => requestNestedTreeReload(paths),
});
const handleUploadExternalFiles = useCallback(async (dataTransfer: DataTransfer, targetPath?: string) => {
await callbacks.onUploadExternalFiles?.(dataTransfer, targetPath);
const affectedPath = targetPath ?? pane.connection?.currentPath;
if (affectedPath && affectedPath !== pane.connection?.currentPath) {
requestTreeReload([affectedPath]);
}
}, [callbacks, pane.connection?.currentPath, requestTreeReload]);
const handleUploadExternalFileList = useCallback(async (fileList: FileList, targetPath?: string) => {
await callbacks.onUploadExternalFileList?.(fileList, targetPath);
const affectedPath = targetPath ?? pane.connection?.currentPath;
if (affectedPath && affectedPath !== pane.connection?.currentPath) {
requestTreeReload([affectedPath]);
}
}, [callbacks, pane.connection?.currentPath, requestTreeReload]);
const handleUploadExternalFolder = useCallback(async (targetPath?: string) => {
await callbacks.onUploadExternalFolder?.(targetPath);
const affectedPath = targetPath ?? pane.connection?.currentPath;
if (affectedPath && affectedPath !== pane.connection?.currentPath) {
requestTreeReload([affectedPath]);
}
}, [callbacks, pane.connection?.currentPath, requestTreeReload]);
const handleExtractArchive = useCallback((entry: Parameters<NonNullable<typeof callbacks.onExtractArchive>>[0], fullPath?: string) => {
const archivePath = fullPath ?? joinPath(pane.connection?.currentPath ?? "", entry.name);
void Promise.resolve(callbacks.onExtractArchive?.(entry, fullPath)).then(() => {
const parentPath = getParentPath(archivePath);
if (parentPath) requestNestedTreeReload([parentPath]);
});
}, [callbacks, pane.connection?.currentPath, requestNestedTreeReload]);
const handleMoveEntriesToPath = useCallback(async (sourcePaths: string[], targetPath: string) => {
await callbacks.onMoveEntriesToPath(sourcePaths, targetPath);
}, [callbacks]);
const {
dragOverEntry,
isDragOverPane,
paneContainerRef,
handlePaneDragOver,
handlePaneDragLeave,
handlePaneDrop,
handleFileDragStart,
handleEntryDragOver,
handleEntryDrop,
handleRowDragLeave,
handleRowSelect,
handleRowOpen,
} = useSftpPaneDragAndSelect({
side,
pane,
sortedDisplayFiles,
draggedFiles,
onDragStart,
onReceiveFromOtherPane: callbacks.onReceiveFromOtherPane,
onMoveEntriesToPath: callbacks.onMoveEntriesToPath,
onUploadExternalFiles: handleUploadExternalFiles,
onOpenEntry: callbacks.onOpenEntry,
onRangeSelect: callbacks.onRangeSelect,
onToggleSelection: callbacks.onToggleSelection,
});
const {
fileListRef,
rowHeight,
handleFileListScroll,
shouldVirtualize,
totalHeight,
visibleRows,
} = useSftpPaneVirtualList({
isActive,
enabled: viewMode === 'list',
sortedDisplayFiles,
layoutKey: listDensity,
});
const toFullPath = useCallback(
(target: string) => {
const currentPath = pane.connection?.currentPath;
if (!currentPath || target.includes("/") || target.includes("\\")) {
return target;
}
return joinPath(currentPath, target);
},
[pane.connection?.currentPath],
);
// Handle keyboard shortcut dialog actions
const dialogActionHandlers = useMemo(
() => ({
onRename: (fileName: string) => openRenameDialog(toFullPath(fileName)),
onDelete: (fileNames: string[]) => openDeleteConfirm(fileNames.map(toFullPath)),
onNewFolder: () => {
setNewFolderName("");
setShowNewFolderDialog(true);
},
onNewFile: () => {
const defaultName = getNextUntitledName(pane.files.map(f => f.name));
setNewFileName(defaultName);
setFileNameError(null);
setShowNewFileDialog(true);
},
}),
[
getNextUntitledName,
openDeleteConfirm,
openRenameDialog,
pane.files,
toFullPath,
setFileNameError,
setNewFileName,
setNewFolderName,
setShowNewFileDialog,
setShowNewFolderDialog,
],
);
useSftpDialogActionHandler(side, dialogActionScopeId, dialogActionHandlers, isActive);
const handleSortWithTransition = useCallback((field: typeof sortField) => {
startTransition(() => handleSort(field));
}, [handleSort, startTransition]);
const sortingControls = useMemo<UseSftpPaneSortingResult>(() => ({
sortField,
sortOrder,
directoriesFirst,
columnWidths,
visibleColumns,
handleSort: handleSortWithTransition,
handleResizeStart,
toggleColumnVisibility,
toggleDirectoriesFirst,
}), [
columnWidths,
directoriesFirst,
handleResizeStart,
handleSortWithTransition,
sortField,
sortOrder,
toggleColumnVisibility,
toggleDirectoriesFirst,
visibleColumns,
]);
const handleRefresh = useCallback(() => {
callbacks.onRefresh();
if (viewMode === 'tree') {
requestTreeReload(undefined, true);
}
}, [callbacks, requestTreeReload, viewMode]);
const onSetFilterRef = useRef(callbacks.onSetFilter);
onSetFilterRef.current = callbacks.onSetFilter;
const onClearSelectionRef = useRef(callbacks.onClearSelection);
onClearSelectionRef.current = callbacks.onClearSelection;
const handleSetViewMode = useCallback((mode: 'list' | 'tree') => {
setViewMode(mode);
saveHostViewMode(mode);
if (mode === 'tree') {
setShowFilterBar(false);
onSetFilterRef.current('');
onClearSelectionRef.current();
}
}, [saveHostViewMode]);
useEffect(() => {
sftpPaneViewModeStore.set(pane.id, viewMode);
if (viewMode === 'list') {
sftpTreeSelectionStore.clearPane(pane.id);
} else {
sftpListOrderStore.clearPane(pane.id);
}
return () => sftpPaneViewModeStore.clear(pane.id);
}, [pane.id, viewMode]);
// When connecting to a host, restore its saved view mode preference
const prevHostIdRef = useRef<string | undefined>(undefined);
useEffect(() => {
if (hostId && hostId !== prevHostIdRef.current) {
setViewMode(hostViewMode ?? sftpDefaultViewMode);
}
prevHostIdRef.current = hostId;
}, [hostId, hostViewMode, sftpDefaultViewMode]);
useEffect(() => {
logger.debug("SftpPaneView active state", {
side,
paneId: pane.id,
isActive,
});
}, [isActive, pane.id, side]);
const lastHandledTransferMutationTokenRef = useRef(0);
useEffect(() => {
if (!pane.connection || pane.transferMutationToken === 0) return;
if (pane.transferMutationToken === lastHandledTransferMutationTokenRef.current) return;
lastHandledTransferMutationTokenRef.current = pane.transferMutationToken;
callbacks.onRefreshTab(pane.id);
if (viewMode === 'tree') {
requestTreeReload(undefined, true);
}
}, [callbacks, pane.connection, pane.id, pane.transferMutationToken, requestTreeReload, viewMode]);
if (!pane.connection) {
return (
<SftpPaneEmptyState
side={side}
showEmptyHeader={showEmptyHeader}
t={t}
showHostPicker={showHostPicker}
setShowHostPicker={setShowHostPicker}
hostSearch={hostSearch}
setHostSearch={setHostSearch}
hosts={hosts}
connectedHosts={connectedHosts}
onConnect={callbacks.onConnect}
/>
);
}
return (
<div
ref={paneContainerRef}
data-section="terminal-sftp-pane"
data-sftp-pane-side={side}
data-sftp-view-mode={viewMode}
className={cn(
"absolute inset-0 flex flex-col transition-colors",
isDragOverPane && "bg-primary/5",
)}
onDragOver={handlePaneDragOver}
onDragLeave={handlePaneDragLeave}
onDrop={handlePaneDrop}
>
<SftpPaneToolbar
t={t}
pane={pane}
onNavigateTo={callbacks.onNavigateTo}
onSetFilter={callbacks.onSetFilter}
onSetFilenameEncoding={callbacks.onSetFilenameEncoding}
onRefresh={handleRefresh}
showFilterBar={showFilterBar}
setShowFilterBar={setShowFilterBar}
filterInputRef={filterInputRef}
isEditingPath={isEditingPath}
editingPathValue={editingPathValue}
setEditingPathValue={setEditingPathValue}
setShowPathSuggestions={setShowPathSuggestions}
showPathSuggestions={showPathSuggestions}
setPathSuggestionIndex={setPathSuggestionIndex}
pathSuggestions={pathSuggestions}
pathSuggestionIndex={pathSuggestionIndex}
pathInputRef={pathInputRef}
pathDropdownRef={pathDropdownRef}
handlePathBlur={handlePathBlur}
handlePathKeyDown={handlePathKeyDown}
handlePathDoubleClick={handlePathDoubleClick}
handlePathSubmit={handlePathSubmit}
getNextUntitledName={getNextUntitledName}
setNewFileName={setNewFileName}
setFileNameError={setFileNameError}
setShowNewFileDialog={setShowNewFileDialog}
setShowNewFolderDialog={setShowNewFolderDialog}
setNewFolderName={setNewFolderName}
bookmarks={mergedBookmarks}
isCurrentPathBookmarked={isCurrentPathBookmarked}
onToggleBookmark={toggleBookmark}
onAddGlobalBookmark={globalBookmarks.addBookmark}
isCurrentPathGlobalBookmarked={globalBookmarks.isCurrentPathBookmarked}
onNavigateToBookmark={callbacks.onNavigateTo}
onDeleteBookmark={deleteBookmark}
onReorderBookmark={reorderBookmark}
onRenameBookmark={renameBookmark}
showHiddenFiles={pane.showHiddenFiles}
onToggleShowHiddenFiles={onToggleShowHiddenFiles}
onGoToTerminalCwd={onGoToTerminalCwd}
onLocatePathInTerminal={onLocatePathInTerminal}
followTerminalCwd={followTerminalCwd}
onToggleFollowTerminalCwd={onToggleFollowTerminalCwd}
viewMode={viewMode}
onSetViewMode={handleSetViewMode}
listDensity={listDensity}
onSetListDensity={setListDensity}
onListDrives={callbacks.onListDrives}
/>
{treeEverMounted && (
<div
className={viewMode === 'tree' ? 'flex-1 min-h-0 flex flex-col' : 'hidden'}
data-section="terminal-sftp-tree"
>
<SftpPaneTreeView
pane={pane}
side={side}
onPrepareSelection={callbacks.onPrepareSelection}
onLoadChildren={callbacks.onListDirectory}
onMoveEntriesToPath={handleMoveEntriesToPath}
onNavigateUp={callbacks.onNavigateUp}
onNavigateTo={callbacks.onNavigateTo}
onRefresh={handleRefresh}
onOpenEntry={callbacks.onOpenEntry}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
openRenameDialog={openRenameDialog}
openDeleteConfirm={openDeleteConfirm}
onCopyToOtherPane={callbacks.onCopyToOtherPane}
onReceiveFromOtherPane={callbacks.onReceiveFromOtherPane}
onOpenFileWithSystemDefault={callbacks.onOpenFileWithSystemDefault}
onOpenFileWith={callbacks.onOpenFileWith}
onEditFile={callbacks.onEditFile}
onDownloadFile={callbacks.onDownloadFile}
onExtractArchive={callbacks.onExtractArchive ? handleExtractArchive : undefined}
onEditPermissions={callbacks.onEditPermissions}
draggedFiles={draggedFiles}
openNewFolderDialog={openNewFolderDialogAtPath}
openNewFileDialog={openNewFileDialogAtPath}
onUploadExternalFiles={handleUploadExternalFiles}
onUploadExternalFileList={handleUploadExternalFileList}
onUploadExternalFolder={handleUploadExternalFolder}
sorting={sortingControls}
reloadRequest={treeReloadRequest}
/>
</div>
)}
<div
className={viewMode === 'list' ? 'flex-1 min-h-0 flex flex-col' : 'hidden'}
>
<SftpPaneFileList
t={t}
pane={pane}
side={side}
isPaneFocused={isPaneFocused}
sorting={sortingControls}
fileListRef={fileListRef}
handleFileListScroll={handleFileListScroll}
shouldVirtualize={shouldVirtualize}
totalHeight={totalHeight}
sortedDisplayFiles={sortedDisplayFiles}
isDragOverPane={isDragOverPane}
draggedFiles={draggedFiles}
onRefresh={handleRefresh}
onNavigateTo={callbacks.onNavigateTo}
onClearSelection={callbacks.onClearSelection}
setShowNewFolderDialog={setShowNewFolderDialog}
setShowNewFileDialog={setShowNewFileDialog}
getNextUntitledName={getNextUntitledName}
setNewFileName={setNewFileName}
setFileNameError={setFileNameError}
dragOverEntry={dragOverEntry}
handleRowSelect={handleRowSelect}
handleRowOpen={handleRowOpen}
handleFileDragStart={handleFileDragStart}
onDragEnd={onDragEnd}
handleEntryDragOver={handleEntryDragOver}
handleRowDragLeave={handleRowDragLeave}
handleEntryDrop={handleEntryDrop}
onCopyToOtherPane={callbacks.onCopyToOtherPane}
onMoveEntriesToPath={handleMoveEntriesToPath}
onOpenFileWithSystemDefault={callbacks.onOpenFileWithSystemDefault}
onOpenFileWith={callbacks.onOpenFileWith}
onEditFile={callbacks.onEditFile}
onDownloadFile={callbacks.onDownloadFile}
onDownloadFiles={callbacks.onDownloadFiles}
onExtractArchive={callbacks.onExtractArchive ? handleExtractArchive : undefined}
onEditPermissions={callbacks.onEditPermissions}
onUploadExternalFileList={handleUploadExternalFileList}
onUploadExternalFolder={handleUploadExternalFolder}
isLocal={!!pane.connection?.isLocal}
openRenameDialog={openRenameDialog}
openDeleteConfirm={openDeleteConfirm}
rowHeight={rowHeight}
visibleRows={visibleRows}
listDensity={listDensity}
/>
</div>
<SftpPaneDialogs
t={t}
hostLabel={pane.connection?.hostLabel}
currentPath={pane.connection?.currentPath}
showNewFolderDialog={showNewFolderDialog}
setShowNewFolderDialog={setShowNewFolderDialog}
newFolderName={newFolderName}
setNewFolderName={setNewFolderName}
handleCreateFolder={handleCreateFolder}
isCreating={isCreating}
showNewFileDialog={showNewFileDialog}
setShowNewFileDialog={setShowNewFileDialog}
newFileName={newFileName}
setNewFileName={setNewFileName}
fileNameError={fileNameError}
setFileNameError={setFileNameError}
handleCreateFile={handleCreateFile}
isCreatingFile={isCreatingFile}
showOverwriteConfirm={showOverwriteConfirm}
setShowOverwriteConfirm={setShowOverwriteConfirm}
overwriteTarget={overwriteTarget}
handleOverwriteConfirm={handleConfirmOverwrite}
showRenameDialog={showRenameDialog}
setShowRenameDialog={setShowRenameDialog}
renameName={renameName}
setRenameName={setRenameName}
handleRename={handleRename}
isRenaming={isRenaming}
showDeleteConfirm={showDeleteConfirm}
setShowDeleteConfirm={setShowDeleteConfirm}
deleteTargets={deleteTargets}
handleDelete={handleDelete}
isDeleting={isDeleting}
showHostPicker={showHostPicker}
setShowHostPicker={setShowHostPicker}
hosts={hosts}
connectedHosts={connectedHosts}
side={side}
hostSearch={hostSearch}
setHostSearch={setHostSearch}
onConnect={callbacks.onConnect}
onDisconnect={callbacks.onDisconnect}
/>
<SftpClipboardUploadDialog
request={clipboardUploadRequest}
currentPath={pane.connection?.currentPath}
onUploaded={(targetPath) => {
if (targetPath && targetPath !== pane.connection?.currentPath) {
requestTreeReload([targetPath]);
}
}}
/>
</div>
);
};
const sftpPaneViewAreEqual = (
prev: SftpPaneViewProps,
next: SftpPaneViewProps,
): boolean => {
if (prev.pane !== next.pane) return false;
if (prev.side !== next.side) return false;
if (prev.dialogActionScopeId !== next.dialogActionScopeId) return false;
if (prev.isPaneFocused !== next.isPaneFocused) return false;
if (prev.showHeader !== next.showHeader) return false;
if (prev.showEmptyHeader !== next.showEmptyHeader) return false;
if (prev.sftpDefaultViewMode !== next.sftpDefaultViewMode) return false;
if (prev.followTerminalCwd !== next.followTerminalCwd) return false;
if (prev.onToggleFollowTerminalCwd !== next.onToggleFollowTerminalCwd) return false;
if (prev.onGoToTerminalCwd !== next.onGoToTerminalCwd) return false;
if (prev.onLocatePathInTerminal !== next.onLocatePathInTerminal) return false;
if (prev.onToggleShowHiddenFiles !== next.onToggleShowHiddenFiles) return false;
return true;
};
const SftpPaneView = memo(SftpPaneViewInner, sftpPaneViewAreEqual);
SftpPaneView.displayName = "SftpPaneView";
export { SftpPaneView, SftpPaneWrapper };