foxygit / RPA-Remisser Log in
commits tags

/webui/frontend/src/components/Palette.tsx · 5.5 KB

raw
import { useEffect, useMemo, useState } from "react";
import { api, type BlockMeta } from "../lib/api";
import type { Schema } from "../lib/schema";
import { fieldLabel } from "../lib/schema";
import type { Lang, T } from "../lib/i18n";

export const DND_STEP = "application/rpa-step";
export const DND_BLOCK = "application/rpa-block";

interface Props {
  schema: Schema;
  lang: Lang;
  t: T;
  blocksRefreshKey: number;
  onSaveSelectionAsBlock: () => void;
  /** Klick-fallback (utöver drag-and-drop). */
  onAddStep: (type: string) => void;
  onInsertBlock: (name: string) => void;
}

const CAT_ORDER = ["navigation", "interaction", "data", "control", "files", "desktop"];
const CAT_LABEL: Record<string, { sv: string; en: string }> = {
  navigation: { sv: "Navigering", en: "Navigation" },
  interaction: { sv: "Interaktion", en: "Interaction" },
  data: { sv: "Data / variabler", en: "Data / variables" },
  control: { sv: "Kontrollflöde", en: "Control flow" },
  files: { sv: "Filer", en: "Files" },
  desktop: { sv: "Desktop (Windows)", en: "Desktop (Windows)" },
};

export function Palette({
  schema,
  lang,
  t,
  blocksRefreshKey,
  onSaveSelectionAsBlock,
  onAddStep,
  onInsertBlock,
}: Props) {
  const [tab, setTab] = useState<"steps" | "blocks">("steps");
  const [q, setQ] = useState("");
  const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
  const [blocks, setBlocks] = useState<BlockMeta[]>([]);

  useEffect(() => {
    api.listBlocks().then(setBlocks).catch(() => setBlocks([]));
  }, [blocksRefreshKey]);

  const groups = useMemo(() => {
    const ql = q.trim().toLowerCase();
    const by: Record<string, typeof schema.step_types> = {};
    for (const s of schema.step_types) {
      const label = fieldLabel(s, lang).toLowerCase();
      if (ql && !label.includes(ql) && !s.type.includes(ql)) continue;
      (by[s.category] ??= []).push(s);
    }
    return CAT_ORDER.filter((c) => by[c]?.length).map((c) => ({ cat: c, steps: by[c] }));
  }, [schema, lang, q]);

  return (
    <div className="palette">
      <div className="palette-tabs">
        <button className={tab === "steps" ? "on" : ""} onClick={() => setTab("steps")}>
          {t("palette")}
        </button>
        <button className={tab === "blocks" ? "on" : ""} onClick={() => setTab("blocks")}>
          {t("palette_blocks")}
        </button>
      </div>

      {tab === "steps" && (
        <>
          <input
            className="palette-search"
            placeholder={t("palette_search")}
            value={q}
            onChange={(e) => setQ(e.target.value)}
          />
          <div className="palette-scroll">
            {groups.map(({ cat, steps }) => {
              const isCollapsed = collapsed.has(cat) && !q;
              return (
                <div className="palette-group" key={cat}>
                  <button
                    className="palette-group-head"
                    onClick={() =>
                      setCollapsed((prev) => {
                        const n = new Set(prev);
                        if (n.has(cat)) n.delete(cat);
                        else n.add(cat);
                        return n;
                      })
                    }
                  >
                    {isCollapsed ? "▸" : "▾"} {(CAT_LABEL[cat]?.[lang] ?? cat)}
                  </button>
                  {!isCollapsed &&
                    steps.map((s) => (
                      <div
                        key={s.type}
                        className={"palette-item cat-" + s.category}
                        draggable
                        title={(s.help ?? "") + "  ·  " + t("palette_click_add")}
                        onDragStart={(e) => {
                          e.dataTransfer.setData(DND_STEP, s.type);
                          e.dataTransfer.setData("text/plain", s.type);
                          e.dataTransfer.effectAllowed = "copy";
                        }}
                        onDoubleClick={() => onAddStep(s.type)}
                      >
                        {fieldLabel(s, lang)}
                        <button className="palette-add" title={t("palette_click_add")} onClick={() => onAddStep(s.type)}>
                          +
                        </button>
                      </div>
                    ))}
                </div>
              );
            })}
          </div>
        </>
      )}

      {tab === "blocks" && (
        <div className="palette-scroll">
          <button className="secondary palette-save-block" onClick={onSaveSelectionAsBlock}>
            {t("block_save_selection")}
          </button>
          {blocks.length === 0 && <p className="hint">{t("block_none")}</p>}
          {blocks.map((b) => (
            <div
              key={b.name}
              className="palette-block"
              draggable
              title={b.description}
              onDragStart={(e) => {
                e.dataTransfer.setData(DND_BLOCK, b.name);
                e.dataTransfer.setData("text/plain", b.name);
                e.dataTransfer.effectAllowed = "copy";
              }}
              onDoubleClick={() => onInsertBlock(b.name)}
            >
              <div className="palette-block-name">
                {b.name}
                <button className="palette-add" title={t("block_insert")} onClick={() => onInsertBlock(b.name)}>
                  {t("block_insert")}
                </button>
              </div>
              <div className="hint">{b.description || `${b.step_count} steg`}</div>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}