fix few issues

This commit is contained in:
Mahesh Sanikommu 2026-05-11 23:00:39 -07:00
parent f93d3f0c27
commit e52e43d073
6 changed files with 393 additions and 250 deletions

View file

@ -33,7 +33,7 @@ import {
} from "@ui/components/dropdown-menu"
import { RemoveConnectionDialog } from "@/components/remove-connection-dialog"
import { SyncStatusBadge } from "@/components/settings/sync-status-badge"
import { SyncHistorySheet } from "@/components/settings/sync-history-sheet"
import { SyncHistoryPanel } from "@/components/settings/sync-history-panel"
import { useTriggerSync } from "@/hooks/use-trigger-sync"
import { formatRelativeTime } from "@/components/settings/sync-utils"
import type { ImportProvider } from "@/components/settings/sync-utils"
@ -101,7 +101,6 @@ function ConnectionRow({
projects,
onTriggerSync,
isSyncing,
onViewHistory,
}: {
connection: Connection
onDelete: () => void
@ -109,8 +108,8 @@ function ConnectionRow({
projects: Project[]
onTriggerSync: () => void
isSyncing: boolean
onViewHistory: () => void
}) {
const [historyOpen, setHistoryOpen] = useState(false)
const config = CONNECTORS[connection.provider as ConnectorProvider]
if (!config) return null
@ -196,13 +195,25 @@ function ConnectionRow({
type="button"
onClick={(e) => {
e.stopPropagation()
onViewHistory()
setHistoryOpen((v) => !v)
}}
className="text-[#737373] hover:text-[#FAFAFA] transition-colors disabled:opacity-50 disabled:cursor-not-allowed p-1.5 rounded-lg hover:bg-white/5"
aria-label="Sync history"
title="Sync history"
aria-expanded={historyOpen}
title={historyOpen ? "Hide sync history" : "Sync history"}
className={cn(
"transition-colors disabled:opacity-50 disabled:cursor-not-allowed p-1.5 rounded-lg hover:bg-white/5 flex items-center gap-0.5",
historyOpen
? "text-[#FAFAFA] bg-white/5"
: "text-[#737373] hover:text-[#FAFAFA]",
)}
>
<History className="size-[18px]" />
<ChevronDown
className={cn(
"size-3 transition-transform",
historyOpen && "rotate-180",
)}
/>
</button>
<button
type="button"
@ -253,6 +264,15 @@ function ConnectionRow({
</span>
</div>
</div>
{historyOpen && (
<div className="border-t border-[rgba(82,89,102,0.12)] pt-3">
<SyncHistoryPanel
connectionId={connection.id}
isOpen={historyOpen}
/>
</div>
)}
</div>
</div>
)
@ -276,10 +296,6 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
connection: Connection | null
}>({ open: false, connection: null })
const triggerSync = useTriggerSync()
const [syncHistorySheet, setSyncHistorySheet] = useState<{
open: boolean
connection: Connection | null
}>({ open: false, connection: null })
const projects = (queryClient.getQueryData<Project[]>(["projects"]) ||
[]) as Project[]
@ -706,9 +722,6 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
triggerSync.variables?.connectionId === connection.id) ||
getConnectionMeta(connection).syncInProgress
}
onViewHistory={() =>
setSyncHistorySheet({ open: true, connection })
}
/>
))}
</div>
@ -799,13 +812,6 @@ export function ConnectContent({ selectedProject }: ConnectContentProps) {
}}
isDeleting={deleteConnectionMutation.isPending}
/>
<SyncHistorySheet
open={syncHistorySheet.open}
onOpenChange={(open) => {
if (!open) setSyncHistorySheet({ open: false, connection: null })
}}
connection={syncHistorySheet.connection}
/>
</div>
)
}

View file

