app/components/admin/revision-list.tsx
178 lines and 13 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| RevisionList | 15 to 168 | 3 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| toggle | 33 to 56 | 6 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:49> | 49 to 49 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| restore | 58 to 84 | 4 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:60> | 60 to 60 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:68> | 68 to 68 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:78> | 78 to 82 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:105> | 105 to 164 | 11 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:126> | 126 to 126 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:139> | 139 to 141 | 2 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| DiffBlock | 174 to 185 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| <anonymous:177> | 177 to 182 | 1 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
| diffKind | 187 to 192 | 6 | No data: no coverage report in this map | No data: no coverage report in this map | No data: no mutants in this code |
Imports
It imports app/lib/editor/frontmatter.ts, app/lib/error-message.mjs. Imported by app/components/admin/post-editor.tsx, app/routes/admin.posts.$slug.history.tsx.
Source
import { Fragment, useState } from "react";
import { Alert } from "capsomer/react/banner";
import type { PostFields } from "~/lib/editor/frontmatter";
import { errorMessage } from "~/lib/error-message.mjs";
export type Revision = {
sha: string;
message: string;
author: string;
date: string;
};
// Diffs load on demand: a post with fifty commits would otherwise pull fifty patches to show none.
export function RevisionList({
slug,
revisions,
onRestore,
}: {
slug: string;
revisions: Revision[];
onRestore: (fields: PostFields, sha: string) => void;
}) {
const [openSha, setOpenSha] = useState<string | null>(null);
const [patches, setPatches] = useState<Record<string, string | null>>({});
/* Per sha: one shared flag let a finished load clear another's, and read an unloaded diff as "No diff". */
const [restoring, setRestoring] = useState<Record<string, boolean>>({});
const [error, setError] = useState<string | null>(null);
/* Announced inside the drawer: the drawer is modal, so the editor's own notice behind it is inert
and a load would otherwise finish in silence. */
const [status, setStatus] = useState("");
const toggle = async (sha: string) => {
setError(null);
if (openSha === sha) {
setOpenSha(null);
return;
}
setOpenSha(sha);
if (sha in patches) return;
const short = sha.slice(0, 7);
setStatus(`Loading the diff for ${short}.`);
try {
const response = await fetch(
`/admin/posts/${slug}/revisions?sha=${encodeURIComponent(sha)}`,
);
if (!response.ok) throw new Error(`Could not read that commit (${response.status}).`);
const body = (await response.json()) as { patch: string | null };
setPatches((prev) => ({ ...prev, [sha]: body.patch }));
setStatus(body.patch ? `Diff for ${short} loaded.` : `No diff recorded for ${short}.`);
} catch (cause) {
setStatus("");
setError(errorMessage(cause));
setOpenSha(null);
}
};
const restore = async (sha: string) => {
setError(null);
setRestoring((prev) => ({ ...prev, [sha]: true }));
const short = sha.slice(0, 7);
setStatus(`Loading ${short} into the editor.`);
try {
const response = await fetch(
`/admin/posts/${slug}/revisions?sha=${encodeURIComponent(sha)}&want=content`,
);
if (!response.ok) {
const detail = (await response.json().catch(() => ({}))) as { error?: string };
throw new Error(detail.error ?? `Could not read that revision (${response.status}).`);
}
const body = (await response.json()) as { fields: PostFields };
onRestore(body.fields, sha);
setStatus(`Loaded ${short} into the editor. Nothing is written until you save.`);
} catch (cause) {
setStatus("");
setError(errorMessage(cause));
} finally {
setRestoring((prev) => {
const next = { ...prev };
delete next[sha];
return next;
});
}
};
if (revisions.length === 0) {
return <p className="cap-muted">No commits found for this post.</p>;
}
return (
<>
<p className="cap-muted">
Restoring LOADS that version into the editor as unsaved changes. Nothing
is written until you save, and saving lands a new commit on top; history
is never rewritten.
</p>
{error ? <Alert tone="crit">{error}</Alert> : null}
<p className="cap-sr-only" role="status">
{status}
</p>
<ol className="cap-rows" role="list" aria-label="Versions">
{revisions.map((revision, index) => (
<Fragment key={revision.sha}>
<li className="cap-row">
<div className="cap-row-title">{revision.message}</div>
<p className="cap-row-detail">
<code className="cap-mono">{revision.sha.slice(0, 7)}</code>
{" · "}
{revision.author}
{" · "}
{/* UTC, so the server render and hydration agree on which day a commit landed. */}
{new Date(revision.date).toLocaleString("en-US", { timeZone: "UTC" })}
{index === 0 ? " · current" : ""}
</p>
<div className="cap-row-actions">
<button
type="button"
className="cap-btn"
data-size="sm"
data-variant="quiet"
aria-expanded={openSha === revision.sha}
aria-controls={openSha === revision.sha ? `diff-${revision.sha}` : undefined}
onClick={() => void toggle(revision.sha)}
>
{openSha === revision.sha ? "Hide diff" : "View diff"}
</button>
{/* Not for the newest commit: that revision is the editor's current content. */}
{index > 0 ? (
<button
type="button"
className="cap-btn"
data-size="sm"
data-variant="quiet"
aria-disabled={restoring[revision.sha] === true || undefined}
onClick={() => {
if (restoring[revision.sha] !== true) void restore(revision.sha);
}}
>
{restoring[revision.sha] ? "Loading..." : "Load into editor"}
</button>
) : null}
</div>
</li>
{openSha === revision.sha ? (
<li id={`diff-${revision.sha}`}>
{!(revision.sha in patches) ? (
<p className="cap-muted">Loading diff...</p>
) : patches[revision.sha] ? (
<DiffBlock
patch={patches[revision.sha] ?? ""}
label={`Diff for ${revision.sha.slice(0, 7)}`}
/>
) : (
<p className="cap-muted">No diff recorded for this commit.</p>
)}
</li>
) : null}
</Fragment>
))}
</ol>
</>
);
}
/**
* A unified patch, each line tagged for the add, delete and hunk colors. A focusable named region,
* because it scrolls both ways and a keyboard can only scroll what it can focus (2.1.1).
*/
export function DiffBlock({ patch, label = "Diff" }: { patch: string; label?: string }) {
return (
<pre className="history-diff" tabIndex={0} role="region" aria-label={label}>
{patch.split("\n").map((line, i) => (
<span key={i} data-diff={diffKind(line)}>
{line}
{"\n"}
</span>
))}
</pre>
);
}
function diffKind(line: string) {
if (line.startsWith("+") && !line.startsWith("+++")) return "add";
if (line.startsWith("-") && !line.startsWith("---")) return "del";
if (line.startsWith("@@")) return "hunk";
return undefined;
}