Skip to the content

Capsid menu

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

The functions of revision-list.tsx
FunctionLinesComplexityCoveredCRAPMutation
RevisionList15 to 1683No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
toggle33 to 566No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:49>49 to 491No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
restore58 to 844No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:60>60 to 601No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:68>68 to 681No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:78>78 to 821No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:105>105 to 16411No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:126>126 to 1261No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:139>139 to 1412No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
DiffBlock174 to 1851No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:177>177 to 1821No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
diffKind187 to 1926No data: no coverage report in this mapNo data: no coverage report in this mapNo 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;
}