@ -7,7 +7,16 @@ import { hasActivePlan } from "@lib/queries"
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
import { useCustomer } from "autumn-js/react"
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { Check, History, Loader2, Play, Plus, Trash2, Zap } from "lucide-react"
import {
Check,
ChevronDown,
History,
Loader2,
Play,
Plus,
Trash2,
Zap,
} from "lucide-react"
import { useEffect, useState } from "react"
import { toast } from "sonner"
import { useQueryState } from "nuqs"
@ -21,7 +30,7 @@ import { addDocumentParam } from "@/lib/search-params"
import { DEFAULT_PROJECT_ID } from "@lib/constants"
import type { Project } from "@lib/types"
import { SyncStatusBadge } from "@/components/settings/sync-status-badge"
import { SyncHistorySheet } from "@/components/settings/sync-history-sheet"
import { SyncHistoryPanel } from "@/components/settings/sync-history-panel"
import { useTriggerSync } from "@/hooks/use-trigger-sync"
import { formatRelativeTime } from "@/components/settings/sync-utils"
import type { ImportProvider } from "@/components/settings/sync-utils"
@ -157,7 +166,6 @@ function ConnectionRow({
projects,
onTriggerSync,
isSyncing,
onViewHistory,
}: {
connection: Connection
onDelete: () => void
@ -166,8 +174,8 @@ function ConnectionRow({
projects: Project[]
onTriggerSync: () => void
isSyncing: boolean
onViewHistory: () => void
}) {
const [historyOpen, setHistoryOpen] = useState(false)
const config = CONNECTORS[connection.provider as ConnectorProvider]
if (!config) return null
@ -259,14 +267,26 @@ function ConnectionRow({
type="button"
onClick={(e) => {
e.stopPropagation()
onViewHistory()
setHistoryOpen((v) => !v)
}}
disabled={disabled}
className="text-[#737373] hover:text-[#FAFAFA] transition-colors disabled:opacity-50 disabled:cursor-not-allowed p-1.5 rounded-lg hover:bg-white/5"
aria-label="Sync history"
title="Sync history"
aria-expanded={historyOpen}
title={historyOpen ? "Hide sync history" : "Sync history"}
className={cn(
"transition-colors disabled:opacity-50 disabled:cursor-not-allowed p-1.5 rounded-lg hover:bg-white/5 flex items-center gap-0.5",
historyOpen
? "text-[#FAFAFA] bg-white/5"
: "text-[#737373] hover:text-[#FAFAFA]",
)}
>
<History className="size-[18px]" />
<ChevronDown
className={cn(
"size-3 transition-transform",
historyOpen && "rotate-180",
)}
/>
</button>
<button
type="button"
@ -314,6 +334,15 @@ function ConnectionRow({
{meta.documentCount} {config.documentLabel} connected
</span>
</div>
{historyOpen && (
<div className="border-t border-[rgba(82,89,102,0.15)] pt-4">
<SyncHistoryPanel
connectionId={connection.id}
isOpen={historyOpen}
/>
</div>
)}
</div>
</div>
)
@ -378,10 +407,6 @@ export default function ConnectionsMCP() {
connection: Connection | null
}>({ open: false, connection: null })
const triggerSync = useTriggerSync()
const [syncHistorySheet, setSyncHistorySheet] = useState<{
open: boolean
connection: Connection | null
}>({ open: false, connection: null })
const projects = (queryClient.getQueryData<Project[]>(["projects"]) ||
[]) as Project[]
@ -555,9 +580,6 @@ export default function ConnectionsMCP() {
connection.id) ||
getConnectionMeta(connection).syncInProgress
}
onViewHistory={() =>
setSyncHistorySheet({ open: true, connection })
}
/>
))
) : (
@ -658,14 +680,6 @@ export default function ConnectionsMCP() {
}}
isDeleting={deleteConnectionMutation.isPending}
/>
<SyncHistorySheet
open={syncHistorySheet.open}
onOpenChange={(open) => {
if (!open) setSyncHistorySheet({ open: false, connection: null })
}}
connection={syncHistorySheet.connection}
/>
</div>
)
}

View file

