Compare commits

...
Author SHA1 Message Date
Cursor Agentand陈大猫 13fbd49095 fix(settings): confirm SFTP association removal in-app (Fixes #3920)
Native confirm() on Windows leaves settings inputs unable to take focus.

Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
2026-10-25 03:52:57 +00:00
2 changed files with 62 additions and 4 deletions

No files matched your search

@@ -4,7 +4,45 @@ import test from "node:test";
const source = readFileSync(new URL("./SettingsFileAssociationsTab.tsx", import.meta.url), "utf8");
function functionBody(functionName: string): string {
const start = source.indexOf(`const ${functionName} = useCallback`);
assert.notEqual(start, -1, `${functionName} should exist`);
const next = source.indexOf("\n const ", start + 1);
return source.slice(start, next === -1 ? source.length : next);
}
test("SFTP settings imports the shared Toggle used by its option rows", () => {
assert.match(source, /<Toggle\b/);
assert.match(source, /Toggle,\s*\n\s*Select,/);
});
test("removing an SFTP file association confirms with the in-app dialog", () => {
assert.match(source, /import \{ ConfirmDialog \} from "\.\.\/\.\.\/ui\/confirm-dialog";/);
assert.doesNotMatch(source, /window\.confirm|globalThis\.confirm|\bconfirm\(/);
assert.match(
source,
/const \[pendingRemoveExtension, setPendingRemoveExtension\] = useState<string \| null>\(null\);/,
);
const handleRemove = functionBody("handleRemove");
assert.match(handleRemove, /setPendingRemoveExtension\(extension\)/);
assert.doesNotMatch(handleRemove, /removeAssociation\(/);
const handleConfirm = functionBody("handleConfirmRemoveAssociation");
assert.match(handleConfirm, /if \(!pendingRemoveExtension\) return;/);
assert.match(handleConfirm, /removeAssociation\(pendingRemoveExtension\);/);
assert.match(handleConfirm, /setPendingRemoveExtension\(null\);/);
const dialogStart = source.indexOf("<ConfirmDialog");
assert.notEqual(dialogStart, -1, "ConfirmDialog should be rendered");
const dialog = source.slice(dialogStart);
assert.match(dialog, /open=\{pendingRemoveExtension !== null\}/);
assert.match(dialog, /settings\.sftpFileAssociations\.removeConfirm/);
assert.match(dialog, /confirmLabel=\{t\('action\.remove'\)\}/);
assert.match(dialog, /\bdestructive\b/);
assert.match(dialog, /onConfirm=\{handleConfirmRemoveAssociation\}/);
const onOpenChange = dialog.slice(dialog.indexOf("onOpenChange"), dialog.indexOf("onConfirm"));
assert.match(onOpenChange, /if \(!open\) setPendingRemoveExtension\(null\);/);
assert.doesNotMatch(onOpenChange, /removeAssociation\(/);
});
@@ -9,6 +9,7 @@ import { useSettingsState } from "../../../application/state/useSettingsState";
import type { FileOpenerType, SystemAppInfo } from "../../../lib/sftpFileUtils";
import { netcattyBridge } from "../../../infrastructure/services/netcattyBridge";
import { Button } from "../../ui/button";
import { ConfirmDialog } from "../../ui/confirm-dialog";
import { Tooltip, TooltipContent, TooltipTrigger } from "../../ui/tooltip";
import {
SectionHeader,
@@ -51,6 +52,7 @@ export default function SettingsFileAssociationsTab() {
const associations = getAllAssociations();
const defaultOpener = getDefaultOpener();
const [editingExtension, setEditingExtension] = useState<string | null>(null);
const [pendingRemoveExtension, setPendingRemoveExtension] = useState<string | null>(null);
const [isSelectingDefaultApp, setIsSelectingDefaultApp] = useState(false);
const defaultOpenerValue = useMemo(() => {
@@ -60,10 +62,14 @@ export default function SettingsFileAssociationsTab() {
}, [defaultOpener]);
const handleRemove = useCallback((extension: string) => {
if (confirm(t('settings.sftpFileAssociations.removeConfirm', { ext: extension === 'file' ? t('sftp.opener.noExtension') : extension }))) {
removeAssociation(extension);
}
}, [removeAssociation, t]);
setPendingRemoveExtension(extension);
}, []);
const handleConfirmRemoveAssociation = useCallback(() => {
if (!pendingRemoveExtension) return;
removeAssociation(pendingRemoveExtension);
setPendingRemoveExtension(null);
}, [pendingRemoveExtension, removeAssociation]);
const handleSelectDefaultSystemApp = useCallback(async () => {
setIsSelectingDefaultApp(true);
@@ -372,6 +378,20 @@ export default function SettingsFileAssociationsTab() {
</table>
</div>
)}
<ConfirmDialog
open={pendingRemoveExtension !== null}
title={pendingRemoveExtension
? t('settings.sftpFileAssociations.removeConfirm', {
ext: pendingRemoveExtension === 'file' ? t('sftp.opener.noExtension') : pendingRemoveExtension,
})
: ''}
confirmLabel={t('action.remove')}
destructive
onOpenChange={(open) => {
if (!open) setPendingRemoveExtension(null);
}}
onConfirm={handleConfirmRemoveAssociation}
/>
</SettingsTabContent>
);
}