story-kit: merge 224_story_expand_work_item_to_full_screen_detail_view
This commit is contained in:
217
frontend/src/components/WorkItemDetailPanel.tsx
Normal file
217
frontend/src/components/WorkItemDetailPanel.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import * as React from "react";
|
||||
import Markdown from "react-markdown";
|
||||
import { api } from "../api/client";
|
||||
|
||||
const { useEffect, useRef, useState } = React;
|
||||
|
||||
const STAGE_LABELS: Record<string, string> = {
|
||||
upcoming: "Upcoming",
|
||||
current: "Current",
|
||||
qa: "QA",
|
||||
merge: "To Merge",
|
||||
done: "Done",
|
||||
archived: "Archived",
|
||||
};
|
||||
|
||||
interface WorkItemDetailPanelProps {
|
||||
storyId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function WorkItemDetailPanel({
|
||||
storyId,
|
||||
onClose,
|
||||
}: WorkItemDetailPanelProps) {
|
||||
const [content, setContent] = useState<string | null>(null);
|
||||
const [stage, setStage] = useState<string>("");
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
api
|
||||
.getWorkItemContent(storyId)
|
||||
.then((data) => {
|
||||
setContent(data.content);
|
||||
setStage(data.stage);
|
||||
setName(data.name);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
setError(err instanceof Error ? err.message : "Failed to load content");
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [storyId]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [onClose]);
|
||||
|
||||
const stageLabel = STAGE_LABELS[stage] ?? stage;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="work-item-detail-panel"
|
||||
ref={panelRef}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
background: "#1a1a1a",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid #333",
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
padding: "12px 16px",
|
||||
borderBottom: "1px solid #333",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "2px",
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
data-testid="detail-panel-title"
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.95em",
|
||||
color: "#ececec",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{name ?? storyId}
|
||||
</div>
|
||||
{stage && (
|
||||
<div
|
||||
data-testid="detail-panel-stage"
|
||||
style={{ fontSize: "0.75em", color: "#888" }}
|
||||
>
|
||||
{stageLabel}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="detail-panel-close"
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "1px solid #444",
|
||||
borderRadius: "6px",
|
||||
color: "#aaa",
|
||||
cursor: "pointer",
|
||||
padding: "4px 10px",
|
||||
fontSize: "0.8em",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Scrollable content area */}
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
overflowY: "auto",
|
||||
padding: "16px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "16px",
|
||||
}}
|
||||
>
|
||||
{loading && (
|
||||
<div
|
||||
data-testid="detail-panel-loading"
|
||||
style={{ color: "#666", fontSize: "0.85em" }}
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div
|
||||
data-testid="detail-panel-error"
|
||||
style={{ color: "#ff7b72", fontSize: "0.85em" }}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{!loading && !error && content !== null && (
|
||||
<div
|
||||
data-testid="detail-panel-content"
|
||||
className="markdown-body"
|
||||
style={{ fontSize: "0.9em", lineHeight: 1.6 }}
|
||||
>
|
||||
<Markdown>{content}</Markdown>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Placeholder sections for future content */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
{(
|
||||
[
|
||||
{ id: "agent-logs", label: "Agent Logs" },
|
||||
{ id: "test-output", label: "Test Output" },
|
||||
{ id: "coverage", label: "Coverage" },
|
||||
] as { id: string; label: string }[]
|
||||
).map(({ id, label }) => (
|
||||
<div
|
||||
key={id}
|
||||
data-testid={`placeholder-${id}`}
|
||||
style={{
|
||||
border: "1px solid #2a2a2a",
|
||||
borderRadius: "8px",
|
||||
padding: "10px 12px",
|
||||
background: "#161616",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontWeight: 600,
|
||||
fontSize: "0.8em",
|
||||
color: "#555",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</div>
|
||||
<div style={{ fontSize: "0.75em", color: "#444" }}>
|
||||
Coming soon
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user