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("⏳");
|
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) => {
|
{items.map((item) => {
|
||||||
const itemNumber = item.story_id.match(/^(\d+)/)?.[1];
|
const itemNumber = item.story_id?.match(/^(\d+)/)?.[1];
|
||||||
const itemType = getWorkItemType(item.story_id);
|
const itemType = item.story_id
|
||||||
|
? getWorkItemType(item.story_id)
|
||||||
|
: "unknown";
|
||||||
const borderColor = TYPE_COLORS[itemType];
|
const borderColor = TYPE_COLORS[itemType];
|
||||||
const typeLabel = TYPE_LABELS[itemType];
|
const typeLabel = TYPE_LABELS[itemType];
|
||||||
const hasMergeFailure = Boolean(item.merge_failure);
|
const hasMergeFailure = Boolean(item.merge_failure);
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ export function WorkItemDetailPanel({
|
|||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!loading && !error && content !== null && (
|
{!loading && !error && content != null && (
|
||||||
<div
|
<div
|
||||||
data-testid="detail-panel-content"
|
data-testid="detail-panel-content"
|
||||||
className="markdown-body"
|
className="markdown-body"
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export const STATUS_COLORS: Record<AgentStatusValue, string> = {
|
|||||||
* them again inside the markdown body creates duplicate information.
|
* them again inside the markdown body creates duplicate information.
|
||||||
*/
|
*/
|
||||||
export function stripDisplayContent(content: string): string {
|
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;
|
let text = content;
|
||||||
// Strip YAML front matter (--- ... ---)
|
// Strip YAML front matter (--- ... ---)
|
||||||
if (text.startsWith("---")) {
|
if (text.startsWith("---")) {
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
|
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
<ErrorBoundary>
|
||||||
<App />
|
<App />
|
||||||
|
</ErrorBoundary>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user