feat(scripts): add sidecar icons and tags in the admin UI
Serve optional PNG/JPG next to each script and show meta tags in lists, cards, and the editor. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { TbFileTypeJs } from "react-icons/tb";
|
||||
|
||||
export function scriptIconSrc(name) {
|
||||
return `/api/scripts/${encodeURIComponent(name)}/icon`;
|
||||
}
|
||||
|
||||
/** Same-name PNG/JPG next to the script, or a JS document icon. */
|
||||
export function ScriptIcon({ name, hasIcon, className = "size-10" }) {
|
||||
const [failed, setFailed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setFailed(false);
|
||||
}, [name]);
|
||||
|
||||
const showFile = Boolean(name) && hasIcon !== false && !failed;
|
||||
|
||||
if (!showFile) {
|
||||
return <TbFileTypeJs className={`${className} text-warning`} aria-hidden />;
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
src={scriptIconSrc(name)}
|
||||
alt=""
|
||||
className={`${className} object-contain`}
|
||||
onError={() => setFailed(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import { scriptTags } from "../lib/script.js";
|
||||
import { TagBadge } from "./TagBadge.jsx";
|
||||
|
||||
function FieldTable({ title, fields }) {
|
||||
const entries = Object.entries(fields ?? {});
|
||||
if (entries.length === 0) return null;
|
||||
@@ -56,9 +59,18 @@ export function ScriptMetaPanel({ meta, metaError, className = "" }) {
|
||||
);
|
||||
}
|
||||
|
||||
const tags = scriptTags(meta);
|
||||
|
||||
return (
|
||||
<div className={`space-y-3 ${className}`}>
|
||||
{meta.description ? <p className="text-sm">{meta.description}</p> : null}
|
||||
{tags.length ? (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{tags.map((tag) => (
|
||||
<TagBadge key={tag} tag={tag} />
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<FieldTable title="Config" fields={meta.config} />
|
||||
<FieldTable title="Input (data)" fields={meta.input} />
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { tagColor } from "../lib/script.js";
|
||||
|
||||
export function TagBadge({ tag, className = "", onClick, title }) {
|
||||
const { bg, text } = tagColor(tag);
|
||||
const Comp = onClick ? "button" : "span";
|
||||
return (
|
||||
<Comp
|
||||
type={onClick ? "button" : undefined}
|
||||
className={`badge badge-sm border-0 font-medium ${className}`}
|
||||
style={{ backgroundColor: bg, color: text }}
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
>
|
||||
{tag}
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useScripts } from "../../api/hooks.js";
|
||||
import { ScriptIcon } from "../ScriptIcon.jsx";
|
||||
import { scriptTags } from "../../lib/script.js";
|
||||
|
||||
export function AddScriptDialog({ open, onClose, onPick }) {
|
||||
const { data: scripts = [], isLoading } = useScripts();
|
||||
@@ -12,12 +14,15 @@ export function AddScriptDialog({ open, onClose, onPick }) {
|
||||
description: typeof s === "string" ? "" : s.meta?.description ?? "",
|
||||
meta: typeof s === "string" ? null : s.meta,
|
||||
metaError: typeof s === "string" ? null : s.metaError,
|
||||
hasIcon: typeof s === "string" ? undefined : s.hasIcon,
|
||||
tags: typeof s === "string" ? [] : scriptTags(s.meta),
|
||||
}));
|
||||
if (!term) return list;
|
||||
return list.filter(
|
||||
(s) =>
|
||||
s.name.toLowerCase().includes(term) ||
|
||||
s.description.toLowerCase().includes(term),
|
||||
s.description.toLowerCase().includes(term) ||
|
||||
s.tags.some((t) => t.toLowerCase().includes(term)),
|
||||
);
|
||||
}, [scripts, q]);
|
||||
|
||||
@@ -61,9 +66,12 @@ export function AddScriptDialog({ open, onClose, onPick }) {
|
||||
setQ("");
|
||||
}}
|
||||
>
|
||||
<span className="font-mono">{s.name}</span>
|
||||
<span className="text-xs opacity-70 line-clamp-2">
|
||||
{s.metaError ? s.metaError : s.description || "No description"}
|
||||
<ScriptIcon name={s.name} hasIcon={s.hasIcon} className="size-8 shrink-0" />
|
||||
<span className="min-w-0">
|
||||
<span className="font-mono">{s.name}</span>
|
||||
<span className="text-xs opacity-70 line-clamp-2 block">
|
||||
{s.metaError ? s.metaError : s.description || "No description"}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { StatusBadge } from "../../lib/format.jsx";
|
||||
import { contextFromMeta, prettyJson } from "../../lib/script.js";
|
||||
import { ConfigFields } from "./ConfigFields.jsx";
|
||||
import { ConfigTooltip, configValueText, FieldLabel, previewConfigValue, SchemaTooltip } from "./FieldHelp.jsx";
|
||||
import { ScriptIcon } from "../ScriptIcon.jsx";
|
||||
|
||||
export function ScriptCard({
|
||||
step,
|
||||
@@ -66,6 +67,9 @@ export function ScriptCard({
|
||||
>
|
||||
<LuGripVertical className="size-4 opacity-60" />
|
||||
</button>
|
||||
{step.kind === "script" && step.script ? (
|
||||
<ScriptIcon name={step.script} hasIcon={listed?.hasIcon} className="size-8 shrink-0" />
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="min-w-0 flex-1 text-left"
|
||||
@@ -76,7 +80,7 @@ export function ScriptCard({
|
||||
<span className="min-w-0 truncate">{baseName}</span>
|
||||
{!expanded && preview ? (
|
||||
<span
|
||||
className="badge badge-secondary badge-sm max-w-[min(16rem,45%)] shrink-0 truncate font-mono font-normal"
|
||||
className="badge badge-secondary badge-sm shrink-0 truncate font-mono font-normal"
|
||||
title={previewFull}
|
||||
>
|
||||
{preview}
|
||||
|
||||
Reference in New Issue
Block a user