@ -0,0 +1,267 @@
"use client"
import { cn } from "@lib/utils"
import { dmSans125ClassName } from "@/lib/fonts"
import { useSyncRuns } from "@/hooks/use-sync-runs"
import type { SyncRun } from "@/hooks/use-sync-runs"
import {
formatRelativeTime,
TRIGGER_TYPE_LABELS,
} from "@/components/settings/sync-utils"
const STATUS_COLORS: Record<string, { dot: string; text: string }> = {
completed: { dot: "bg-[#00AC3F]", text: "text-[#00AC3F]" },
failed: { dot: "bg-[#EF4444]", text: "text-[#EF4444]" },
running: { dot: "bg-[#4BA0FA] animate-pulse", text: "text-[#4BA0FA]" },
}
function pluralize(count: number, noun: string) {
return `${count} ${noun}${count === 1 ? "" : "s"}`
}
/** Calendar-day bucket label for grouping runs in the timeline. */
function dayLabel(date: string) {
const d = new Date(date)
if (Number.isNaN(d.getTime())) return "Unknown"
const today = new Date()
const startOfDay = (x: Date) =>
new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime()
const diffDays = Math.round((startOfDay(today) - startOfDay(d)) / 86_400_000)
if (diffDays <= 0) return "Today"
if (diffDays === 1) return "Yesterday"
if (diffDays < 7) return `${diffDays} days ago`
return d.toLocaleDateString(undefined, { month: "short", day: "numeric" })
}
function StatTile({ value, label }: { value: string; label: string }) {
return (
<div className="flex-1 bg-[#0D0F14] rounded-[10px] border border-[rgba(82,89,102,0.2)] px-3 py-2 flex flex-col gap-0.5">
<span
className={cn(
dmSans125ClassName(),
"text-[15px] font-medium text-[#FAFAFA] tabular-nums",
)}
>
{value}
</span>
<span className={cn(dmSans125ClassName(), "text-[11px] text-[#737373]")}>
{label}
</span>
</div>
)
}
function SummaryStats({ runs }: { runs: SyncRun[] }) {
const totalItems = runs.reduce((sum, r) => sum + r.itemsProcessed, 0)
const finished = runs.filter((r) => r.status !== "running")
const succeeded = finished.filter((r) => r.status === "completed").length
const successRate =
finished.length > 0 ? Math.round((succeeded / finished.length) * 100) : null
return (
<div className="flex items-stretch gap-2">
<StatTile
value={String(runs.length)}
label={runs.length === 1 ? "sync" : "syncs"}
/>
<StatTile
value={String(totalItems)}
label={totalItems === 1 ? "item" : "items"}
/>
<StatTile
value={successRate === null ? "—" : `${successRate}%`}
label="success"
/>
</div>
)
}
function TimelineRow({ run, isLast }: { run: SyncRun; isLast: boolean }) {
const colors = STATUS_COLORS[run.status] ?? {
dot: "bg-[#4BA0FA] animate-pulse",
text: "text-[#4BA0FA]",
}
const triggerLabel = TRIGGER_TYPE_LABELS[run.triggerType] ?? run.triggerType
const statusLabel = run.status.charAt(0).toUpperCase() + run.status.slice(1)
return (
<div className="flex gap-2.5">
{/* Timeline rail: dot + connecting line */}
<div className="flex flex-col items-center pt-1.5">
<div className={cn("size-[7px] rounded-full shrink-0", colors.dot)} />
{!isLast && <div className="w-px flex-1 bg-[#1A1D24] mt-1" />}
</div>
{/* Content */}
<div className={cn("flex-1 min-w-0", isLast ? "pb-0" : "pb-3")}>
<div className="flex items-baseline justify-between gap-2">
<div className="flex items-baseline gap-1.5 min-w-0">
<span
className={cn(
dmSans125ClassName(),
"text-[13px] font-medium shrink-0",
colors.text,
)}
>
{statusLabel}
</span>
<span
className={cn(
dmSans125ClassName(),
"text-[12px] text-[#737373] truncate",
)}
>
· {triggerLabel}
</span>
</div>
<span
className={cn(
dmSans125ClassName(),
"text-[12px] text-[#737373] shrink-0",
)}
>
{formatRelativeTime(run.startedAt)}
</span>
</div>
{(run.itemsProcessed > 0 || run.itemsFailed > 0) && (
<div
className={cn(
dmSans125ClassName(),
"text-[12px] text-[#737373] mt-0.5",
)}
>
{pluralize(run.itemsProcessed, "item")} processed
{run.itemsFailed > 0 && (
<span className="text-[#EF4444]">
{" "}
· {run.itemsFailed} failed
</span>
)}
</div>
)}
{run.error && (
<p
className={cn(
dmSans125ClassName(),
"text-[12px] text-[#EF4444]/80 break-words line-clamp-3 mt-0.5",
)}
>
{run.error}
</p>
)}
</div>
</div>
)
}
function Timeline({ runs }: { runs: SyncRun[] }) {
// Group consecutive runs by calendar-day label, preserving server order.
const groups: { label: string; runs: SyncRun[] }[] = []
for (const run of runs) {
const label = dayLabel(run.startedAt)
const last = groups.at(-1)
if (last && last.label === label) {
last.runs.push(run)
} else {
groups.push({ label, runs: [run] })
}
}
return (
<div className="flex flex-col gap-3">
{groups.map((group, gi) => (
<div key={`${group.label}-${gi}`} className="flex flex-col gap-1.5">
<span
className={cn(
dmSans125ClassName(),
"text-[10px] uppercase tracking-wide text-[#525966]",
)}
>
{group.label}
</span>
<div className="flex flex-col">
{group.runs.map((run, i) => (
<TimelineRow
key={run.id}
run={run}
isLast={i === group.runs.length - 1}
/>
))}
</div>
</div>
))}
</div>
)
}
interface SyncHistoryPanelProps {
connectionId: string
/** Only fetch / render when expanded. */
isOpen: boolean
}
/** Inline sync-history view (stats strip + timeline) rendered inside an expanded connection row. */
export function SyncHistoryPanel({
connectionId,
isOpen,
}: SyncHistoryPanelProps) {
const {
data: syncRuns,
isLoading,
error,
refetch,
} = useSyncRuns(isOpen ? connectionId : "")
if (!isOpen) return null
const hasRuns = !isLoading && !error && syncRuns && syncRuns.length > 0
return (
<div className="flex flex-col gap-3">
{isLoading && (
<div className="flex items-center justify-center py-6">
<div className="size-5 border-2 border-[#737373] border-t-transparent rounded-full animate-spin" />
</div>
)}
{error && !isLoading && (
<div className="flex items-center justify-center gap-2 py-6">
<span
className={cn(dmSans125ClassName(), "text-[13px] text-[#737373]")}
>
Failed to load sync history
</span>
<button
type="button"
onClick={() => refetch()}
className={cn(
dmSans125ClassName(),
"text-[13px] text-[#4BA0FA] hover:text-[#4BA0FA]/80 underline cursor-pointer",
)}
>
Try again
</button>
</div>
)}
{!isLoading && !error && syncRuns && syncRuns.length === 0 && (
<div className="flex items-center justify-center py-6">
<span
className={cn(dmSans125ClassName(), "text-[13px] text-[#737373]")}
>
No syncs yet runs will appear here.
</span>
</div>
)}
{hasRuns && (
<>
<SummaryStats runs={syncRuns} />
<Timeline runs={syncRuns} />
</>
)}
</div>
)
}

