Vectrum
Sign In
60 lines · 2.3 KB · TypeScript
"use client";

import { useActionState, useState } from "react";
import { FormField, Select } from "@unlocalhosted/metalui";
import type { FormState } from "@/app/actions/types";
import { BranchIcon, PencilIcon } from "@/components/icons";
import { TextareaField, TextField } from "@/components/settings-fields";
import { Alert, Button } from "@/components/ui";

/** Editing a pull request's title, description and (while it's open) the branch it merges into, behind an Edit button. */
export function EditPullForm({
  action,
  title,
  body,
  base,
  branches,
}: {
  action: (prev: FormState, formData: FormData) => Promise<FormState>;
  title: string;
  body: string;
  base: string;
  /** Branches it could merge into instead; empty when the base can't change. */
  branches: string[];
}) {
  const [open, setOpen] = useState(false);
  const [state, formAction, pending] = useActionState(async (prev: FormState, formData: FormData) => {
    const result = await action(prev, formData);
    if (result?.success) setOpen(false);
    return result;
  }, undefined);
  if (!open) {
    return (
      <Button type="button" variant="invisible" icon={<PencilIcon />} onClick={() => setOpen(true)}>
        Edit
      </Button>
    );
  }
  return (
    <form action={formAction} className="flex w-full flex-col gap-4">
      <TextField label="Title" name="title" required maxLength={256} defaultValue={title} />
      <TextareaField label="Description" name="body" minRows={5} maxRows={20} defaultValue={body} description="“Fixes #12” links an issue." />
      {branches.length > 1 && (
        <FormField>
          <FormField.Label>Base branch</FormField.Label>
          <Select name="base" aria-label="Base branch" defaultValue={base} options={branches.map((b) => ({ value: b, label: b, lead: <BranchIcon /> }))} className="max-w-72" />
          <FormField.Description>The branch the changes are merged into.</FormField.Description>
        </FormField>
      )}
      {state?.error && <Alert>{state.error}</Alert>}
      <div className="flex justify-end gap-2">
        <Button type="button" onClick={() => setOpen(false)}>
          Cancel
        </Button>
        <Button variant="primary" disabled={pending}>
          {pending ? "Saving…" : "Save"}
        </Button>
      </div>
    </form>
  );
}