milestone 18: collapse duplicated infrastructure into shared listener core, crud list helper, resource shells, transport race, name and line helpers, ui modules
CI / test (push) Successful in 1m22s
CI / test-aarch64 (push) Successful in 5m6s
CI / frontend (push) Successful in 45s
CI / cross (push) Successful in 7m53s
CI / docker (push) Failing after 1h10m57s

This commit is contained in:
2026-08-07 18:20:30 +02:00
parent c50c6d285a
commit 6f67940995
82 changed files with 3167 additions and 3114 deletions
+24
View File
@@ -0,0 +1,24 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { ApiError } from "@/lib/api";
import InlineError from "./InlineError";
test("no retry button without onRetry", () => {
render(<InlineError error={new ApiError(409, "already exists")} />);
expect(screen.getByRole("alert").textContent).toBe("already exists");
expect(screen.queryByRole("button", { name: "Retry" })).toBeNull();
});
test("onRetry renders a focusable retry button that calls back", () => {
const onRetry = vi.fn();
render(<InlineError error={new ApiError(500, "internal")} onRetry={onRetry} />);
const button = screen.getByRole("button", { name: "Retry" });
expect(button.className).toContain("focus-visible:outline-2");
fireEvent.click(button);
expect(onRetry).toHaveBeenCalledTimes(1);
});
test("a null error renders nothing even with onRetry", () => {
const { container } = render(<InlineError error={null} onRetry={() => undefined} />);
expect(container.innerHTML).toBe("");
});
+14 -2
View File
@@ -1,8 +1,12 @@
import { useEffect, useState } from "react";
import { ApiError } from "@/lib/api";
import { focusRing } from "@/ui/classes";
/** Inline mutation error per ruling 17: 400/409 messages verbatim, 429 with countdown. */
export default function InlineError({ error }: { error: unknown }) {
/**
* Inline mutation error per ruling 17: 400/409 messages verbatim, 429 with
* countdown. Pass `onRetry` to append a retry button for a failed query.
*/
export default function InlineError({ error, onRetry }: { error: unknown; onRetry?: () => void }) {
const retryAfter = error instanceof ApiError && error.status === 429 ? (error.retryAfter ?? null) : null;
const [remaining, setRemaining] = useState<number | null>(retryAfter);
@@ -34,6 +38,14 @@ export default function InlineError({ error }: { error: unknown }) {
return (
<p role="alert" className="mt-2 text-sm text-red-600 dark:text-red-400">
{message}
{onRetry !== undefined && (
<>
{" "}
<button type="button" onClick={onRetry} className={`font-medium underline ${focusRing}`}>
Retry
</button>
</>
)}
</p>
);
}