commits
tags
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>
);
}