View file

@ -1,192 +0,0 @@
"use client"
import { cn } from "@lib/utils"
import { dmSans125ClassName } from "@/lib/fonts"
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@ui/components/sheet"
import { useSyncRuns } from "@/hooks/use-sync-runs"
import type { SyncRun } from "@/hooks/use-sync-runs"
import {
formatRelativeTime,
TRIGGER_TYPE_LABELS,
PROVIDER_DISPLAY_NAMES,
} from "@/components/settings/sync-utils"
import type { ConnectionResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
type Connection = z.infer<typeof ConnectionResponseSchema>
const STATUS_COLORS: Record<string, { dot: string; text: string }> = {
completed: { dot: "bg-[#00AC3F]", text: "text-[#00AC3F]" },
failed: { dot: "bg-[#EF4444]", text: "text-[#EF4444]" },
running: { dot: "bg-[#4BA0FA] animate-pulse", text: "text-[#4BA0FA]" },
}
function SyncRunCard({ run }: { run: SyncRun }) {
const colors = STATUS_COLORS[run.status] ?? {
dot: "bg-[#4BA0FA] animate-pulse",
text: "text-[#4BA0FA]",
}
return (
<div className="bg-[#14161A] rounded-[12px] p-4 border border-[rgba(82,89,102,0.2)] flex flex-col gap-2">
{/* Top row: status + trigger + time */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className={cn("size-[7px] rounded-full", colors.dot)} />
<span
className={cn(
dmSans125ClassName(),
"font-medium text-[14px]",
colors.text,
)}
>
{run.status.charAt(0).toUpperCase() + run.status.slice(1)}
</span>
<span
className={cn(
dmSans125ClassName(),
"text-[12px] text-[#737373] bg-[#1A1D24] rounded-full px-2 py-0.5",
)}
>
{TRIGGER_TYPE_LABELS[run.triggerType] ?? run.triggerType}
</span>
</div>
<span
className={cn(dmSans125ClassName(), "text-[12px] text-[#737373]")}
>
{formatRelativeTime(run.startedAt)}
</span>
</div>
{/* Middle row: item counts */}
{(run.itemsProcessed > 0 || run.itemsFailed > 0) && (
<div
className={cn(
dmSans125ClassName(),
"flex items-center gap-1 text-[13px]",
)}
>
<span className="text-[#737373]">{run.itemsProcessed} processed</span>
{run.itemsFailed > 0 && (
<span className="text-[#EF4444]">· {run.itemsFailed} failed</span>
)}
</div>
)}
{/* Bottom row: error message */}
{run.error && (
<p
className={cn(
dmSans125ClassName(),
"text-[13px] text-[#EF4444]/80 break-words line-clamp-3",
)}
>
{run.error}
</p>
)}
</div>
)
}
interface SyncHistorySheetProps {
open: boolean
onOpenChange: (open: boolean) => void
connection: Connection | null
}
export function SyncHistorySheet({
open,
onOpenChange,
connection,
}: SyncHistorySheetProps) {
const {
data: syncRuns,
isLoading,
error,
refetch,
} = useSyncRuns(open && connection ? connection.id : "")
const providerTitle = connection
? (PROVIDER_DISPLAY_NAMES[connection.provider] ?? connection.provider)
: ""
const email = connection?.email ?? ""
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
side="right"
className="bg-[#0D0F14] border-l border-[#1A1D24] w-full max-w-[400px] gap-0 overflow-hidden p-0 [&>button]:text-[#737373] [&>button]:hover:text-[#FAFAFA]"
>
<SheetHeader className="px-6 pt-6 pb-4 border-b border-[#1A1D24]">
<SheetTitle
className={cn(dmSans125ClassName(), "text-[#FAFAFA] text-[18px]")}
>
Sync History
</SheetTitle>
<SheetDescription
className={cn(dmSans125ClassName(), "text-[#737373] text-[14px]")}
>
{providerTitle}
{email ? ` · ${email}` : ""}
</SheetDescription>
</SheetHeader>
<div className="px-6 py-4 overflow-y-auto flex-1 flex flex-col gap-3">
{isLoading && (
<div className="flex items-center justify-center py-12">
<div className="size-6 border-2 border-[#737373] border-t-transparent rounded-full animate-spin" />
</div>
)}
{error && !isLoading && (
<div className="flex flex-col items-center justify-center py-12 gap-2">
<p
className={cn(
dmSans125ClassName(),
"text-[14px] text-[#737373]",
)}
>
Failed to load sync history
</p>
<button
type="button"
onClick={() => refetch()}
className={cn(
dmSans125ClassName(),
"text-[14px] text-[#4BA0FA] hover:text-[#4BA0FA]/80 underline cursor-pointer",
)}
>
Try again
</button>
</div>
)}
{!isLoading && !error && syncRuns && syncRuns.length === 0 && (
<div className="flex items-center justify-center py-12">
<p
className={cn(
dmSans125ClassName(),
"text-[14px] text-[#737373]",
)}
>
No sync runs yet
</p>
</div>
)}
{!isLoading &&
!error &&
syncRuns &&
syncRuns.length > 0 &&
syncRuns.map((run) => <SyncRunCard key={run.id} run={run} />)}
</div>
</SheetContent>
</Sheet>
)
}

View file

@ -29,10 +29,10 @@ export function SyncStatusBadge({
const status = deriveStatus(syncInProgress, lastSyncedAt, isExpired)
return (
<div className={cn("flex items-center gap-2", className)}>
<div className={cn("flex items-center gap-1.5", className)}>
<div
className={cn(
"size-[7px] rounded-full",
"size-[6px] rounded-full",
status === "syncing" && "bg-[#4BA0FA] animate-pulse",
status === "synced" && "bg-[#00AC3F]",
status === "expired" && "bg-[#EF4444]",
@ -43,7 +43,7 @@ export function SyncStatusBadge({
<span
className={cn(
dmSans125ClassName(),
"font-medium text-[16px] tracking-[-0.16px] text-[#4BA0FA]",
"font-medium text-[13px] tracking-[-0.13px] text-[#4BA0FA]",
)}
>
Syncing...
@ -54,7 +54,7 @@ export function SyncStatusBadge({
<span
className={cn(
dmSans125ClassName(),
"font-medium text-[16px] tracking-[-0.16px] text-[#00AC3F]",
"font-medium text-[13px] tracking-[-0.13px] text-[#00AC3F]",
)}
>
Synced
@ -63,7 +63,7 @@ export function SyncStatusBadge({
<span
className={cn(
dmSans125ClassName(),
"font-medium text-[14px] tracking-[-0.14px] text-[#737373]",
"font-medium text-[12px] tracking-[-0.12px] text-[#737373]",
)}
>
{formatRelativeTime(lastSyncedAt)}
@ -74,7 +74,7 @@ export function SyncStatusBadge({
<span
className={cn(
dmSans125ClassName(),
"font-medium text-[16px] tracking-[-0.16px] text-[#EF4444]",
"font-medium text-[13px] tracking-[-0.13px] text-[#EF4444]",
)}
>
Disconnected
@ -84,7 +84,7 @@ export function SyncStatusBadge({
<span
className={cn(
dmSans125ClassName(),
"font-medium text-[16px] tracking-[-0.16px] text-[#737373]",
"font-medium text-[13px] tracking-[-0.13px] text-[#737373]",
)}
>
Waiting for first sync

View file

@ -3,7 +3,12 @@
import { $fetch } from "@lib/api"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { toast } from "sonner"
import type { ConnectionResponseSchema } from "@repo/validation/api"
import type { z } from "zod"
import type { ImportProvider } from "@/components/settings/sync-utils"
import type { SyncRun } from "@/hooks/use-sync-runs"
type Connection = z.infer<typeof ConnectionResponseSchema>
export function useTriggerSync() {
const queryClient = useQueryClient()
@ -13,9 +18,7 @@ export function useTriggerSync() {
provider,
containerTags,
}: {
// connectionId is not sent to the backend — the import endpoint is keyed
// by provider, so it re-syncs all connections for that provider.
// It's kept here so onSuccess can target cache invalidation.
// connectionId isn't sent to the backend (import is keyed by provider) — kept for cache updates
connectionId: string
provider: ImportProvider
containerTags?: string[]
@ -32,15 +35,60 @@ export function useTriggerSync() {
}
return response.data
},
onSuccess: (_data, variables) => {
// Optimistically flip to "syncing" so the badge/button update instantly; the 5s poll then converges on real state
onMutate: async (variables) => {
await queryClient.cancelQueries({ queryKey: ["connections"] })
const previousConnections = queryClient.getQueryData<Connection[]>([
"connections",
])
queryClient.setQueryData<Connection[]>(["connections"], (old) =>
old?.map((c) =>
c.provider === variables.provider
? {
...c,
metadata: {
...((c.metadata as Record<string, unknown> | null) ?? {}),
syncInProgress: true,
},
}
: c,
),
)
const syncRunsKey = ["sync-runs", variables.connectionId]
const previousSyncRuns = queryClient.getQueryData<SyncRun[]>(syncRunsKey)
if (previousSyncRuns) {
const optimisticRun: SyncRun = {
id: `optimistic-${Date.now()}`,
connectionId: variables.connectionId,
status: "running",
triggerType: "manual",
startedAt: new Date().toISOString(),
completedAt: null,
itemsProcessed: 0,
itemsFailed: 0,
error: null,
}
queryClient.setQueryData<SyncRun[]>(syncRunsKey, [
optimisticRun,
...previousSyncRuns,
])
}
return { previousConnections, previousSyncRuns, syncRunsKey }
},
// Don't invalidate connections/sync-runs here — an immediate refetch races the backend and clobbers the optimistic state; the 5s polls handle it
onSuccess: () => {
toast.success("Sync started")
queryClient.invalidateQueries({ queryKey: ["connections"] })
queryClient.invalidateQueries({
queryKey: ["sync-runs", variables.connectionId],
})
queryClient.invalidateQueries({ queryKey: ["processing-documents"] })
},
onError: (error) => {
onError: (error, _variables, context) => {
if (context?.previousConnections !== undefined) {
queryClient.setQueryData(["connections"], context.previousConnections)
}
if (context?.previousSyncRuns !== undefined) {
queryClient.setQueryData(context.syncRunsKey, context.previousSyncRuns)
}
toast.error("Failed to start sync", {
description: error instanceof Error ? error.message : "Unknown error",
})