Skip to the content

Capsid menu

app/components/admin/kb-file-editor.tsx

145 lines and 6 functions in DrDustinEdwards/dustinedwards-info, at commit 1590ba4 on 2026-10-24. This file on GitHub.

Functions

The functions of kb-file-editor.tsx
FunctionLinesComplexityCoveredCRAPMutation
documentCspNonce21 to 243No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
plural26 to 282No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
KbCheckSave35 to 648No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:56>56 to 581No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
KbFileEditor71 to 15714No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code
<anonymous:149>149 to 1511No data: no coverage report in this mapNo data: no coverage report in this mapNo data: no mutants in this code

Imports

It imports app/components/admin/shell-parts.tsx, app/kb/editor.server.ts. Imported by app/components/admin/kb-form-steps.tsx, app/components/admin/kb-form.tsx, app/kb/editor-route.server.ts, app/routes/admin.kb.entry.tsx, app/routes/admin.kb.item.tsx.

Source

import { useState } from "react";
import { Form, Link, useNavigation } from "react-router";

import { Alert, Banner } from "capsomer/react/banner";
import { Button } from "capsomer/react/button";
import { MarkdownEditor } from "capsomer/react/markdown-editor";
import { Panel } from "capsomer/react/panel";
import { Row, RowList } from "capsomer/react/row-list";

import { PageHead } from "capsomer/react/page-head";
import { crumbLink } from "~/components/admin/shell-parts";
import type { KbFile, KbGap } from "~/kb/editor.server";

/** What the action hands back: a check's verdict, or a save that did not land, with the text the reader sent. */
export type KbEditorActionData =
  | { intent: "check"; raw: string; model?: string; ok: boolean; errors: string[]; gaps: KbGap[] }
  | { intent: "save"; raw: string; model?: string; refused?: string[]; conflict?: string }
  | { intent: "duplicate"; raw: string; refused: string[] };

// Read off the document, not loader data, as the post editor does: the IDL property, since browsers hide the attribute.
function documentCspNonce(): string {
  if (typeof document === "undefined") return "";
  return document.querySelector<HTMLScriptElement>("script[nonce]")?.nonce ?? "";
}

export function plural(count: number, one: string, many: string) {
  return `${count} ${count === 1 ? one : many}`;
}

/**
 * Check and Save, and what the last one said, for the file editor and the form editor alike. The answer sits beside the
 * buttons that ask for it, so a Check's answer is in view. The status region is always in the DOM, so a result is
 * announced; a refusal is an alert. `onFile` names the committed file when no Check has run yet.
 */
export function KbCheckSave({ busy, actionData, errors, onFile = false }: { busy: string; actionData?: KbEditorActionData; errors: string[]; onFile?: boolean }) {
  const checked = actionData?.intent === "check" ? actionData : null;
  return (
    <>
      <div className="app-actions">
        <Button type="submit" name="intent" value="check" pending={busy === "check"}>
          Check
        </Button>
        <Button type="submit" name="intent" value="save" variant="primary" pending={busy === "save"}>
          Save
        </Button>
      </div>
      <div role="status">{checked?.ok ? <Banner tone="ok">Passes every check. Nothing was saved.</Banner> : null}</div>
      {actionData?.intent === "save" && actionData.conflict ? (
        <Alert tone="crit" title="Not saved">
          {actionData.conflict}
        </Alert>
      ) : null}
      {errors.length > 0 ? (
        <Alert tone="crit" title={`${plural(errors.length, "check fails", "checks fail")}${onFile && !checked ? " on this file" : ""}`}>
          <ul>
            {errors.map((error) => (
              <li key={error}>{error}</li>
            ))}
          </ul>
        </Alert>
      ) : null}
    </>
  );
}

/**
 * One Knowledge Base file (docs/KNOWLEDGE-BASE.md, step 3), an entry or a registry item, edited as the file it is. Check
 * runs the save's own checks and commits nothing; Save commits through the save the operator API uses. Without script the
 * editor is a plain textarea in the same form, so both buttons still work.
 */
export function KbFileEditor({
  file,
  actionData,
  saved,
}: {
  file: KbFile;
  actionData?: KbEditorActionData;
  saved?: { commit: string; unchanged: boolean } | null;
}) {
  const navigation = useNavigation();
  const busy = navigation.state === "submitting" ? String(navigation.formData?.get("intent") ?? "") : "";
  const [raw, setRaw] = useState(actionData?.raw ?? file.raw);

  // The latest word on the file: a check just run, else what the loader's check said of the committed file.
  const checked = actionData?.intent === "check" ? actionData : null;
  const errors = checked ? checked.errors : actionData?.intent === "save" && actionData.refused ? actionData.refused : file.errors;
  const gaps = checked ? checked.gaps : file.gaps;

  return (
    <div className="app-page">
      <PageHead
        renderLink={crumbLink}
        crumbs={[
          { label: "Knowledge Base", href: "/admin/kb" },
          { label: file.group, href: file.listHref },
        ]}
        title={file.title}
        actions={
          <Link to={{ search: "" }} className="cap-btn" data-variant="quiet">
            Back to the form
          </Link>
        }
        lead={
          <>
            <code>{file.file}</code>
            {file.publicHref ? (
              <>
                {" "}
                <Link to={file.publicHref} reloadDocument>
                  View the page
                </Link>
              </>
            ) : null}
          </>
        }
      />

      {saved ? (
        <Banner tone="ok">
          {saved.unchanged
            ? "Nothing to commit: the file was already this. Its page was brought up to date."
            : `Saved as commit ${saved.commit}. The page shows it on the next visit.`}
        </Banner>
      ) : null}

      <Form method="post" className="kb-editor-form">
        <input type="hidden" name="sha" value={file.sha} />
        <MarkdownEditor
          label="File"
          ariaLabel="File, markdown with front matter"
          id="kb-file"
          name="raw"
          value={raw}
          onChange={setRaw}
          nonce={documentCspNonce()}
          lineNumbers
          help={
            file.target.type === "entry"
              ? "The whole file: its fields between the --- lines, then the steps with their marks. Check runs every check a save runs and saves nothing."
              : "The whole file: the item's fields between the --- lines, and nothing after them. Check runs every check a save runs and saves nothing."
          }
        />
        <KbCheckSave busy={busy} actionData={actionData} errors={errors} onFile />
      </Form>

      {gaps.length > 0 ? (
        <Panel title="Needs info" count={gaps.length} flush>
          <RowList label="Needs info">
            {gaps.map((gap) => (
              <Row key={`${gap.field}#${gap.reason}`} title={<code>{gap.field}</code>} detail={gap.reason} />
            ))}
          </RowList>
        </Panel>
      ) : null}
    </div>
  );
}