diff --git a/src/components/layout/workflow-card-meta.ts b/src/components/layout/workflow-card-meta.ts new file mode 100644 index 0000000..4d5dabb --- /dev/null +++ b/src/components/layout/workflow-card-meta.ts @@ -0,0 +1,39 @@ +import type { RadarSourceType, JanitorSourceType } from "@/lib/graph-api" +import type { LucideIcon } from "lucide-react" +import { AtSign, Video, Rss, Hash, Sparkles, Layers, ShieldCheck } from "lucide-react" + +export interface WorkflowTypeMeta { + icon: LucideIcon + label: string + tone: string // Tailwind color group, e.g. "sky", "red", "violet" +} + +export const WORKFLOW_TYPE_META: Record = { + twitter_handle: { icon: AtSign, label: "Twitter Handle", tone: "sky" }, + youtube_channel: { icon: Video, label: "YouTube Channel", tone: "red" }, + rss: { icon: Rss, label: "RSS Feed", tone: "orange" }, + topic: { icon: Hash, label: "Topic", tone: "amber" }, + deduplication: { icon: Layers, label: "Deduplication", tone: "violet" }, + content_review: { icon: ShieldCheck, label: "Content Review", tone: "emerald" }, + topic_review: { icon: Sparkles, label: "Topic Review", tone: "fuchsia" }, +} + +// Static tone → Tailwind class mapping to avoid Tailwind purge issues +export const TONE_CLASSES: Record = { + sky: "bg-sky-500/10 text-sky-400 border-sky-500/20", + red: "bg-red-500/10 text-red-400 border-red-500/20", + orange: "bg-orange-500/10 text-orange-400 border-orange-500/20", + amber: "bg-amber-500/10 text-amber-400 border-amber-500/20", + violet: "bg-violet-500/10 text-violet-400 border-violet-500/20", + emerald: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", + fuchsia: "bg-fuchsia-500/10 text-fuchsia-400 border-fuchsia-500/20", + slate: "bg-slate-500/10 text-slate-400 border-slate-500/20", +} + +// Returns item.label if set, otherwise the human-friendly label from meta +export function getWorkflowDisplayName(item: { + label?: string + source_type: RadarSourceType | JanitorSourceType +}): string { + return item.label ?? WORKFLOW_TYPE_META[item.source_type]?.label ?? item.source_type +} diff --git a/src/components/layout/workflows-panel.tsx b/src/components/layout/workflows-panel.tsx index d596639..1895bd0 100644 --- a/src/components/layout/workflows-panel.tsx +++ b/src/components/layout/workflows-panel.tsx @@ -6,6 +6,11 @@ import { ScrollArea } from "@/components/ui/scroll-area" import { getWorkflowMarketplace, type WorkflowMarketplaceItem, type CronKind } from "@/lib/graph-api" import { isMocksEnabled, MOCK_WORKFLOW_MARKETPLACE } from "@/lib/mock-data" import { cn } from "@/lib/utils" +import { + WORKFLOW_TYPE_META, + TONE_CLASSES, + getWorkflowDisplayName, +} from "./workflow-card-meta" type FilterKind = "all" | CronKind @@ -26,33 +31,49 @@ function kindBadgeClass(kind: CronKind): string { } function WorkflowCard({ item }: { item: WorkflowMarketplaceItem }) { - const label = item.label || item.source_type + const meta = WORKFLOW_TYPE_META[item.source_type] + const Icon = meta?.icon ?? Cpu + const tone = meta?.tone ?? "slate" + const displayName = getWorkflowDisplayName(item) + return ( -
- + {/* Tinted icon tile */} +
+ > + +
+ + {/* Name — single source of truth, no duplicate sub-text */}
-

{label}

-

- {item.source_type} -

+

{displayName}

+
+ + {/* Kind chip + enabled dot */} +
+ + {kindBadgeLabel(item.kind)} + +
- - {kindBadgeLabel(item.kind)} -
) } @@ -144,7 +165,7 @@ export function WorkflowsPanel({ onClose }: { onClose: () => void }) {

) : ( -
+
{filtered.map((item) => ( ))} diff --git a/src/lib/__tests__/workflows-panel.test.tsx b/src/lib/__tests__/workflows-panel.test.tsx index f63a7b1..3fe52be 100644 --- a/src/lib/__tests__/workflows-panel.test.tsx +++ b/src/lib/__tests__/workflows-panel.test.tsx @@ -71,12 +71,18 @@ describe("WorkflowsPanel", () => { render( {}} />) await waitFor(() => { - // twitter_handle appears twice (label fallback + sub-text); ensure at least one exists - expect(screen.getAllByText("twitter_handle").length).toBeGreaterThan(0) + // twitter_handle has no label — humanized name "Twitter Handle" appears once + expect(screen.getByText("Twitter Handle")).toBeInTheDocument() expect(screen.getByText("YouTube Channel")).toBeInTheDocument() expect(screen.getByText("Deduplication")).toBeInTheDocument() expect(screen.getByText("Content Review")).toBeInTheDocument() }) + + // Icon tiles render with data-testid per source_type + expect(document.querySelector("[data-testid='workflow-icon-twitter_handle']")).toBeTruthy() + expect(document.querySelector("[data-testid='workflow-icon-youtube_channel']")).toBeTruthy() + expect(document.querySelector("[data-testid='workflow-icon-deduplication']")).toBeTruthy() + expect(document.querySelector("[data-testid='workflow-icon-content_review']")).toBeTruthy() }) it("falls back to source_type when label is absent", async () => { @@ -86,8 +92,9 @@ describe("WorkflowsPanel", () => { render( {}} />) await waitFor(() => { - // rc-twitter has no label — source_type is rendered as both primary text and sub-text - expect(screen.getAllByText("twitter_handle").length).toBeGreaterThan(0) + // rc-twitter has no label — humanized display name shown once, raw source_type not visible + expect(screen.getByText("Twitter Handle")).toBeInTheDocument() + expect(screen.queryByText("twitter_handle")).not.toBeInTheDocument() }) }) @@ -123,8 +130,8 @@ describe("WorkflowsPanel", () => { await userEvent.click(screen.getByRole("button", { name: "Ingestion" })) - // Source items should be visible (twitter_handle may appear twice as label+sub-text) - expect(screen.getAllByText("twitter_handle").length).toBeGreaterThan(0) + // Source items should be visible + expect(screen.getByText("Twitter Handle")).toBeInTheDocument() expect(screen.getByText("YouTube Channel")).toBeInTheDocument() // Janitor items should not be visible expect(screen.queryByText("Deduplication")).not.toBeInTheDocument() @@ -144,7 +151,7 @@ describe("WorkflowsPanel", () => { expect(screen.getByText("Deduplication")).toBeInTheDocument() expect(screen.getByText("Content Review")).toBeInTheDocument() // Source items should not be visible - expect(screen.queryByText("twitter_handle")).not.toBeInTheDocument() + expect(screen.queryByText("Twitter Handle")).not.toBeInTheDocument() expect(screen.queryByText("YouTube Channel")).not.toBeInTheDocument() }) @@ -159,7 +166,7 @@ describe("WorkflowsPanel", () => { await userEvent.click(screen.getByRole("button", { name: "Janitor" })) await userEvent.click(screen.getByRole("button", { name: "All" })) - expect(screen.getAllByText("twitter_handle").length).toBeGreaterThan(0) + expect(screen.getByText("Twitter Handle")).toBeInTheDocument() expect(screen.getByText("Deduplication")).toBeInTheDocument() }) @@ -184,8 +191,9 @@ describe("WorkflowsPanel", () => { render( {}} />) await waitFor(() => { - // MOCK_ITEMS has twitter_handle (no label) — appears as both primary text and sub-text - expect(screen.getAllByText("twitter_handle").length).toBeGreaterThan(0) + // twitter_handle item has no label — humanized name shown once + expect(screen.getByText("Twitter Handle")).toBeInTheDocument() + expect(screen.queryByText("twitter_handle")).not.toBeInTheDocument() }) }) })