fix(ui): 确认框在 Tauri 下被绕过——改用 await plugin-dialog confirm
tauri-plugin-dialog 把 window.confirm 替换为 async 函数,同步判断拿到恒为真的 Promise,撤销测试账户、删除 Gitea 实例/解绑仓库、删除仓库、准备工具链 5 处 破坏性或改机器状态的操作均未弹框即执行(UI 终验发现)。统一 await @tauri-apps/plugin-dialog 的 confirm,并加源码扫描回归测试防复发。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9211883a9b
commit
f74fe11145
@ -24,6 +24,7 @@ import {
|
|||||||
gitPush,
|
gitPush,
|
||||||
type GiteaInstance,
|
type GiteaInstance,
|
||||||
} from "../../lib/commands";
|
} from "../../lib/commands";
|
||||||
|
import { confirm } from "@tauri-apps/plugin-dialog";
|
||||||
import { useUIStore } from "../../store/ui";
|
import { useUIStore } from "../../store/ui";
|
||||||
|
|
||||||
type BindMode = "create" | "migrate" | "link";
|
type BindMode = "create" | "migrate" | "link";
|
||||||
@ -98,7 +99,7 @@ export function GiteaInstancesSection() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (inst: GiteaInstance) => {
|
const handleDelete = async (inst: GiteaInstance) => {
|
||||||
if (!confirm(`删除实例「${inst.name}」?其下所有项目绑定会一并删除。`)) return;
|
if (!(await confirm(`删除实例「${inst.name}」?其下所有项目绑定会一并删除。`, { kind: "warning" }))) return;
|
||||||
try {
|
try {
|
||||||
await giteaDeleteInstance(inst.id);
|
await giteaDeleteInstance(inst.id);
|
||||||
qc.invalidateQueries({ queryKey: ["gitea-instances"] });
|
qc.invalidateQueries({ queryKey: ["gitea-instances"] });
|
||||||
@ -308,7 +309,7 @@ export function GiteaPanel({ projectId, projectName, projectPath }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleUnlink = async () => {
|
const handleUnlink = async () => {
|
||||||
if (!confirm("解绑当前项目与 Gitea 仓库?(Gitea 侧仓库不会删除)")) return;
|
if (!(await confirm("解绑当前项目与 Gitea 仓库?(Gitea 侧仓库不会删除)", { kind: "warning" }))) return;
|
||||||
try {
|
try {
|
||||||
await giteaUnlinkRepo(projectId);
|
await giteaUnlinkRepo(projectId);
|
||||||
refresh();
|
refresh();
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
saveProjectTestAccount,
|
saveProjectTestAccount,
|
||||||
revokeProjectDevVariable,
|
revokeProjectDevVariable,
|
||||||
} from "../../lib/commands";
|
} from "../../lib/commands";
|
||||||
|
import { confirm } from "@tauri-apps/plugin-dialog";
|
||||||
import { useUIStore } from "../../store/ui";
|
import { useUIStore } from "../../store/ui";
|
||||||
import { Dialog } from "../ui/Dialog";
|
import { Dialog } from "../ui/Dialog";
|
||||||
|
|
||||||
@ -142,8 +143,8 @@ export function ProjectDevVariablesPanel({ projectId, projectName, onClose }: Pr
|
|||||||
{item.configured && (
|
{item.configured && (
|
||||||
<button
|
<button
|
||||||
disabled={revokeMutation.isPending}
|
disabled={revokeMutation.isPending}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
if (window.confirm(`撤销 ${item.name}?Agent 将无法再读取该值。`)) {
|
if (await confirm(`撤销 ${item.name}?Agent 将无法再读取该值。`, { kind: "warning" })) {
|
||||||
revokeMutation.mutate(item.name);
|
revokeMutation.mutate(item.name);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
type RuntimeToolStatus,
|
type RuntimeToolStatus,
|
||||||
type ToolchainStatus,
|
type ToolchainStatus,
|
||||||
} from "../../lib/commands";
|
} from "../../lib/commands";
|
||||||
|
import { confirm } from "@tauri-apps/plugin-dialog";
|
||||||
import { useUIStore } from "../../store/ui";
|
import { useUIStore } from "../../store/ui";
|
||||||
|
|
||||||
// ── 层元信息 ───────────────────────────────────────────────────────────────────
|
// ── 层元信息 ───────────────────────────────────────────────────────────────────
|
||||||
@ -113,8 +114,9 @@ export function ProjectEnvModal({ project, onClose }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePrepareToolchain = async () => {
|
const handlePrepareToolchain = async () => {
|
||||||
const confirmed = window.confirm(
|
const confirmed = await confirm(
|
||||||
"炼境将通过 mise install 下载并准备项目已声明的 Node/pnpm 版本。不会修改 package.json 或 lockfile,是否继续?",
|
"炼境将通过 mise install 下载并准备项目已声明的 Node/pnpm 版本。不会修改 package.json 或 lockfile,是否继续?",
|
||||||
|
{ kind: "warning" },
|
||||||
);
|
);
|
||||||
if (!confirmed) return;
|
if (!confirmed) return;
|
||||||
setPreparing(true);
|
setPreparing(true);
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
type GitRepo,
|
type GitRepo,
|
||||||
type UpdateGitRepoDeployInput,
|
type UpdateGitRepoDeployInput,
|
||||||
} from "../../lib/commands";
|
} from "../../lib/commands";
|
||||||
|
import { confirm } from "@tauri-apps/plugin-dialog";
|
||||||
import { useUIStore } from "../../store/ui";
|
import { useUIStore } from "../../store/ui";
|
||||||
|
|
||||||
// ── 部署信息编辑表单 ──────────────────────────────────────────────────────────
|
// ── 部署信息编辑表单 ──────────────────────────────────────────────────────────
|
||||||
@ -127,7 +128,7 @@ export function RegistryTab() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleDelete = async (id: string, name: string) => {
|
const handleDelete = async (id: string, name: string) => {
|
||||||
if (!confirm(`确定删除仓库「${name}」?关联的工作区也会被删除。`)) return;
|
if (!(await confirm(`确定删除仓库「${name}」?关联的工作区也会被删除。`, { kind: "warning" }))) return;
|
||||||
try {
|
try {
|
||||||
await deleteGitRepo(id);
|
await deleteGitRepo(id);
|
||||||
qc.invalidateQueries({ queryKey: ["git-repos"] });
|
qc.invalidateQueries({ queryKey: ["git-repos"] });
|
||||||
|
|||||||
26
src/lib/confirmUsage.test.ts
Normal file
26
src/lib/confirmUsage.test.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
// tauri-plugin-dialog 把 window.confirm 替换成 async 函数:同步写法拿到的是永远为真的 Promise,
|
||||||
|
// 确认框形同虚设(2026-09-30 撤销/删除未弹框即执行)。破坏性操作必须 await plugin-dialog 的 confirm。
|
||||||
|
const sources = import.meta.glob<string>(["../**/*.{ts,tsx}", "!../**/*.test.ts"], {
|
||||||
|
query: "?raw",
|
||||||
|
import: "default",
|
||||||
|
eager: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("confirm usage", () => {
|
||||||
|
it("scans the frontend sources", () => {
|
||||||
|
expect(Object.keys(sources).length).toBeGreaterThan(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("never calls confirm without await", () => {
|
||||||
|
const offenders = Object.entries(sources).flatMap(([file, text]) =>
|
||||||
|
text
|
||||||
|
.split("\n")
|
||||||
|
.map((line, index) => ({ line, at: `${file}:${index + 1}` }))
|
||||||
|
.filter(({ line }) => /(^|[^\w.])(window\.)?confirm\(/.test(line) && !/await\s+(window\.)?confirm\(/.test(line))
|
||||||
|
.map(({ at }) => at),
|
||||||
|
);
|
||||||
|
expect(offenders).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user