import { useCallback, useEffect, useMemo, useState } from "react"; import { RefreshCw, RotateCcw, ScanSearch, Trash2 } from "lucide-react"; import { ActionBlockerHint, Button, ConfirmDialog, Dialog, DismissibleAlert, DocumentationHelpLink, SegmentedControl, StatusBadge, type ApiSettings, } from "@govoplan/core-webui"; import { listCalendarOutbox, recoverCalendarOutboxOperation, type CalendarCollection, type CalendarOutboxOperation, type CalendarSyncSource, } from "../../api/calendar"; import { dateTimeLabel, errorText } from "./calendarViewModel"; import { CALENDAR_I18N, CALENDAR_RECOVERY_DOCUMENTATION, } from "./interfacePatterns"; type OutboxFilter = "unresolved" | "all"; type RecoveryAction = "retry" | "reconcile" | "discard"; const RESOLVED_STATUSES = new Set(["succeeded", "superseded", "cancelled"]); export function CalendarOutboxDialog({ settings, calendar, source, onClose, }: { settings: ApiSettings; calendar: CalendarCollection; source: CalendarSyncSource; onClose: () => void; }) { const [operations, setOperations] = useState([]); const [filter, setFilter] = useState("unresolved"); const [loading, setLoading] = useState(true); const [busyOperationId, setBusyOperationId] = useState(""); const [error, setError] = useState(""); const [discardOperation, setDiscardOperation] = useState(null); const load = useCallback(async () => { setLoading(true); setError(""); try { const response = await listCalendarOutbox(settings, { source_id: source.id, limit: 100, }); setOperations(response.operations); } catch (err) { setError(errorText(err)); } finally { setLoading(false); } }, [settings, source.id]); useEffect(() => { void load(); }, [load]); const visibleOperations = useMemo( () => filter === "all" ? operations : operations.filter((operation) => !RESOLVED_STATUSES.has(operation.status)), [filter, operations], ); const unresolvedCount = operations.filter((operation) => !RESOLVED_STATUSES.has(operation.status)).length; const conflictCount = operations.filter((operation) => ["conflict", "dead"].includes(operation.status)).length; async function recover(operation: CalendarOutboxOperation, action: RecoveryAction) { setBusyOperationId(operation.id); setError(""); try { await recoverCalendarOutboxOperation(settings, operation.id, action); setDiscardOperation(null); await load(); } catch (err) { setError(errorText(err)); } finally { setBusyOperationId(""); } } return ( <> } >

{calendar.name}

{error && {error}}
value={filter} ariaLabel="i18n:govoplan-calendar.outbox_filter.8305af40" onChange={setFilter} options={[ { id: "unresolved", label: "i18n:govoplan-calendar.unresolved.11c41de4" }, { id: "all", label: "i18n:govoplan-calendar.all_history.8829c44e" }, ]} />
i18n:govoplan-calendar.unresolved.11c41de4
{unresolvedCount}
i18n:govoplan-calendar.conflicts_dead.31252c3a
{conflictCount}
i18n:govoplan-calendar.shown.498e85a1
{visibleOperations.length}
{loading && !operations.length ?

i18n:govoplan-calendar.loading_outbound_changes.3fc59656

: visibleOperations.length ? (
    {visibleOperations.map((operation) => (
  1. {operation.operation_kind.toUpperCase()}
    {operation.resource_href}
    i18n:govoplan-calendar.attempts.448fa12e
    {operation.attempt_count}/{operation.max_attempts}
    i18n:govoplan-calendar.available.17bc146b
    {dateTimeLabel(new Date(operation.available_at))}
    {operation.last_error &&

    {operation.last_error}

    } action === "discard" ? setDiscardOperation(operation) : void recover(operation, action)} />
  2. ))}
) :

i18n:govoplan-calendar.no_outbound_changes_match_this_filter.43d3aa64

} {operations.length === 100 && (

i18n:govoplan-calendar.only_the_100_most_recent_outbound_changes_are_shown.94bd8365

)}
setDiscardOperation(null)} onConfirm={() => discardOperation && void recover(discardOperation, "discard")} /> ); } function RecoveryActions({ operation, busy, onRecover, }: { operation: CalendarOutboxOperation; busy: boolean; onRecover: (action: RecoveryAction) => void; }) { const available = (["retry", "reconcile", "discard"] as const).filter( (action) => operation.actions[action]?.allowed, ); const unavailableReason = Object.values(operation.actions).find((action) => action.reason)?.reason; return (
{available.length > 0 && (
{available.includes("retry") && ( )} {available.includes("reconcile") && ( )} {available.includes("discard") && ( )}
)} {!available.length && unavailableReason && ( )}
); }