story-198: distinguish work item types (story/bug/spike) in web UI
Add visual type indicators to pipeline stage panels so stories, bugs, and spikes are distinguishable at a glance. Squash merge of feature/story-198 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -101,4 +101,120 @@ describe("StagePanel", () => {
|
||||
render(<StagePanel title="Upcoming" items={items} />);
|
||||
expect(screen.getByText("Missing front matter")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows STORY badge for story items", () => {
|
||||
const items: PipelineStageItem[] = [
|
||||
{
|
||||
story_id: "10_story_some_feature",
|
||||
name: "Some Feature",
|
||||
error: null,
|
||||
agent: null,
|
||||
},
|
||||
];
|
||||
render(<StagePanel title="Upcoming" items={items} />);
|
||||
expect(
|
||||
screen.getByTestId("type-badge-10_story_some_feature"),
|
||||
).toHaveTextContent("STORY");
|
||||
});
|
||||
|
||||
it("shows BUG badge for bug items", () => {
|
||||
const items: PipelineStageItem[] = [
|
||||
{
|
||||
story_id: "11_bug_broken_thing",
|
||||
name: "Broken Thing",
|
||||
error: null,
|
||||
agent: null,
|
||||
},
|
||||
];
|
||||
render(<StagePanel title="Current" items={items} />);
|
||||
expect(
|
||||
screen.getByTestId("type-badge-11_bug_broken_thing"),
|
||||
).toHaveTextContent("BUG");
|
||||
});
|
||||
|
||||
it("shows SPIKE badge for spike items", () => {
|
||||
const items: PipelineStageItem[] = [
|
||||
{
|
||||
story_id: "12_spike_investigate_perf",
|
||||
name: "Investigate Perf",
|
||||
error: null,
|
||||
agent: null,
|
||||
},
|
||||
];
|
||||
render(<StagePanel title="QA" items={items} />);
|
||||
expect(
|
||||
screen.getByTestId("type-badge-12_spike_investigate_perf"),
|
||||
).toHaveTextContent("SPIKE");
|
||||
});
|
||||
|
||||
it("shows no badge for unrecognised type prefix", () => {
|
||||
const items: PipelineStageItem[] = [
|
||||
{
|
||||
story_id: "13_task_do_something",
|
||||
name: "Do Something",
|
||||
error: null,
|
||||
agent: null,
|
||||
},
|
||||
];
|
||||
render(<StagePanel title="Done" items={items} />);
|
||||
expect(
|
||||
screen.queryByTestId("type-badge-13_task_do_something"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies green left border for story items", () => {
|
||||
const items: PipelineStageItem[] = [
|
||||
{
|
||||
story_id: "20_story_green_border",
|
||||
name: "Green Border",
|
||||
error: null,
|
||||
agent: null,
|
||||
},
|
||||
];
|
||||
render(<StagePanel title="Upcoming" items={items} />);
|
||||
const card = screen.getByTestId("card-20_story_green_border");
|
||||
expect(card.style.borderLeft).toContain("rgb(63, 185, 80)");
|
||||
});
|
||||
|
||||
it("applies red left border for bug items", () => {
|
||||
const items: PipelineStageItem[] = [
|
||||
{
|
||||
story_id: "21_bug_red_border",
|
||||
name: "Red Border",
|
||||
error: null,
|
||||
agent: null,
|
||||
},
|
||||
];
|
||||
render(<StagePanel title="Current" items={items} />);
|
||||
const card = screen.getByTestId("card-21_bug_red_border");
|
||||
expect(card.style.borderLeft).toContain("rgb(248, 81, 73)");
|
||||
});
|
||||
|
||||
it("applies blue left border for spike items", () => {
|
||||
const items: PipelineStageItem[] = [
|
||||
{
|
||||
story_id: "22_spike_blue_border",
|
||||
name: "Blue Border",
|
||||
error: null,
|
||||
agent: null,
|
||||
},
|
||||
];
|
||||
render(<StagePanel title="QA" items={items} />);
|
||||
const card = screen.getByTestId("card-22_spike_blue_border");
|
||||
expect(card.style.borderLeft).toContain("rgb(88, 166, 255)");
|
||||
});
|
||||
|
||||
it("applies neutral left border for unrecognised type", () => {
|
||||
const items: PipelineStageItem[] = [
|
||||
{
|
||||
story_id: "23_task_neutral_border",
|
||||
name: "Neutral Border",
|
||||
error: null,
|
||||
agent: null,
|
||||
},
|
||||
];
|
||||
render(<StagePanel title="Done" items={items} />);
|
||||
const card = screen.getByTestId("card-23_task_neutral_border");
|
||||
expect(card.style.borderLeft).toContain("rgb(68, 68, 68)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,32 @@ import { useLozengeFly } from "./LozengeFlyContext";
|
||||
|
||||
const { useLayoutEffect, useRef } = React;
|
||||
|
||||
type WorkItemType = "story" | "bug" | "spike" | "unknown";
|
||||
|
||||
const TYPE_COLORS: Record<WorkItemType, string> = {
|
||||
story: "#3fb950",
|
||||
bug: "#f85149",
|
||||
spike: "#58a6ff",
|
||||
unknown: "#444",
|
||||
};
|
||||
|
||||
const TYPE_LABELS: Record<WorkItemType, string | null> = {
|
||||
story: "STORY",
|
||||
bug: "BUG",
|
||||
spike: "SPIKE",
|
||||
unknown: null,
|
||||
};
|
||||
|
||||
function getWorkItemType(storyId: string): WorkItemType {
|
||||
const match = storyId.match(/^\d+_([a-z]+)_/);
|
||||
if (!match) return "unknown";
|
||||
const segment = match[1];
|
||||
if (segment === "story" || segment === "bug" || segment === "spike") {
|
||||
return segment;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
interface StagePanelProps {
|
||||
title: string;
|
||||
items: PipelineStageItem[];
|
||||
@@ -137,13 +163,18 @@ export function StagePanel({
|
||||
>
|
||||
{items.map((item) => {
|
||||
const itemNumber = item.story_id.match(/^(\d+)/)?.[1];
|
||||
const itemType = getWorkItemType(item.story_id);
|
||||
const borderColor = TYPE_COLORS[itemType];
|
||||
const typeLabel = TYPE_LABELS[itemType];
|
||||
return (
|
||||
<div
|
||||
key={`${title}-${item.story_id}`}
|
||||
data-testid={`card-${item.story_id}`}
|
||||
style={{
|
||||
border: item.agent
|
||||
? "1px solid #2a3a4a"
|
||||
: "1px solid #2a2a2a",
|
||||
borderLeft: `3px solid ${borderColor}`,
|
||||
borderRadius: "8px",
|
||||
padding: "8px 12px",
|
||||
background: item.agent ? "#161e2a" : "#191919",
|
||||
@@ -165,6 +196,20 @@ export function StagePanel({
|
||||
#{itemNumber}
|
||||
</span>
|
||||
)}
|
||||
{typeLabel && (
|
||||
<span
|
||||
data-testid={`type-badge-${item.story_id}`}
|
||||
style={{
|
||||
fontSize: "0.7em",
|
||||
fontWeight: 700,
|
||||
color: borderColor,
|
||||
marginRight: "8px",
|
||||
letterSpacing: "0.05em",
|
||||
}}
|
||||
>
|
||||
{typeLabel}
|
||||
</span>
|
||||
)}
|
||||
{item.name ?? item.story_id}
|
||||
</div>
|
||||
{item.error && (
|
||||
|
||||
Reference in New Issue
Block a user