dev-manager-tauri/src/components/dashboard/RepoRegistryModal.tsx
lanrtop f74fe11145 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>
2026-09-30 13:48:54 +09:00

215 lines
8.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import {
getGitRepos,
updateGitRepoDeploy,
deleteGitRepo,
type GitRepo,
type UpdateGitRepoDeployInput,
} from "../../lib/commands";
import { confirm } from "@tauri-apps/plugin-dialog";
import { useUIStore } from "../../store/ui";
// ── 部署信息编辑表单 ──────────────────────────────────────────────────────────
function DeployForm({
repo,
onSaved,
onCancel,
}: {
repo: GitRepo;
onSaved: () => void;
onCancel: () => void;
}) {
const showToast = useUIStore((s) => s.showToast);
const qc = useQueryClient();
const [form, setForm] = useState<UpdateGitRepoDeployInput>({
staging_url: repo.staging_url ?? "",
prod_url: repo.prod_url ?? "",
server_note: repo.server_note ?? "",
upstream_url: repo.upstream_url ?? "",
tech_stack: repo.tech_stack ?? "",
status: repo.status ?? "active",
priority: repo.priority ?? "mid",
});
const [saving, setSaving] = useState(false);
const set = (k: keyof UpdateGitRepoDeployInput) =>
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
setForm((f) => ({ ...f, [k]: e.target.value }));
const handleSave = async () => {
setSaving(true);
try {
await updateGitRepoDeploy(repo.id, form);
qc.invalidateQueries({ queryKey: ["git-repos"] });
showToast("已保存 ✓");
onSaved();
} catch (e) {
showToast(`❌ ${e}`);
} finally {
setSaving(false);
}
};
const inputCls = "w-full border border-gray-200 rounded-lg px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-blue-200";
return (
<div className="border-t border-blue-100 px-4 py-4 space-y-3 bg-blue-50/30">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">生产环境 URL</label>
<input className={inputCls} value={form.prod_url ?? ""} onChange={set("prod_url")} placeholder="https://example.com" />
</div>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">预发 / 测试 URL</label>
<input className={inputCls} value={form.staging_url ?? ""} onChange={set("staging_url")} placeholder="https://staging.example.com" />
</div>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">公司上游仓库</label>
<input className={inputCls} value={form.upstream_url ?? ""} onChange={set("upstream_url")} placeholder="https://github.com/company/repo.git" />
</div>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">技术栈</label>
<input className={inputCls} value={form.tech_stack ?? ""} onChange={set("tech_stack")} placeholder="React / Rust / Node" />
</div>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">状态</label>
<select className={inputCls} value={form.status ?? "active"} onChange={set("status")}>
<option value="active">进行中</option>
<option value="paused">暂停</option>
<option value="archived">归档</option>
</select>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">优先级</label>
<select className={inputCls} value={form.priority ?? "mid"} onChange={set("priority")}>
<option value="high">高</option>
<option value="mid">中</option>
<option value="low">低</option>
</select>
</div>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 mb-1">服务器备注</label>
<textarea
className={`${inputCls} resize-none`}
rows={2}
value={form.server_note ?? ""}
onChange={set("server_note")}
placeholder="服务器地址、SSH Key 路径…"
/>
</div>
<div className="flex justify-end gap-2">
<button onClick={onCancel} className="px-3 py-1.5 text-xs border border-gray-200 rounded-lg text-gray-500 hover:bg-gray-50">取消</button>
<button
onClick={handleSave}
disabled={saving}
className="px-3 py-1.5 text-xs bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{saving ? "保存中…" : "保存"}
</button>
</div>
</div>
);
}
// ── 已注册仓库列表 ────────────────────────────────────────────────────────────
// 同时被 ManagePage「仓库」tab 复用(ui-restructure N5)
export function RegistryTab() {
const showToast = useUIStore((s) => s.showToast);
const qc = useQueryClient();
const [expandedId, setExpandedId] = useState<string | null>(null);
const { data: repos = [], isLoading } = useQuery({
queryKey: ["git-repos"],
queryFn: getGitRepos,
});
const handleDelete = async (id: string, name: string) => {
if (!(await confirm(`确定删除仓库「${name}」?关联的工作区也会被删除。`, { kind: "warning" }))) return;
try {
await deleteGitRepo(id);
qc.invalidateQueries({ queryKey: ["git-repos"] });
qc.invalidateQueries({ queryKey: ["projects"] });
showToast("已删除");
} catch (e) {
showToast(`❌ ${e}`);
}
};
if (isLoading) {
return <div className="flex-1 flex items-center justify-center text-gray-400 text-sm">加载中…</div>;
}
if (repos.length === 0) {
return (
<div className="flex-1 flex flex-col items-center justify-center text-gray-400 py-16 gap-2">
<p className="text-2xl">📦</p>
<p className="text-sm">还没有注册仓库——在项目编辑中填写仓库地址或从仓库挂载后自动注册</p>
</div>
);
}
return (
<div className="flex-1 overflow-y-auto min-h-0">
{repos.map((repo) => {
const isExpanded = expandedId === repo.id;
return (
<div key={repo.id} className="border-b border-gray-100 last:border-0">
<div className="flex items-center gap-3 px-4 py-3 hover:bg-gray-50 transition-colors">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-gray-800">{repo.name}</span>
{repo.tech_stack && <span className="text-xs text-gray-400">{repo.tech_stack}</span>}
{repo.default_branch && (
<span className="px-1.5 py-0.5 rounded text-xs bg-gray-100 text-gray-500 font-mono">{repo.default_branch}</span>
)}
{repo.status && repo.status !== "active" && (
<span className={`text-xs px-1.5 py-0.5 rounded ${repo.status === "archived" ? "bg-gray-100 text-gray-400" : "bg-yellow-50 text-yellow-600"}`}>
{repo.status === "archived" ? "已归档" : "暂停"}
</span>
)}
</div>
<p className="text-xs text-gray-400 truncate mt-0.5 font-mono">{repo.repo_url}</p>
{(repo.prod_url || repo.staging_url) && (
<div className="flex gap-3 mt-1">
{repo.prod_url && <span className="text-xs text-green-600 truncate">生产: {repo.prod_url}</span>}
{repo.staging_url && <span className="text-xs text-blue-500 truncate">预发: {repo.staging_url}</span>}
</div>
)}
</div>
<div className="flex gap-1.5 shrink-0">
<button
onClick={() => setExpandedId(isExpanded ? null : repo.id)}
className={`px-2.5 py-1.5 rounded-lg text-xs border transition-colors ${
isExpanded
? "border-blue-300 bg-blue-50 text-blue-700"
: "border-gray-200 text-gray-500 hover:bg-gray-50"
}`}
>
{isExpanded ? "收起" : "部署信息"}
</button>
<button
onClick={() => handleDelete(repo.id, repo.name)}
className="px-2.5 py-1.5 rounded-lg text-xs border border-gray-200 text-red-400 hover:bg-red-50 hover:border-red-200 transition-colors"
>
删除
</button>
</div>
</div>
{isExpanded && (
<DeployForm
repo={repo}
onSaved={() => setExpandedId(null)}
onCancel={() => setExpandedId(null)}
/>
)}
</div>
);
})}
</div>
);
}