~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
109 lines · 3.5 KB Raw History
import { useQueryClient } from '@tanstack/react-query';
import { PencilIcon } from 'lucide-react';
import { useState } from 'react';
import { Button } from '#/components/atoms/button.jsx';
import { MarkdownField } from '#/components/molecules/markdown-field.jsx';
import { StatementCard } from '#/components/molecules/statement-card.jsx';
import { publishPull } from '#/lib/publish.js';
import { useSession } from '#/lib/session.jsx';

/**
 * The author's description of their own pull request: read as the
 * conversation's opening word, and edited in its place with the reading
 * beside the writing.
 *
 * The description is markdown wherever it is read, so it is written the way a
 * review and a comment are: in a field that previews the same renderer. The
 * record holds the title and status too, so a description edit carries the
 * title and status the author last set.
 *
 * Editing is the author's alone. A record in anybody else's repo would be a
 * statement about somebody else's work, which is what a review is.
 */
export function PullNoteEdit({ repo, pull }) {
  const { session } = useSession();
  const queryClient = useQueryClient();
  const mine = session?.did === pull.author.did;
  const [editing, setEditing] = useState(false);
  const [note, setNote] = useState(pull.note ?? '');
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState('');

  const save = async (event) => {
    event.preventDefault();
    setBusy(true);
    setError('');
    try {
      await publishPull(session, {
        repo,
        ref: `refs/heads/${pull.branch}`,
        sha: pull.sha,
        title: pull.title ?? '',
        status: pull.status ?? '',
        note: note.trim(),
      });
      setEditing(false);
      // The list holds every author's words; the next look at it re-reads.
      queryClient.invalidateQueries({ queryKey: ['pulls', repo] });
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy(false);
    }
  };

  if (editing) {
    return (
      <form onSubmit={save} className="flex flex-col gap-2.5">
        <div className="overflow-hidden rounded-lg border border-input bg-background">
          <MarkdownField
            value={note}
            onChange={setNote}
            placeholder="What this is for, and what is left to do."
            label="Description"
            rows={4}
            disabled={busy}
          />
        </div>
        <div className="flex items-center gap-3">
          <Button type="submit" size="sm" disabled={busy}>
            Save
          </Button>
          <Button
            type="button"
            variant="ghost"
            size="sm"
            onClick={() => setEditing(false)}
          >
            Cancel
          </Button>
          {error && (
            <span className="text-[12.5px] text-destructive">{error}</span>
          )}
        </div>
      </form>
    );
  }

  return (
    <>
      {pull.note && (
        <StatementCard who={pull.author} at={pull.noteAt} note={pull.note} />
      )}
      {mine && (
        <button
          type="button"
          onClick={() => {
            setNote(pull.note ?? '');
            setError('');
            setEditing(true);
          }}
          className="mt-2 flex cursor-pointer items-center gap-1.5 text-[12.5px] text-key hover:underline"
        >
          <PencilIcon className="size-3.5" />
          {pull.note ? 'Edit description' : 'Add a description'}
        </button>
      )}
    </>
  );
}