128 lines · 4.3 KB · TypeScript
RawDownload
import { useState } from "react";import type { EnvironmentId, ScopedThreadRef } from "@t3tools/contracts"; import { cn } from "~/lib/utils"; import { Button } from "../ui/button";import { Textarea } from "../ui/textarea";import { Toggle, ToggleGroup } from "../ui/toggle-group";import { PullRequestMarkdown } from "./PullRequestMarkdown"; /** * The box a body is rewritten in — a description, or a remark already posted. It owns the draft * and nothing else: the caller sends the request and says whether it is still in flight, so the * same box serves every mutation without knowing which one it is. * * Preview renders through the same component the saved body will be read through, which is the * only way to see what a host's markdown will actually become before it is sent. */export function PullRequestMarkdownEditor({  value,  cwd,  environmentId,  threadRef = null,  placeholder,  label,  saving,  allowEmpty = false,  className,  onSave,  onCancel,}: {  readonly value: string;  readonly cwd: string;  readonly environmentId: EnvironmentId;  /** Thread the editor sits beside, so links in its preview follow the link target setting. */  readonly threadRef?: ScopedThreadRef | null;  readonly placeholder?: string | undefined;  readonly label: string;  readonly saving: boolean;  /** A description may be cleared, which is how one is removed; a remark may not be emptied. */  readonly allowEmpty?: boolean;  readonly className?: string | undefined;  readonly onSave: (next: string) => void;  readonly onCancel: () => void;}) {  const [draft, setDraft] = useState(value);  const [preview, setPreview] = useState(false);  // The words this draft started from. React keeps a component instance wherever the same  // position and key come round again, so an editor opened on one remark can be handed another's  // words without being rebuilt — and saving would then write the first remark's text onto the  // second. Different words mean a different subject, and the draft starts again from them.  const [seed, setSeed] = useState(value);  if (seed !== value) {    setSeed(value);    setDraft(value);  }  const empty = draft.trim().length === 0;  const saveDisabled = saving || (empty && !allowEmpty);   return (    <div      className={cn("space-y-2", className)}      onKeyDown={(event) => {        if (event.nativeEvent.isComposing || event.keyCode === 229) return;        if (          event.key === "Enter" &&          (event.metaKey || event.ctrlKey) &&          !event.shiftKey &&          !event.altKey        ) {          event.preventDefault();          event.stopPropagation();          if (!saveDisabled && !event.repeat) onSave(draft);          return;        }        if (event.key !== "Escape" || saving) return;        event.preventDefault();        onCancel();      }}    >      <ToggleGroup        aria-label="Markdown editor mode"        variant="segmented"        value={[preview ? "preview" : "write"]}        disabled={saving}        onValueChange={(next) => {          const mode = next[0];          if (mode === "write" || mode === "preview") setPreview(mode === "preview");        }}      >        <Toggle value="write">Write</Toggle>        <Toggle value="preview">Preview</Toggle>      </ToggleGroup>      {preview ? (        <div className="rounded-lg border border-border/60 px-3 py-2">          {empty ? (            <p className="text-xs text-muted-foreground">Nothing to preview.</p>          ) : (            <PullRequestMarkdown              text={draft}              cwd={cwd}              environmentId={environmentId}              threadRef={threadRef}            />          )}        </div>      ) : (        <Textarea          autoFocus          disabled={saving}          value={draft}          rows={6}          placeholder={placeholder}          aria-label={label}          onChange={(event) => setDraft(event.target.value)}        />      )}      <div className="flex justify-end gap-2">        <Button size="xs" variant="ghost" disabled={saving} onClick={onCancel}>          Cancel        </Button>        <Button size="xs" variant="outline" disabled={saveDisabled} onClick={() => onSave(draft)}>          {saving ? "Saving..." : "Save"}        </Button>      </div>    </div>  );}