mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
Various functional updates and improvements across provider dialogs, MCP panel, skills page, and internationalization. Provider Dialogs: - AddProviderDialog * Simplified form state management * Improved preset selection workflow * Better validation error messages * Enhanced template variable handling - EditProviderDialog * Streamlined edit flow with better state synchronization * Improved handling of live config backfilling * Better error recovery for failed updates * Enhanced integration with parent components MCP & Skills: - UnifiedMcpPanel * Reduced complexity from 140+ to ~95 lines * Improved multi-app server management * Better server type detection (stdio/http) * Enhanced server status indicators * Cleaner integration with MCP form modal - SkillsPage * Simplified navigation and state management * Better integration with RepoManagerPanel * Improved error handling for repository operations * Enhanced loading states - SkillCard * Minor layout adjustments * Better action button placement Environment & Configuration: - EnvWarningBanner * Improved conflict detection messages * Better visual hierarchy for warnings * Enhanced dismissal behavior - tauri.conf.json * Updated build configuration * Added new window management options Internationalization: - en.json & zh.json * Added 17 new translation keys for new features * Updated existing keys for better clarity * Added translations for new settings page * Improved consistency across UI text Code Cleanup: - mutations.ts * Removed 14 lines of unused mutation definitions * Cleaned up deprecated query invalidation logic * Better type safety for mutation parameters Overall Impact: - Reduced total lines by 51 (-10% in affected files) - Improved component integration and data flow - Better error handling and user feedback - Enhanced i18n coverage for new features These changes improve the overall polish and integration of various components while removing technical debt and unused code.
275 lines
9.8 KiB
TypeScript
275 lines
9.8 KiB
TypeScript
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { AlertTriangle, ChevronDown, ChevronUp, X, Trash2 } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import type { EnvConflict } from "@/types/env";
|
|
import { deleteEnvVars } from "@/lib/api/env";
|
|
import { toast } from "sonner";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
|
|
interface EnvWarningBannerProps {
|
|
conflicts: EnvConflict[];
|
|
onDismiss: () => void;
|
|
onDeleted: () => void;
|
|
}
|
|
|
|
export function EnvWarningBanner({
|
|
conflicts,
|
|
onDismiss,
|
|
onDeleted,
|
|
}: EnvWarningBannerProps) {
|
|
const { t } = useTranslation();
|
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
const [selectedConflicts, setSelectedConflicts] = useState<Set<string>>(
|
|
new Set(),
|
|
);
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
|
|
|
if (conflicts.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const toggleSelection = (key: string) => {
|
|
const newSelection = new Set(selectedConflicts);
|
|
if (newSelection.has(key)) {
|
|
newSelection.delete(key);
|
|
} else {
|
|
newSelection.add(key);
|
|
}
|
|
setSelectedConflicts(newSelection);
|
|
};
|
|
|
|
const toggleSelectAll = () => {
|
|
if (selectedConflicts.size === conflicts.length) {
|
|
setSelectedConflicts(new Set());
|
|
} else {
|
|
setSelectedConflicts(
|
|
new Set(conflicts.map((c) => `${c.varName}:${c.sourcePath}`)),
|
|
);
|
|
}
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
setShowConfirmDialog(false);
|
|
setIsDeleting(true);
|
|
|
|
try {
|
|
const conflictsToDelete = conflicts.filter((c) =>
|
|
selectedConflicts.has(`${c.varName}:${c.sourcePath}`),
|
|
);
|
|
|
|
if (conflictsToDelete.length === 0) {
|
|
toast.warning(t("env.error.noSelection"));
|
|
return;
|
|
}
|
|
|
|
const backupInfo = await deleteEnvVars(conflictsToDelete);
|
|
|
|
toast.success(t("env.delete.success"), {
|
|
description: t("env.backup.location", {
|
|
path: backupInfo.backupPath,
|
|
}),
|
|
duration: 5000,
|
|
});
|
|
|
|
// 清空选择并通知父组件
|
|
setSelectedConflicts(new Set());
|
|
onDeleted();
|
|
} catch (error) {
|
|
console.error("删除环境变量失败:", error);
|
|
toast.error(t("env.delete.error"), {
|
|
description: String(error),
|
|
});
|
|
} finally {
|
|
setIsDeleting(false);
|
|
}
|
|
};
|
|
|
|
const getSourceDescription = (conflict: EnvConflict): string => {
|
|
if (conflict.sourceType === "system") {
|
|
if (conflict.sourcePath.includes("HKEY_CURRENT_USER")) {
|
|
return t("env.source.userRegistry");
|
|
} else if (conflict.sourcePath.includes("HKEY_LOCAL_MACHINE")) {
|
|
return t("env.source.systemRegistry");
|
|
} else {
|
|
return t("env.source.systemEnv");
|
|
}
|
|
} else {
|
|
return conflict.sourcePath;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div className="fixed top-0 left-0 right-0 z-[100] bg-yellow-50 dark:bg-yellow-950 border-b border-yellow-200 dark:border-yellow-900 shadow-lg animate-slide-down">
|
|
<div className="container mx-auto px-4 py-3">
|
|
<div className="flex items-start gap-3">
|
|
<AlertTriangle className="h-5 w-5 text-yellow-600 dark:text-yellow-500 flex-shrink-0 mt-0.5" />
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center justify-between gap-3">
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-yellow-900 dark:text-yellow-100">
|
|
{t("env.warning.title")}
|
|
</h3>
|
|
<p className="text-sm text-yellow-800 dark:text-yellow-200 mt-0.5">
|
|
{t("env.warning.description", { count: conflicts.length })}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 flex-shrink-0">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
className="text-yellow-900 dark:text-yellow-100 hover:bg-yellow-100 dark:hover:bg-yellow-900/50"
|
|
>
|
|
{isExpanded ? (
|
|
<>
|
|
{t("env.actions.collapse")}
|
|
<ChevronUp className="h-4 w-4 ml-1" />
|
|
</>
|
|
) : (
|
|
<>
|
|
{t("env.actions.expand")}
|
|
<ChevronDown className="h-4 w-4 ml-1" />
|
|
</>
|
|
)}
|
|
</Button>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={onDismiss}
|
|
className="text-yellow-900 dark:text-yellow-100 hover:bg-yellow-100 dark:hover:bg-yellow-900/50"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{isExpanded && (
|
|
<div className="mt-4 space-y-3">
|
|
<div className="flex items-center gap-2 pb-2 border-b border-yellow-200 dark:border-yellow-900/50">
|
|
<Checkbox
|
|
id="select-all"
|
|
checked={selectedConflicts.size === conflicts.length}
|
|
onCheckedChange={toggleSelectAll}
|
|
/>
|
|
<label
|
|
htmlFor="select-all"
|
|
className="text-sm font-medium text-yellow-900 dark:text-yellow-100 cursor-pointer"
|
|
>
|
|
{t("env.actions.selectAll")}
|
|
</label>
|
|
</div>
|
|
|
|
<div className="max-h-96 overflow-y-auto space-y-2">
|
|
{conflicts.map((conflict) => {
|
|
const key = `${conflict.varName}:${conflict.sourcePath}`;
|
|
return (
|
|
<div
|
|
key={key}
|
|
className="flex items-start gap-3 p-3 bg-white dark:bg-gray-900 rounded-md border border-yellow-200 dark:border-yellow-900/50"
|
|
>
|
|
<Checkbox
|
|
id={key}
|
|
checked={selectedConflicts.has(key)}
|
|
onCheckedChange={() => toggleSelection(key)}
|
|
/>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<label
|
|
htmlFor={key}
|
|
className="block text-sm font-medium text-gray-900 dark:text-gray-100 cursor-pointer"
|
|
>
|
|
{conflict.varName}
|
|
</label>
|
|
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1 break-all">
|
|
{t("env.field.value")}: {conflict.varValue}
|
|
</p>
|
|
<p className="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
|
{t("env.field.source")}:{" "}
|
|
{getSourceDescription(conflict)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div className="flex items-center justify-end gap-2 pt-2 border-t border-yellow-200 dark:border-yellow-900/50">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setSelectedConflicts(new Set())}
|
|
disabled={selectedConflicts.size === 0}
|
|
className="text-yellow-900 dark:text-yellow-100 border-yellow-300 dark:border-yellow-800"
|
|
>
|
|
{t("env.actions.clearSelection")}
|
|
</Button>
|
|
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
onClick={() => setShowConfirmDialog(true)}
|
|
disabled={selectedConflicts.size === 0 || isDeleting}
|
|
className="gap-1"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
{isDeleting
|
|
? t("env.actions.deleting")
|
|
: t("env.actions.deleteSelected", {
|
|
count: selectedConflicts.size,
|
|
})}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Dialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
|
<DialogContent className="max-w-md" zIndex="top">
|
|
<DialogHeader>
|
|
<DialogTitle className="flex items-center gap-2">
|
|
<AlertTriangle className="h-5 w-5 text-destructive" />
|
|
{t("env.confirm.title")}
|
|
</DialogTitle>
|
|
<DialogDescription className="space-y-2">
|
|
<p>
|
|
{t("env.confirm.message", { count: selectedConflicts.size })}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{t("env.confirm.backupNotice")}
|
|
</p>
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setShowConfirmDialog(false)}
|
|
>
|
|
{t("common.cancel")}
|
|
</Button>
|
|
<Button variant="destructive" onClick={handleDelete}>
|
|
{t("env.confirm.confirm")}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</>
|
|
);
|
|
}
|