feat(file-manager): allow disabling move-to-trash confirmation (#1511)

This commit is contained in:
ZacharyZcR authored and GitHub committed 2026-09-30 09:46:56 -05:00
1 parent fef33142b3
commit e69029fbf7
5 files changed
+56 -11

No files matched your search

+4
View File
@@ -27,6 +27,10 @@
"transferConcurrency": { "transferConcurrency": {
"label": "Simultaneous File Transfers", "label": "Simultaneous File Transfers",
"description": "How many files the side by side local/remote manager uploads or downloads at the same time. Lower this if your server limits SFTP channels." "description": "How many files the side by side local/remote manager uploads or downloads at the same time. Lower this if your server limits SFTP channels."
},
"confirmBeforeTrash": {
"label": "Confirm before moving files to trash",
"description": "Ask before moving remote files to trash. Permanent deletion still requires confirmation."
} }
} }
}, },
+7
View File
@@ -80,6 +80,13 @@
"min": 1, "min": 1,
"max": 8, "max": 8,
"default": 4 "default": 4
},
{
"key": "confirmBeforeTrash",
"type": "boolean",
"labelKey": "settings.user.confirmBeforeTrash.label",
"descriptionKey": "settings.user.confirmBeforeTrash.description",
"default": true
} }
], ],
"host": { "host": {
@@ -48,6 +48,7 @@ import { ConnectionLogPanel } from "@termix/plugin-sdk/ui";
import { import {
usePluginUiPreferences, usePluginUiPreferences,
invokeAction, invokeAction,
useSettings,
useConnectionRetry, useConnectionRetry,
logActivity, logActivity,
} from "@termix/plugin-sdk/frontend"; } from "@termix/plugin-sdk/frontend";
@@ -146,6 +147,8 @@ function FileManagerContent({
[t], [t],
); );
const { confirmWithToast } = useConfirmation(); const { confirmWithToast } = useConfirmation();
const confirmBeforeTrash =
useSettings("user").values.confirmBeforeTrash !== false;
const { addLog, clearLogs } = useConnectionLog(); const { addLog, clearLogs } = useConnectionLog();
const [currentHost] = useState<SSHHost | null>(initialHost || null); const [currentHost] = useState<SSHHost | null>(initialHost || null);
@@ -1484,9 +1487,7 @@ function FileManagerContent({
const fullMessage = `${confirmMessage}\n\n${t("fileManager.moveToTrashWarning")}`; const fullMessage = `${confirmMessage}\n\n${t("fileManager.moveToTrashWarning")}`;
confirmWithToast( const moveToTrash = async () => {
fullMessage,
async () => {
const operationPath = currentPath; const operationPath = currentPath;
const operationSession = sshSessionId; const operationSession = sshSessionId;
const deletedPaths = new Set(files.map((file) => file.path)); const deletedPaths = new Set(files.map((file) => file.path));
@@ -1595,9 +1596,12 @@ function FileManagerContent({
} }
console.error("Delete failed:", error); console.error("Delete failed:", error);
} }
}, };
"destructive", if (confirmBeforeTrash) {
); confirmWithToast(fullMessage, moveToTrash, "destructive");
} else {
await moveToTrash();
}
} }
async function handleSudoPasswordSubmit(password: string) { async function handleSudoPasswordSubmit(password: string) {
@@ -1,3 +1,4 @@
import { useSettings } from "@termix/plugin-sdk/frontend";
import { fileManagerHostSetting } from "./host-settings"; import { fileManagerHostSetting } from "./host-settings";
import { import {
useCallback, useCallback,
@@ -548,6 +549,8 @@ function RemotePane({
} }
export function SftpTransferTab() { export function SftpTransferTab() {
const confirmBeforeTrash =
useSettings("user").values.confirmBeforeTrash !== false;
const { t } = useTranslation(); const { t } = useTranslation();
const formatTransferMetrics = useMemo( const formatTransferMetrics = useMemo(
() => createFormatTransferMetrics(t), () => createFormatTransferMetrics(t),
@@ -894,10 +897,10 @@ export function SftpTransferTab() {
} }
}; };
const handleDeleteConfirmed = async () => { const handleDeleteConfirmed = async (target = deleteTarget) => {
if (!deleteTarget) return; if (!target) return;
const entries = getContextEntries(deleteTarget); const entries = getContextEntries(target);
const paneId = deleteTarget.paneId; const paneId = target.paneId;
setDeleteTarget(null); setDeleteTarget(null);
if (entries.length === 0) return; if (entries.length === 0) return;
@@ -1071,7 +1074,8 @@ export function SftpTransferTab() {
<ContextMenuButton <ContextMenuButton
disabled={!canMutateContextPane || contextEntries.length === 0} disabled={!canMutateContextPane || contextEntries.length === 0}
onClick={() => { onClick={() => {
setDeleteTarget(contextMenu); if (confirmBeforeTrash) setDeleteTarget(contextMenu);
else void handleDeleteConfirmed(contextMenu);
setContextMenu(null); setContextMenu(null);
}} }}
> >
@@ -6,6 +6,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
import { SftpTransferTab } from "../../src/frontend/SftpTransferTab"; import { SftpTransferTab } from "../../src/frontend/SftpTransferTab";
const api = vi.hoisted(() => ({ const api = vi.hoisted(() => ({
confirmBeforeTrash: undefined as boolean | undefined,
addTransferRecent: vi.fn(), addTransferRecent: vi.fn(),
browseSSHDirectory: vi.fn(), browseSSHDirectory: vi.fn(),
changeSSHPermissions: vi.fn(), changeSSHPermissions: vi.fn(),
@@ -51,12 +52,20 @@ vi.mock("react-i18next", () => ({
initReactI18next: { type: "3rdParty", init: () => {} }, initReactI18next: { type: "3rdParty", init: () => {} },
})); }));
vi.mock("@termix/plugin-sdk/frontend", async (importOriginal) => ({
...(await importOriginal<typeof import("@termix/plugin-sdk/frontend")>()),
useSettings: () => ({
values: { confirmBeforeTrash: api.confirmBeforeTrash },
}),
}));
vi.mock("sonner", () => ({ vi.mock("sonner", () => ({
toast: { success: vi.fn(), error: vi.fn() }, toast: { success: vi.fn(), error: vi.fn() },
})); }));
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks(); vi.clearAllMocks();
api.confirmBeforeTrash = undefined;
api.getSSHHosts.mockResolvedValue([ api.getSSHHosts.mockResolvedValue([
{ {
id: 1, id: 1,
@@ -206,12 +215,29 @@ describe("SftpTransferTab", () => {
}); });
}); });
it("moves files to trash directly when confirmation is disabled", async () => {
api.confirmBeforeTrash = false;
render(<SftpTransferTab />);
await selectHosts();
fireEvent.contextMenu(screen.getByText("remote-1.txt"));
await userEvent.click(screen.getByText("sftpTransfer.delete"));
await waitFor(() =>
expect(api.deleteSSHItem).toHaveBeenCalledWith(
"1",
"/srv/remote-1.txt",
false,
),
);
expect(screen.queryByText("sftpTransfer.deleteSelectedItems")).toBeNull();
});
it("deletes a remote file after confirming", async () => { it("deletes a remote file after confirming", async () => {
render(<SftpTransferTab />); render(<SftpTransferTab />);
await selectHosts(); await selectHosts();
fireEvent.contextMenu(screen.getByText("remote-1.txt")); fireEvent.contextMenu(screen.getByText("remote-1.txt"));
await userEvent.click(screen.getByText("sftpTransfer.delete")); await userEvent.click(screen.getByText("sftpTransfer.delete"));
expect(api.deleteSSHItem).not.toHaveBeenCalled();
const confirmButtons = await screen.findAllByText("sftpTransfer.delete"); const confirmButtons = await screen.findAllByText("sftpTransfer.delete");
await userEvent.click(confirmButtons[confirmButtons.length - 1]); await userEvent.click(confirmButtons[confirmButtons.length - 1]);