From 2b26fe1efab18962ad9f0710ef021b743d67ee16 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Mon, 2 Nov 2026 11:26:48 +0700 Subject: [PATCH] fix(jobs): a failed clone's chip is readable and clickable A failed or stopped clone also raised a toast on the same corner, a layer above the dock, so it covered the chip's x / Retry / Stop and repeated the same long message; the chip now says it alone, and every toast stacks above the dock (--jobs-dock-h). A URL with no space to break at (user@host:/PATH) wraps inside the chip and inside a toast instead of running off the screen. Open Settings -> Git is a router link; a plain reloaded the app. --- client/src/components/BackgroundJobsDock.tsx | 35 +++++++++++++++---- client/src/components/Toaster.tsx | 8 +++-- .../__tests__/BackgroundJobsDock.test.tsx | 31 ++++++++++++++-- client/src/lib/cloneJobs.ts | 11 ++---- 4 files changed, 66 insertions(+), 19 deletions(-) diff --git a/client/src/components/BackgroundJobsDock.tsx b/client/src/components/BackgroundJobsDock.tsx index bf5a89be..e2a323e3 100644 --- a/client/src/components/BackgroundJobsDock.tsx +++ b/client/src/components/BackgroundJobsDock.tsx @@ -13,9 +13,9 @@ * @author Nguyễn Ngọc Trí Vĩ */ -import { useEffect, useState, type ReactNode } from "react"; +import { useEffect, useRef, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { ChevronDown, FolderGit2, Square, Upload, X } from "lucide-react"; import type { CloneJob } from "../lib/api"; import { @@ -156,15 +156,16 @@ function CloneJobChip({ job }: { job: CloneJob }) { <>

{cloneJobError(job)}{" "} {code === "ESSHAUTH" && ( - + // A router link: a plain reloaded the whole app. + {t("clone.sshSettings")} - + )}

{code === "ECREDENTIALS" && job.state === "failed" ? ( @@ -226,7 +227,7 @@ function CloneJobChip({ job }: { job: CloneJob }) { ) )} {error && ( -

+

{error}

)} @@ -244,7 +245,7 @@ function UploadChip({ job }: { job: UploadJob }) {
-

+

{t("uploads.interrupted", { total: job.total, folder: job.folder, @@ -305,15 +306,35 @@ export function BackgroundJobsDock() { const jobs = useCloneJobs(); const uploads = useUploadJobs(); const [collapsed, setCollapsed] = useState(false); + const dockRef = useRef(null); useEffect(() => ensureCloneJobs(), []); const count = jobs.length + uploads.length; + // Toasts stack ABOVE the dock (Toaster reads --jobs-dock-h): they used to + // sit on the same corner and cover a chip's × / Retry / Stop. + useEffect(() => { + const root = document.documentElement; + const el = dockRef.current; + if (!count || !el) { + root.style.removeProperty("--jobs-dock-h"); + return; + } + const set = () => root.style.setProperty("--jobs-dock-h", `${el.offsetHeight + 8}px`); + set(); + const ro = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(set); + ro?.observe(el); + return () => { + ro?.disconnect(); + root.style.removeProperty("--jobs-dock-h"); + }; + }, [count, collapsed]); if (!count) return null; const running = jobs.filter(isActiveJob).length + uploads.filter((u) => u.state !== "interrupted").length; return (

- {item.message} + + {item.message} +