DefinitionActions

tcmts.studio.DefinitionActions · #3swqawjd · term · runs in the browser

uses useState

What a definition's page does with it in the studio: Edit it (in a scratch file), Rename it (or move it: a name of another namespace), as tcm mv, or Delete its name, as tcm rm, asking first and saying what uses it (which keeps working, by hash). usedBy: the named definitions that use it; editable: whether it's code or text to edit.

import { navigate } from "#codebase/tcmts/react";
import { codeHref, toasts } from "#codebase/tcmts/share";
import { EditButton, removeDefinition, renameDefinition } from "#codebase/tcmts/studio";
import { useState } from "react";
import { Button, Dialog, DialogTrigger, Heading, Input, Label, Popover, TextField } from "react-aria-components";

export function DefinitionActions({
  name,
  usedBy,
  editable,
}: {
  name: string;
  usedBy: string[];
  editable: boolean;
}) {
  const [to, setTo] = useState(name);
  const [busy, setBusy] = useState(false);
  const parent = name.includes(".") ? name.slice(0, name.lastIndexOf(".")) : "";
  return (
    <>
      {editable && <EditButton name={name} />}
      <DialogTrigger onOpenChange={(open) => open && setTo(name)}>
        <Button className="btn">Rename</Button>
        <Popover className="menu-pop getit" placement="bottom end" offset={6}>
          <Dialog className="getit-body" aria-label={`Rename ${name}`}>
            {({ close }) => (
              <form
                className="prodform"
                onSubmit={async (e) => {
                  e.preventDefault();
                  const want = to.trim();
                  if (want === name) return close();
                  setBusy(true);
                  try {
                    const said = await renameDefinition(name, want);
                    close();
                    toasts.add(
                      { title: said || `Renamed to ${want}`, tone: "ok" },
                      { timeout: 4000 },
                    );
                    navigate(codeHref(want));
                  } catch (err) {
                    toasts.add(
                      {
                        title: `Couldn't rename ${name}`,
                        description: (err as Error).message,
                        tone: "bad",
                      },
                      { timeout: 8000 },
                    );
                  }
                  setBusy(false);
                }}
              >
                <Heading slot="title" className="getit-title">
                  Rename {name}
                </Heading>
                <TextField className="fieldrow" value={to} onChange={setTo} autoFocus>
                  <Label>Its name</Label>
                  <Input className="field mono" />
                </TextField>
                <p>
                  Its code stays as it is, and what uses it follows (by hash). Another namespace
                  moves it there.
                </p>
                <div className="actions">
                  <Button className="btn" onPress={close}>
                    Cancel
                  </Button>
                  <Button className="btn primary" type="submit" isPending={busy}>
                    Rename
                  </Button>
                </div>
              </form>
            )}
          </Dialog>
        </Popover>
      </DialogTrigger>
      <DialogTrigger>
        <Button className="btn danger">Delete</Button>
        <Popover className="menu-pop getit" placement="bottom end" offset={6}>
          <Dialog className="getit-body" aria-label={`Delete ${name}`}>
            {({ close }) => (
              <>
                <Heading slot="title" className="getit-title">
                  Delete {name}?
                </Heading>
                {usedBy.length ? (
                  <p>
                    {`${usedBy.length} named definition${usedBy.length === 1 ? " uses" : "s use"} it (${usedBy.slice(0, 4).join(", ")}${usedBy.length > 4 ? ", …" : ""}). ${usedBy.length === 1 ? "It keeps" : "They keep"} working, by its hash, but its name goes: history keeps it.`}
                  </p>
                ) : (
                  <p>Nothing named uses it. Its name goes; history keeps it.</p>
                )}
                <div className="actions">
                  <Button className="btn" onPress={close}>
                    Keep it
                  </Button>
                  <Button
                    className="btn danger"
                    onPress={async () => {
                      close();
                      try {
                        const said = await removeDefinition(name, usedBy.length > 0);
                        toasts.add(
                          { title: said || `Deleted ${name}`, tone: "ok" },
                          { timeout: 4000 },
                        );
                        navigate(codeHref(parent));
                      } catch (err) {
                        toasts.add(
                          {
                            title: `Couldn't delete ${name}`,
                            description: (err as Error).message,
                            tone: "bad",
                          },
                          { timeout: 8000 },
                        );
                      }
                    }}
                  >
                    Delete
                  </Button>
                </div>
              </>
            )}
          </Dialog>
        </Popover>
      </DialogTrigger>
    </>
  );
}

uses

used by