import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; import { Link, useSearch } from "wouter"; import { api, type ChangeEvent, type Connection, type Page } from "../api.ts"; import { LoadError } from "../components/LoadError.tsx"; import { changesFor, usePendingChanges, useSources } from "../hooks.ts"; import { friendlyError, timeAgo } from "../format.ts"; import { changeValue, describeChange, groupByRun, type ChangeAction } from "../labels.ts"; import { useRatingScale } from "../settings.ts"; const TYPE_LABEL: Record = { changed: "Changed", conflict: "Sources disagree", removed: "Gone from source", merged: "Merged at source", added: "Added", }; const DONE_LABEL: Record = { accept: "Done. The new value is in.", keep_mine: "Kept yours. That source won't change it again.", ignore: "Ignored. Nothing changed.", }; /** The Changes inbox: pending source changes, grouped by sync run. `?entry=` narrows it to one entry. */ export function Changes() { const search = new URLSearchParams(useSearch()); const entryId = search.get("entry"); const changes = usePendingChanges(); const sources = useSources(); const engagements = useQuery({ queryKey: ["engagements", entryId], queryFn: () => api.engagements(entryId!), enabled: !!entryId }); const entry = useQuery({ queryKey: ["entry", entryId], queryFn: () => api.entry(entryId!), enabled: !!entryId }); const [said, setSaid] = useState(""); const all = changes.data?.items ?? []; const items = entryId ? changesFor(all, entryId, new Set((engagements.data ?? []).map((g) => g.id))) : all; const groups = groupByRun(items); return (

Changes

When a source changes something you've edited, it waits here. Nothing changes until you choose.

{entryId && (

Showing changes to {entry.data?.work.title ?? "one entry"}. Show all changes

)}

{said}

{changes.error && changes.data && void changes.refetch()} stale />} {changes.isPending ? ( ); } function RunGroup({ events, sources, onResolved }: { events: ChangeEvent[]; sources: Connection[]; onResolved: (s: string) => void }) { const first = events[0]!; const name = sources.find((s) => s.id === first.connectionId)?.name ?? "A source"; return (

{name} sync {timeAgo(first.createdAt)} {events.length} waiting

    {events.map((ev) => ( ))}
); } function ChangeCard({ ev, sources, onResolved }: { ev: ChangeEvent; sources: Connection[]; onResolved: (s: string) => void }) { const qc = useQueryClient(); const scale = useRatingScale(); const d = describeChange(ev, sources); const [error, setError] = useState(null); const key = ["changes", "pending"]; const resolve = useMutation({ mutationFn: (action: ChangeAction) => api.resolveChange(ev.id, action), onMutate: async () => { setError(null); await qc.cancelQueries({ queryKey: key }); const prev = qc.getQueryData>(key); if (prev) qc.setQueryData>(key, { ...prev, items: prev.items.filter((c) => c.id !== ev.id) }); return { prev }; }, onError: (e, _a, ctx) => { if (ctx?.prev) qc.setQueryData(key, ctx.prev); setError(friendlyError(e)); }, onSuccess: (_v, action) => onResolved(DONE_LABEL[action]), onSettled: () => { void qc.invalidateQueries({ queryKey: key }); void qc.invalidateQueries({ queryKey: ["entries"] }); if (ev.targetEntity === "entry" && ev.targetId) { void qc.invalidateQueries({ queryKey: ["entry", ev.targetId] }); void qc.invalidateQueries({ queryKey: ["history", ev.targetId] }); } void qc.invalidateQueries({ queryKey: ["engagements"] }); }, }); const title = ev.title ?? (ev.targetEntity === "list" ? "A list" : "An entry that's gone"); const where = ev.targetEntity === "engagement" ? "A read or play of " : ev.targetEntity === "list" ? "List " : ""; return (
  • {TYPE_LABEL[ev.type] ?? ev.type} {where} {ev.targetEntity === "entry" && ev.targetId && ev.title ? {title} : {title}}

    {d.message}

    {d.before && d.after && (
    {d.before.label}
    {changeValue(d.field ?? "", d.before.value, scale)}
    {d.after.label}
    {changeValue(d.field ?? "", d.after.value, scale)}
    )}
    {d.actions.map((a) => ( ))}
    {error &&

    {error}

    }
  • ); }