import { useCallback, useMemo, useRef, useState } from "react"; import type { MutableRefObject } from "react"; import type { SftpStateApi } from "../../../application/state/useSftpState"; import type { SftpDragCallbacks, SftpTransferSource } from "../SftpContext"; import { keepOnlyActivePaneSelections } from "./selectionScope"; import { editorTabStore } from "../../../application/state/editorTabStore"; import type { EditorTab, EditorTabId } from "../../../application/state/editorTabStore"; import { releaseEditorTabSaveCoordinator, saveEditorTab } from "../../../application/state/editorTabSave"; import { promptUnsavedChanges } from "../../editor/UnsavedChangesDialog"; import { toast } from "../../ui/toast"; import { requireCopyToOtherPaneTarget } from "../copyToOtherPane"; interface UseSftpViewPaneActionsParams { sftpRef: MutableRefObject; t: (key: string, vars?: Record) => string; } interface UseSftpViewPaneActionsResult { dragCallbacks: SftpDragCallbacks; draggedFiles: (SftpTransferSource & { side: "left" | "right" })[] | null; onConnectLeft: ( host: Parameters[1], options?: Parameters[2], ) => void; onConnectRight: ( host: Parameters[1], options?: Parameters[2], ) => void; onDisconnectLeft: () => Promise; onDisconnectRight: () => Promise; onPrepareSelectionLeft: () => void; onPrepareSelectionRight: () => void; onNavigateToLeft: (path: string) => void; onNavigateToRight: (path: string) => void; onNavigateUpLeft: () => void; onNavigateUpRight: () => void; onRefreshLeft: () => void; onRefreshRight: () => void; onRefreshTabLeft: (tabId: string) => void; onRefreshTabRight: (tabId: string) => void; onSetFilenameEncodingLeft: (encoding: Parameters[1]) => void; onSetFilenameEncodingRight: (encoding: Parameters[1]) => void; onToggleSelectionLeft: (name: string, multi: boolean) => void; onToggleSelectionRight: (name: string, multi: boolean) => void; onRangeSelectLeft: (fileNames: string[]) => void; onRangeSelectRight: (fileNames: string[]) => void; onClearSelectionLeft: () => void; onClearSelectionRight: () => void; onSetFilterLeft: (filter: string) => void; onSetFilterRight: (filter: string) => void; onCreateDirectoryLeft: (name: string) => void; onCreateDirectoryRight: (name: string) => void; onCreateDirectoryAtPathLeft: (path: string, name: string) => void; onCreateDirectoryAtPathRight: (path: string, name: string) => void; onCreateFileLeft: (name: string) => void; onCreateFileRight: (name: string) => void; onCreateFileAtPathLeft: (path: string, name: string) => void; onCreateFileAtPathRight: (path: string, name: string) => void; onDeleteFilesLeft: (names: string[]) => void; onDeleteFilesRight: (names: string[]) => void; onDeleteFilesAtPathLeft: (connectionId: string, path: string, names: string[]) => void; onDeleteFilesAtPathRight: (connectionId: string, path: string, names: string[]) => void; onRenameFileLeft: (old: string, newName: string) => void; onRenameFileRight: (old: string, newName: string) => void; onRenameFileAtPathLeft: (oldPath: string, newName: string) => void; onRenameFileAtPathRight: (oldPath: string, newName: string) => void; onMoveEntriesToPathLeft: (sourcePaths: string[], targetPath: string) => void; onMoveEntriesToPathRight: (sourcePaths: string[], targetPath: string) => void; onCopyToOtherPaneLeft: (files: SftpTransferSource[]) => void; onCopyToOtherPaneRight: (files: SftpTransferSource[]) => void; onReceiveFromOtherPaneLeft: (files: SftpTransferSource[]) => void; onReceiveFromOtherPaneRight: (files: SftpTransferSource[]) => void; } export async function disconnectSftpPaneAfterConfirmation(params: { confirmClose: () => Promise; disconnect: () => Promise; }): Promise { if (!await params.confirmClose()) return false; await params.disconnect(); return true; } export const useSftpViewPaneActions = ({ sftpRef, t, }: UseSftpViewPaneActionsParams): UseSftpViewPaneActionsResult => { const tRef = useRef(t); tRef.current = t; const [draggedFiles, setDraggedFiles] = useState< (SftpTransferSource & { side: "left" | "right" })[] | null >(null); const handleDragStart = useCallback( ( files: SftpTransferSource[], side: "left" | "right", ) => { setDraggedFiles(files.map((f) => ({ ...f, side }))); }, [], ); const handleDragEnd = useCallback(() => { setDraggedFiles(null); }, []); const startGroupedTransfer = useCallback( (files: SftpTransferSource[], sourceSide: "left" | "right", targetSide: "left" | "right") => { if (!requireCopyToOtherPaneTarget( sftpRef.current, targetSide, () => toast.info(tRef.current("sftp.copyToOtherPane.unavailable"), "SFTP"), )) { return; } const groups = new Map(); for (const file of files) { const key = `${file.sourceConnectionId ?? ""}::${file.sourcePath ?? ""}`; const group = groups.get(key) ?? []; group.push(file); groups.set(key, group); } for (const group of groups.values()) { const [{ sourceConnectionId, sourcePath, targetPath }] = group; void sftpRef.current.startTransfer(group, sourceSide, targetSide, { sourceConnectionId, sourcePath, targetPath, }); } }, [sftpRef], ); const onCopyToOtherPaneLeft = useCallback( (files: SftpTransferSource[]) => startGroupedTransfer(files, "left", "right"), [startGroupedTransfer], ); const onCopyToOtherPaneRight = useCallback( (files: SftpTransferSource[]) => startGroupedTransfer(files, "right", "left"), [startGroupedTransfer], ); const onReceiveFromOtherPaneLeft = useCallback( (files: SftpTransferSource[]) => startGroupedTransfer(files, "right", "left"), [startGroupedTransfer], ); const onReceiveFromOtherPaneRight = useCallback( (files: SftpTransferSource[]) => startGroupedTransfer(files, "left", "right"), [startGroupedTransfer], ); const onConnectLeft = useCallback( ( host: Parameters[1], options?: Parameters[2], ) => sftpRef.current.connect("left", host, options), [sftpRef], ); const onConnectRight = useCallback( ( host: Parameters[1], options?: Parameters[2], ) => sftpRef.current.connect("right", host, options), [sftpRef], ); // Returns `true` if the disconnect actually happened, `false` if the user // canceled the dirty-editor prompt. Callers that kick off a replacement // connect (e.g. the host picker) MUST gate their follow-up on this result // so a canceled prompt doesn't silently drop the user onto a new host. const confirmCloseActivePaneEditors = useCallback(async (side: "left" | "right"): Promise => { const pane = sftpRef.current.getActivePane(side); if (!pane?.connection?.id && !pane?.id) return true; const choice = (tab: EditorTab) => promptUnsavedChanges(tab.fileName); const saveTab = async (id: EditorTabId) => { const ok = await saveEditorTab(id); const tab = editorTabStore.getTab(id); if (!ok || (tab && tab.content !== tab.baselineContent)) { throw new Error(tab?.saveError ?? "Save failed"); } }; return editorTabStore.confirmCloseByOwner( { sessionId: pane.connection?.id, sftpTabId: pane.id }, choice, saveTab, releaseEditorTabSaveCoordinator, ); }, [sftpRef]); const onDisconnectLeft = useCallback(async (): Promise => { return disconnectSftpPaneAfterConfirmation({ confirmClose: () => confirmCloseActivePaneEditors("left"), disconnect: () => sftpRef.current.disconnect("left"), }); }, [confirmCloseActivePaneEditors, sftpRef]); const onDisconnectRight = useCallback(async (): Promise => { return disconnectSftpPaneAfterConfirmation({ confirmClose: () => confirmCloseActivePaneEditors("right"), disconnect: () => sftpRef.current.disconnect("right"), }); }, [confirmCloseActivePaneEditors, sftpRef]); const onPrepareSelectionLeft = useCallback(() => { keepOnlyActivePaneSelections(sftpRef.current, "left"); }, [sftpRef]); const onPrepareSelectionRight = useCallback(() => { keepOnlyActivePaneSelections(sftpRef.current, "right"); }, [sftpRef]); const onNavigateToLeft = useCallback( (path: string) => sftpRef.current.navigateTo("left", path), [sftpRef], ); const onNavigateToRight = useCallback( (path: string) => sftpRef.current.navigateTo("right", path), [sftpRef], ); const onNavigateUpLeft = useCallback(() => sftpRef.current.navigateUp("left"), [sftpRef]); const onNavigateUpRight = useCallback(() => sftpRef.current.navigateUp("right"), [sftpRef]); const onRefreshLeft = useCallback(() => sftpRef.current.refresh("left"), [sftpRef]); const onRefreshRight = useCallback(() => sftpRef.current.refresh("right"), [sftpRef]); const onRefreshTabLeft = useCallback((tabId: string) => sftpRef.current.refresh("left", { tabId }), [sftpRef]); const onRefreshTabRight = useCallback((tabId: string) => sftpRef.current.refresh("right", { tabId }), [sftpRef]); const onSetFilenameEncodingLeft = useCallback( (encoding: Parameters[1]) => sftpRef.current.setFilenameEncoding("left", encoding), [sftpRef], ); const onSetFilenameEncodingRight = useCallback( (encoding: Parameters[1]) => sftpRef.current.setFilenameEncoding("right", encoding), [sftpRef], ); const onToggleSelectionLeft = useCallback( (name: string, multi: boolean) => { onPrepareSelectionLeft(); sftpRef.current.toggleSelection("left", name, multi); }, [onPrepareSelectionLeft, sftpRef], ); const onToggleSelectionRight = useCallback( (name: string, multi: boolean) => { onPrepareSelectionRight(); sftpRef.current.toggleSelection("right", name, multi); }, [onPrepareSelectionRight, sftpRef], ); const onRangeSelectLeft = useCallback( (fileNames: string[]) => { onPrepareSelectionLeft(); sftpRef.current.rangeSelect("left", fileNames); }, [onPrepareSelectionLeft, sftpRef], ); const onRangeSelectRight = useCallback( (fileNames: string[]) => { onPrepareSelectionRight(); sftpRef.current.rangeSelect("right", fileNames); }, [onPrepareSelectionRight, sftpRef], ); const onClearSelectionLeft = useCallback(() => sftpRef.current.clearSelection("left"), [sftpRef]); const onClearSelectionRight = useCallback(() => sftpRef.current.clearSelection("right"), [sftpRef]); const onSetFilterLeft = useCallback( (filter: string) => sftpRef.current.setFilter("left", filter), [sftpRef], ); const onSetFilterRight = useCallback( (filter: string) => sftpRef.current.setFilter("right", filter), [sftpRef], ); const onCreateDirectoryLeft = useCallback( (name: string) => sftpRef.current.createDirectory("left", name), [sftpRef], ); const onCreateDirectoryRight = useCallback( (name: string) => sftpRef.current.createDirectory("right", name), [sftpRef], ); const onCreateDirectoryAtPathLeft = useCallback( (path: string, name: string) => sftpRef.current.createDirectoryAtPath("left", path, name), [sftpRef], ); const onCreateDirectoryAtPathRight = useCallback( (path: string, name: string) => sftpRef.current.createDirectoryAtPath("right", path, name), [sftpRef], ); const onCreateFileLeft = useCallback( (name: string) => sftpRef.current.createFile("left", name), [sftpRef], ); const onCreateFileRight = useCallback( (name: string) => sftpRef.current.createFile("right", name), [sftpRef], ); const onCreateFileAtPathLeft = useCallback( (path: string, name: string) => sftpRef.current.createFileAtPath("left", path, name), [sftpRef], ); const onCreateFileAtPathRight = useCallback( (path: string, name: string) => sftpRef.current.createFileAtPath("right", path, name), [sftpRef], ); const onDeleteFilesLeft = useCallback( (names: string[]) => sftpRef.current.deleteFiles("left", names), [sftpRef], ); const onDeleteFilesRight = useCallback( (names: string[]) => sftpRef.current.deleteFiles("right", names), [sftpRef], ); const onDeleteFilesAtPathLeft = useCallback( (connectionId: string, path: string, names: string[]) => sftpRef.current.deleteFilesAtPath("left", connectionId, path, names), [sftpRef], ); const onDeleteFilesAtPathRight = useCallback( (connectionId: string, path: string, names: string[]) => sftpRef.current.deleteFilesAtPath("right", connectionId, path, names), [sftpRef], ); const onRenameFileLeft = useCallback( (old: string, newName: string) => sftpRef.current.renameFile("left", old, newName), [sftpRef], ); const onRenameFileRight = useCallback( (old: string, newName: string) => sftpRef.current.renameFile("right", old, newName), [sftpRef], ); const onRenameFileAtPathLeft = useCallback( (oldPath: string, newName: string) => sftpRef.current.renameFileAtPath("left", oldPath, newName), [sftpRef], ); const onRenameFileAtPathRight = useCallback( (oldPath: string, newName: string) => sftpRef.current.renameFileAtPath("right", oldPath, newName), [sftpRef], ); const onMoveEntriesToPathLeft = useCallback( (sourcePaths: string[], targetPath: string) => sftpRef.current.moveEntriesToPath("left", sourcePaths, targetPath), [sftpRef], ); const onMoveEntriesToPathRight = useCallback( (sourcePaths: string[], targetPath: string) => sftpRef.current.moveEntriesToPath("right", sourcePaths, targetPath), [sftpRef], ); const dragCallbacks = useMemo( () => ({ onDragStart: handleDragStart, onDragEnd: handleDragEnd, }), [handleDragStart, handleDragEnd], ); return { dragCallbacks, draggedFiles, onConnectLeft, onConnectRight, onDisconnectLeft, onDisconnectRight, onPrepareSelectionLeft, onPrepareSelectionRight, onNavigateToLeft, onNavigateToRight, onNavigateUpLeft, onNavigateUpRight, onRefreshLeft, onRefreshRight, onRefreshTabLeft, onRefreshTabRight, onSetFilenameEncodingLeft, onSetFilenameEncodingRight, onToggleSelectionLeft, onToggleSelectionRight, onRangeSelectLeft, onRangeSelectRight, onClearSelectionLeft, onClearSelectionRight, onSetFilterLeft, onSetFilterRight, onCreateDirectoryLeft, onCreateDirectoryRight, onCreateDirectoryAtPathLeft, onCreateDirectoryAtPathRight, onCreateFileLeft, onCreateFileRight, onCreateFileAtPathLeft, onCreateFileAtPathRight, onDeleteFilesLeft, onDeleteFilesRight, onDeleteFilesAtPathLeft, onDeleteFilesAtPathRight, onRenameFileLeft, onRenameFileRight, onRenameFileAtPathLeft, onRenameFileAtPathRight, onMoveEntriesToPathLeft, onMoveEntriesToPathRight, onCopyToOtherPaneLeft, onCopyToOtherPaneRight, onReceiveFromOtherPaneLeft, onReceiveFromOtherPaneRight, }; };