fix: add system info copy feedback
This commit is contained in:
1 parent
ab44cb18fc
commit
2b8e3debd0
5 files changed
+52
-10
No files matched your search
@@ -54,3 +54,13 @@ describe("system information diagnostic fields", () => {
|
||||
expect(source).toContain("formatLastSuccessfulSync");
|
||||
});
|
||||
});
|
||||
|
||||
describe("system information copy feedback", () => {
|
||||
test("shows visible success and failure feedback", () => {
|
||||
expect(source).toContain('useState<"idle" | "copied" | "error">');
|
||||
expect(source).toContain("<ClipboardCopyNotice");
|
||||
expect(source).toContain("systemInfo.copySucceeded");
|
||||
expect(source).toContain("systemInfo.copyFailed");
|
||||
expect(source).toContain('copyState === "error"');
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { getClientDisplaySizeParts } from "@edgeever/shared";
|
||||
import type { DeploymentMetadata } from "@edgeever/shared/deployment-metadata";
|
||||
import { Activity, CircleCheck, Cloud, Copy, ExternalLink, Info, LoaderCircle, MonitorSmartphone, RefreshCw, RotateCcw } from "lucide-react";
|
||||
import { Activity, CircleCheck, CircleX, Cloud, Copy, ExternalLink, Info, LoaderCircle, MonitorSmartphone, RefreshCw, RotateCcw } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ClipboardCopyNotice } from "@/components/ClipboardCopyNotice";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useDeployedUpdateNotice } from "@/hooks/useDeployedUpdateNotice";
|
||||
import { detectWebClientKind } from "@/lib/client-environment";
|
||||
@@ -262,7 +263,8 @@ export const getShareableWebSystemInfoItems = (
|
||||
|
||||
export const SystemInfoPanel = ({ active = true }: { active?: boolean }) => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [copyState, setCopyState] = useState<"idle" | "copied" | "error">("idle");
|
||||
const copyResetTimeoutRef = useRef<number | null>(null);
|
||||
const [desktopUpdateChecked, setDesktopUpdateChecked] = useState(false);
|
||||
const [viewportRevision, setViewportRevision] = useState(0);
|
||||
const queryClient = useQueryClient();
|
||||
@@ -308,6 +310,9 @@ export const SystemInfoPanel = ({ active = true }: { active?: boolean }) => {
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
}, [active]);
|
||||
useEffect(() => () => {
|
||||
if (copyResetTimeoutRef.current !== null) window.clearTimeout(copyResetTimeoutRef.current);
|
||||
}, []);
|
||||
const desktopUpdateCheckMutation = useMutation({
|
||||
mutationFn: () => desktopBridge!.checkUpdate(),
|
||||
onSuccess: (status) => {
|
||||
@@ -391,9 +396,13 @@ export const SystemInfoPanel = ({ active = true }: { active?: boolean }) => {
|
||||
const text = infoGroups
|
||||
.map((group) => [group.title, ...group.items.map((item) => `${item.label}: ${item.value}`)].join("\n"))
|
||||
.join("\n\n");
|
||||
if (!(await copyTextToClipboard(text))) return;
|
||||
setCopied(true);
|
||||
window.setTimeout(() => setCopied(false), 1600);
|
||||
const copied = await copyTextToClipboard(text);
|
||||
setCopyState(copied ? "copied" : "error");
|
||||
if (copyResetTimeoutRef.current !== null) window.clearTimeout(copyResetTimeoutRef.current);
|
||||
copyResetTimeoutRef.current = window.setTimeout(() => {
|
||||
setCopyState("idle");
|
||||
copyResetTimeoutRef.current = null;
|
||||
}, copied ? 2200 : 3000);
|
||||
};
|
||||
|
||||
const clientAheadOfInstance = isClientAheadOfInstance(
|
||||
@@ -431,13 +440,25 @@ export const SystemInfoPanel = ({ active = true }: { active?: boolean }) => {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 gap-1.5 bg-card px-2.5 text-xs text-slate-700 shadow-xs hover:bg-slate-50"
|
||||
className={cn(
|
||||
"h-7 gap-1.5 bg-card px-2.5 text-xs text-slate-700 shadow-xs hover:bg-slate-50",
|
||||
copyState === "copied" && "border-emerald-200 bg-emerald-50 text-emerald-700 hover:bg-emerald-50",
|
||||
copyState === "error" && "border-rose-200 bg-rose-50 text-rose-700 hover:bg-rose-50",
|
||||
)}
|
||||
type="button"
|
||||
onClick={() => void handleCopy()}
|
||||
>
|
||||
{copied ? <CircleCheck className="h-3.5 w-3.5 text-emerald-600" /> : <Copy className="h-3.5 w-3.5 text-slate-500" />}
|
||||
<span className={copied ? "font-medium text-emerald-700" : ""}>
|
||||
{copied ? t("common.copied") : t("systemInfo.copy")}
|
||||
{copyState === "copied"
|
||||
? <CircleCheck className="h-3.5 w-3.5" />
|
||||
: copyState === "error"
|
||||
? <CircleX className="h-3.5 w-3.5" />
|
||||
: <Copy className="h-3.5 w-3.5 text-slate-500" />}
|
||||
<span className={copyState === "idle" ? "" : "font-medium"}>
|
||||
{copyState === "copied"
|
||||
? t("common.copied")
|
||||
: copyState === "error"
|
||||
? t("systemInfo.copyFailed")
|
||||
: t("systemInfo.copy")}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -563,6 +584,11 @@ export const SystemInfoPanel = ({ active = true }: { active?: boolean }) => {
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
{copyState !== "idle" ? (
|
||||
<ClipboardCopyNotice status={copyState}>
|
||||
{t(copyState === "copied" ? "systemInfo.copySucceeded" : "systemInfo.copyFailed")}
|
||||
</ClipboardCopyNotice>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1225,6 +1225,8 @@ export const enUS = {
|
||||
browserMode: "Browser",
|
||||
unknown: "Unknown",
|
||||
copy: "Copy info",
|
||||
copySucceeded: "System information copied to the clipboard.",
|
||||
copyFailed: "Copy failed. Check clipboard permissions and try again.",
|
||||
clientAheadOfInstanceByPlatform: {
|
||||
cloudflare: "This client is newer than the connected cloud instance. You can wait for the daily automatic instance update, or run the Update deployed EdgeEver workflow.",
|
||||
docker: "This client is newer than the connected cloud instance. You can wait for the daily automatic instance update, or run ./update.sh in the install directory (default ~/edgeever).",
|
||||
|
||||
@@ -1225,6 +1225,8 @@ export const ja = {
|
||||
browserMode: "ブラウザ",
|
||||
unknown: "不明",
|
||||
copy: "情報をコピー",
|
||||
copySucceeded: "システム情報をクリップボードにコピーしました。",
|
||||
copyFailed: "コピーに失敗しました。クリップボードの権限を確認して、もう一度お試しください。",
|
||||
clientAheadOfInstanceByPlatform: {
|
||||
cloudflare: "このクライアントは接続中のクラウドインスタンスより新しいです。毎日の自動インスタンス更新を待つか、Update deployed EdgeEver ワークフローを実行できます。",
|
||||
docker: "このクライアントは接続中のクラウドインスタンスより新しいです。毎日の自動インスタンス更新を待つか、インストールディレクトリで ./update.sh を実行できます(既定は ~/edgeever)。",
|
||||
|
||||
@@ -1225,6 +1225,8 @@ export const zhCN = {
|
||||
browserMode: "浏览器",
|
||||
unknown: "未知",
|
||||
copy: "复制信息",
|
||||
copySucceeded: "系统信息已复制到剪贴板。",
|
||||
copyFailed: "复制失败,请检查剪贴板权限后重试。",
|
||||
clientAheadOfInstanceByPlatform: {
|
||||
cloudflare: "当前客户端版本高于云端实例。可等待每天自动更新,或手动运行 Update deployed EdgeEver 工作流。",
|
||||
docker: "当前客户端版本高于云端实例。可等待每天自动更新,或在安装目录执行 ./update.sh(默认 ~/edgeever)。",
|
||||
|
||||
Reference in new issue
Block a user