storkit: merge 454_story_deduplicate_work_item_display_in_web_ui_story_panel
This commit is contained in:
@@ -1643,7 +1643,9 @@ describe("Slash command handling (Story 374)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("AC: /help calls botCommand and displays response", async () => {
|
it("AC: /help calls botCommand and displays response", async () => {
|
||||||
mockedApi.botCommand.mockResolvedValue({ response: "Available commands: status, help, ..." });
|
mockedApi.botCommand.mockResolvedValue({
|
||||||
|
response: "Available commands: status, help, ...",
|
||||||
|
});
|
||||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
@@ -1660,11 +1662,7 @@ describe("Slash command handling (Story 374)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockedApi.botCommand).toHaveBeenCalledWith(
|
expect(mockedApi.botCommand).toHaveBeenCalledWith("help", "", undefined);
|
||||||
"help",
|
|
||||||
"",
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
expect(lastSendChatArgs).toBeNull();
|
expect(lastSendChatArgs).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -1723,7 +1721,9 @@ describe("Bug 450: WebSocket error messages displayed in chat", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const link = await screen.findByRole("link", { name: /https:\/\/example\.com\/oauth\/login/ });
|
const link = await screen.findByRole("link", {
|
||||||
|
name: /https:\/\/example\.com\/oauth\/login/,
|
||||||
|
});
|
||||||
expect(link).toBeInTheDocument();
|
expect(link).toBeInTheDocument();
|
||||||
expect(link).toHaveAttribute("href", "https://example.com/oauth/login");
|
expect(link).toHaveAttribute("href", "https://example.com/oauth/login");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("WorkItemDetailPanel", () => {
|
describe("WorkItemDetailPanel", () => {
|
||||||
it("renders the story name in the header", async () => {
|
it("renders the story name in the header with type and ID prefix", async () => {
|
||||||
render(
|
render(
|
||||||
<WorkItemDetailPanel
|
<WorkItemDetailPanel
|
||||||
storyId="237_bug_test"
|
storyId="237_bug_test"
|
||||||
@@ -79,7 +79,7 @@ describe("WorkItemDetailPanel", () => {
|
|||||||
);
|
);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("detail-panel-title")).toHaveTextContent(
|
expect(screen.getByTestId("detail-panel-title")).toHaveTextContent(
|
||||||
"Big Title Story",
|
"Bug 237: Big Title Story",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -110,6 +110,10 @@ describe("WorkItemDetailPanel", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders markdown headings with constrained inline font size", async () => {
|
it("renders markdown headings with constrained inline font size", async () => {
|
||||||
|
mockedGetWorkItemContent.mockResolvedValue({
|
||||||
|
...DEFAULT_CONTENT,
|
||||||
|
content: "# Title Heading\n\n## Section Heading\n\nSome content.",
|
||||||
|
});
|
||||||
render(
|
render(
|
||||||
<WorkItemDetailPanel
|
<WorkItemDetailPanel
|
||||||
storyId="237_bug_test"
|
storyId="237_bug_test"
|
||||||
@@ -119,11 +123,95 @@ describe("WorkItemDetailPanel", () => {
|
|||||||
);
|
);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const content = screen.getByTestId("detail-panel-content");
|
const content = screen.getByTestId("detail-panel-content");
|
||||||
const h1 = content.querySelector("h1");
|
// H1 is stripped by stripDisplayContent; h2 should be constrained
|
||||||
expect(h1).not.toBeNull();
|
const h2 = content.querySelector("h2");
|
||||||
expect(h1?.style.fontSize).toBeTruthy();
|
expect(h2).not.toBeNull();
|
||||||
|
expect(h2?.style.fontSize).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("strips YAML front matter so 'name' is not shown as a prefix in content", async () => {
|
||||||
|
mockedGetWorkItemContent.mockResolvedValue({
|
||||||
|
content:
|
||||||
|
'---\nname: "My Story Name"\n---\n\n# Story 42: My Story Name\n\n## User Story\n\nAs a user...',
|
||||||
|
stage: "current",
|
||||||
|
name: "My Story Name",
|
||||||
|
agent: null,
|
||||||
|
});
|
||||||
|
render(
|
||||||
|
<WorkItemDetailPanel
|
||||||
|
storyId="42_story_test"
|
||||||
|
pipelineVersion={0}
|
||||||
|
onClose={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const content = await screen.findByTestId("detail-panel-content");
|
||||||
|
expect(content.textContent).not.toMatch(/name:/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips the first H1 heading so the story title is not shown twice", async () => {
|
||||||
|
mockedGetWorkItemContent.mockResolvedValue({
|
||||||
|
content:
|
||||||
|
'---\nname: "My Story Name"\n---\n\n# Story 42: My Story Name\n\n## User Story\n\nAs a user...',
|
||||||
|
stage: "current",
|
||||||
|
name: "My Story Name",
|
||||||
|
agent: null,
|
||||||
|
});
|
||||||
|
render(
|
||||||
|
<WorkItemDetailPanel
|
||||||
|
storyId="42_story_test"
|
||||||
|
pipelineVersion={0}
|
||||||
|
onClose={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const content = await screen.findByTestId("detail-panel-content");
|
||||||
|
expect(content.querySelector("h1")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows 'Type N: Name' format in the panel header title (story ID/title left-justified)", async () => {
|
||||||
|
mockedGetWorkItemContent.mockResolvedValue({
|
||||||
|
content: "## User Story\n\nAs a user...",
|
||||||
|
stage: "current",
|
||||||
|
name: "My Story Name",
|
||||||
|
agent: null,
|
||||||
|
});
|
||||||
|
render(
|
||||||
|
<WorkItemDetailPanel
|
||||||
|
storyId="42_story_test"
|
||||||
|
pipelineVersion={0}
|
||||||
|
onClose={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("detail-panel-title")).toHaveTextContent(
|
||||||
|
"Story 42: My Story Name",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show the work item type label twice when front matter and H1 are stripped", async () => {
|
||||||
|
mockedGetWorkItemContent.mockResolvedValue({
|
||||||
|
content:
|
||||||
|
'---\nname: "My Story Name"\n---\n\n# Story 42: My Story Name\n\n## User Story\n\nContent.',
|
||||||
|
stage: "current",
|
||||||
|
name: "My Story Name",
|
||||||
|
agent: null,
|
||||||
|
});
|
||||||
|
render(
|
||||||
|
<WorkItemDetailPanel
|
||||||
|
storyId="42_story_test"
|
||||||
|
pipelineVersion={0}
|
||||||
|
onClose={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await screen.findByTestId("detail-panel-content");
|
||||||
|
// "Story" type label appears exactly once — in the panel header title
|
||||||
|
const title = screen.getByTestId("detail-panel-title");
|
||||||
|
expect(title.textContent).toContain("Story 42:");
|
||||||
|
// The content body should not contain an H1 repeating the type + title
|
||||||
|
const content = screen.getByTestId("detail-panel-content");
|
||||||
|
expect(content.querySelector("h1")).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("WorkItemDetailPanel - Agent Logs", () => {
|
describe("WorkItemDetailPanel - Agent Logs", () => {
|
||||||
|
|||||||
@@ -17,6 +17,46 @@ import { api } from "../api/client";
|
|||||||
|
|
||||||
const { useCallback, useEffect, useRef, useState } = React;
|
const { useCallback, useEffect, useRef, useState } = React;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip YAML front matter and the first H1 heading from story content before
|
||||||
|
* rendering. The panel header already shows the story ID/title, so rendering
|
||||||
|
* them again inside the markdown body creates duplicate information.
|
||||||
|
*/
|
||||||
|
function stripDisplayContent(content: string): string {
|
||||||
|
let text = content;
|
||||||
|
// Strip YAML front matter (--- ... ---)
|
||||||
|
if (text.startsWith("---")) {
|
||||||
|
const eol = text.indexOf("\n");
|
||||||
|
if (eol !== -1) {
|
||||||
|
const closeIdx = text.indexOf("\n---", eol);
|
||||||
|
if (closeIdx !== -1) {
|
||||||
|
text = text.slice(closeIdx + 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Trim leading blank lines left by the front matter
|
||||||
|
text = text.trimStart();
|
||||||
|
// Strip the first H1 heading — it duplicates the panel header title
|
||||||
|
if (text.startsWith("# ")) {
|
||||||
|
const eol = text.indexOf("\n");
|
||||||
|
text = eol !== -1 ? text.slice(eol + 1).trimStart() : "";
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the story ID/title line shown in the panel header.
|
||||||
|
* Produces e.g. "Story 454: My Story Name" or "Bug 12: Crash on startup".
|
||||||
|
* Falls back to name or storyId when the pattern doesn't match.
|
||||||
|
*/
|
||||||
|
function formatStoryTitle(storyId: string, name: string | null): string {
|
||||||
|
const match = storyId.match(/^(\d+)_([a-z]+)_/);
|
||||||
|
if (!match || !name) return name ?? storyId;
|
||||||
|
const [, number, type] = match;
|
||||||
|
const typeLabel = type.charAt(0).toUpperCase() + type.slice(1);
|
||||||
|
return `${typeLabel} ${number}: ${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
const STAGE_LABELS: Record<string, string> = {
|
const STAGE_LABELS: Record<string, string> = {
|
||||||
backlog: "Backlog",
|
backlog: "Backlog",
|
||||||
current: "Current",
|
current: "Current",
|
||||||
@@ -352,7 +392,7 @@ export function WorkItemDetailPanel({
|
|||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{name ?? storyId}
|
{formatStoryTitle(storyId, name)}
|
||||||
</div>
|
</div>
|
||||||
{stage && (
|
{stage && (
|
||||||
<div
|
<div
|
||||||
@@ -504,7 +544,7 @@ export function WorkItemDetailPanel({
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{content}
|
{stripDisplayContent(content)}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user