import { WriteHint } from "./freshness.tsx"; import { useEffect, useId, useRef, useState, type ReactNode } from "react"; import { Badge, Banner, Button, Dialog, Textarea, Tooltip } from "@cloudflare/kumo"; import { Warning } from "@phosphor-icons/react"; import type { BlockerView, WireBlocker } from "./contracts.ts"; import { blockedLabel, blockerPacket, collisionHistory } from "./model.ts"; import { relativeTime } from "./time.ts"; import { ApiError } from "./transport.ts"; export function CollisionSentence({ blocker: b }: { blocker: BlockerView }) { return ( {b.pushingAgent ?? b.agent} pushed {b.sha?.slice(0, 7)} to{" "} {b.paths[0] && ( }> {b.paths[0]} )} {b.paths.length > 1 && <>+{b.paths.length - 1}}, {" "} which {b.claimingAgent} has claimed for {b.packet}. ); } export function MarkResolved({ blocker, owner, open, disabled, disabledReason, }: { blocker: BlockerView; owner: boolean; open?: (blocker: BlockerView) => void; disabled?: boolean; disabledReason?: string; }) { if (!owner || !open || blocker.resolved) return null; return ( ); } export function CollisionBanner({ blocker, owner, open, showPacket, disabled, disabledReason, }: { blocker: BlockerView; owner: boolean; open?: (blocker: BlockerView) => void; showPacket(id: string): void; disabled?: boolean; disabledReason?: string; }) { return ( {blockedLabel(blocker)} is blocked until it's resolved. } action={
} /> ); } export function CollisionRows({ blockers, owner, open, disabled, disabledReason, highlighted, reviewLink, }: { blockers: BlockerView[]; owner: boolean; open?: (blocker: BlockerView) => void; disabled?: boolean; disabledReason?: string; highlighted?: string; reviewLink?(blocker: BlockerView): ReactNode; }) { const root = useRef(null); useEffect(() => { if (highlighted) root.current?.querySelector(".collision-highlight button")?.focus(); }, [highlighted]); const ordered = collisionHistory(blockers); const newestResolved = ordered.find((b) => b.resolved); const focused = ordered.find((b) => b.id === highlighted); return (
{(focused ? [focused, ...ordered.filter((b) => b !== focused)] : ordered).map((b) => { const resolution = b.resolution; return (

{b.resolved ? "Resolved" : "Open"} {" · "} {b.resolved && resolution ? ( <> {relativeTime(resolution.at)} ·{" "} {resolution.by === "owner" ? owner ? "by you" : "by the owner" : resolution.by === "promote" ? "when a claim ended in a promote" : "when a claim was released"} ) : b.at ? ( <>since {relativeTime(b.at)} ) : ( <> pushed {b.sha?.slice(0, 7)} )}

{resolution?.by === "owner" && resolution.reason !== undefined && (

“{resolution.reason}”

)} {b.id === newestResolved?.id && reviewLink?.(b)}
); })}
); } /** Kumo Dialog supplies focus trapping and Escape cancellation; errors never render raw API copy. */ export function ResolveCollisionDialog({ blocker, resolve, close, disabled, disabledReason, }: { disabled?: boolean; disabledReason?: string; blocker: BlockerView; resolve(id: string, reason: string): Promise; close(resolved?: boolean): void; }) { const id = useId(); const firstReason = useRef(null); const textarea = useRef(null); const closeButton = useRef(null); const [reason, setReason] = useState(""); const [sending, setSending] = useState(false); const [error, setError] = useState(); const [missing, setMissing] = useState(false); useEffect(() => { firstReason.current?.focus(); }, []); useEffect(() => { if (missing) closeButton.current?.focus(); }, [missing]); const submit = async () => { if (disabled || sending || missing || reason.trim().length < 1 || reason.length > 200) return; setSending(true); setError(undefined); try { await resolve(blocker.id, reason); close(true); } catch (e) { const notFound = e instanceof ApiError && e.status === 404; setMissing(notFound); setError( notFound ? "This collision doesn't exist anymore." : "Couldn't mark it resolved. Try again.", ); } finally { setSending(false); } }; const quickReasons = [ "Coordinated: both changes can land.", `${blocker.pushingAgent ?? blocker.agent} will rebase on ${blocker.claimingAgent}'s work before promoting.`, `${blocker.claimingAgent} no longer needs these paths.`, ]; return ( { if (!open && !sending) close(); }} > Mark this collision resolved? }>
{blocker.paths.slice(0, 5).map((path) => (

{path}

))} {blocker.paths.length > 5 && ( }> +{blocker.paths.length - 5} more )}

Marking it resolved unblocks {blockedLabel(blocker)}: its reviews can be approved and promoted again. Nothing in either fork changes.

{quickReasons.map((value, index) => ( ))}