huskies: merge 1059
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
/** React error boundary that catches render-time exceptions and shows a
|
||||
* recoverable error UI instead of a white screen. */
|
||||
import * as React from "react";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
/** Catches uncaught render exceptions in its subtree and displays a message. */
|
||||
export class ErrorBoundary extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { error };
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
this.setState({ error: null });
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100vh",
|
||||
background: "#0d1117",
|
||||
color: "#e6edf3",
|
||||
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
|
||||
gap: "16px",
|
||||
padding: "32px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: "2em" }}>⚠</div>
|
||||
<div style={{ fontWeight: 600, fontSize: "1.1em" }}>
|
||||
Something went wrong
|
||||
</div>
|
||||
<div style={{ color: "#8b949e", fontSize: "0.9em", maxWidth: "480px" }}>
|
||||
{this.state.error.message}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={this.handleReset}
|
||||
style={{
|
||||
padding: "8px 18px",
|
||||
borderRadius: "6px",
|
||||
border: "1px solid #30363d",
|
||||
background: "#21262d",
|
||||
color: "#e6edf3",
|
||||
cursor: "pointer",
|
||||
fontSize: "0.9em",
|
||||
}}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
@@ -490,3 +490,39 @@ describe("StagePanel", () => {
|
||||
expect(icon).toHaveTextContent("⏳");
|
||||
});
|
||||
});
|
||||
|
||||
describe("StagePanel - defensive rendering", () => {
|
||||
it("renders without exception when a story is missing its name field", () => {
|
||||
const items = [
|
||||
{
|
||||
story_id: "60_story_no_name",
|
||||
name: undefined as unknown as string,
|
||||
error: null,
|
||||
merge_failure: null,
|
||||
agent: null,
|
||||
review_hold: null,
|
||||
qa: null,
|
||||
depends_on: null,
|
||||
},
|
||||
];
|
||||
expect(() => render(<StagePanel title="Current" items={items} />)).not.toThrow();
|
||||
expect(screen.getByTestId("card-60_story_no_name")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders without exception when a story is missing its story_id field", () => {
|
||||
const items = [
|
||||
{
|
||||
story_id: undefined as unknown as string,
|
||||
name: "Orphaned Story",
|
||||
error: null,
|
||||
merge_failure: null,
|
||||
agent: null,
|
||||
review_hold: null,
|
||||
qa: null,
|
||||
depends_on: null,
|
||||
},
|
||||
];
|
||||
expect(() => render(<StagePanel title="Current" items={items} />)).not.toThrow();
|
||||
expect(screen.getByText("Orphaned Story")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -313,8 +313,10 @@ export function StagePanel({
|
||||
}}
|
||||
>
|
||||
{items.map((item) => {
|
||||
const itemNumber = item.story_id.match(/^(\d+)/)?.[1];
|
||||
const itemType = getWorkItemType(item.story_id);
|
||||
const itemNumber = item.story_id?.match(/^(\d+)/)?.[1];
|
||||
const itemType = item.story_id
|
||||
? getWorkItemType(item.story_id)
|
||||
: "unknown";
|
||||
const borderColor = TYPE_COLORS[itemType];
|
||||
const typeLabel = TYPE_LABELS[itemType];
|
||||
const hasMergeFailure = Boolean(item.merge_failure);
|
||||
|
||||
@@ -258,7 +258,7 @@ export function WorkItemDetailPanel({
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{!loading && !error && content !== null && (
|
||||
{!loading && !error && content != null && (
|
||||
<div
|
||||
data-testid="detail-panel-content"
|
||||
className="markdown-body"
|
||||
|
||||
@@ -24,6 +24,9 @@ export const STATUS_COLORS: Record<AgentStatusValue, string> = {
|
||||
* them again inside the markdown body creates duplicate information.
|
||||
*/
|
||||
export function stripDisplayContent(content: string): string {
|
||||
// Guard: content may be undefined/null at runtime if the server response is
|
||||
// missing the field (e.g. a tombstoned story returns an error object).
|
||||
if (!content) return "";
|
||||
let text = content;
|
||||
// Strip YAML front matter (--- ... ---)
|
||||
if (text.startsWith("---")) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import * as React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user