Address PR UI QoL review feedback

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-05-19 14:56:45 -05:00
1 parent 19c01ada4e
commit 7c0f9261d4
9 files changed
+132 -3

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

@@ -47,6 +47,7 @@ afterEach(() => {
container?.remove();
container = null;
vi.restoreAllMocks();
vi.useRealTimers();
mockHealthApi.requestDevServerRestart.mockReset();
});
@@ -88,4 +89,25 @@ describe("DevRestartBanner", () => {
expect(mockHealthApi.requestDevServerRestart).not.toHaveBeenCalled();
});
it("re-enables the manual restart action when a request does not refresh the page", async () => {
vi.useFakeTimers();
const node = render();
const button = [...node.querySelectorAll("button")]
.find((entry) => entry.textContent?.includes("Restart now")) as HTMLButtonElement | undefined;
await act(async () => {
button?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(button?.disabled).toBe(true);
expect(node.textContent).toContain("Restart requested");
act(() => {
vi.advanceTimersByTime(30_000);
});
expect(button?.disabled).toBe(false);
expect(node.textContent).toContain("Restart now");
});
});
+11 -1
View File
@@ -1,7 +1,9 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { AlertTriangle, RotateCcw, TimerReset } from "lucide-react";
import { healthApi, type DevServerHealthStatus } from "../api/health";
const RESTART_PENDING_RESET_MS = 30_000;
function formatRelativeTimestamp(value: string | null): string | null {
if (!value) return null;
const timestamp = new Date(value).getTime();
@@ -29,6 +31,14 @@ function describeReason(devServer: DevServerHealthStatus): string {
export function DevRestartBanner({ devServer }: { devServer?: DevServerHealthStatus }) {
const [restartPending, setRestartPending] = useState(false);
useEffect(() => {
if (!restartPending) return;
const timeout = window.setTimeout(() => {
setRestartPending(false);
}, RESTART_PENDING_RESET_MS);
return () => window.clearTimeout(timeout);
}, [restartPending]);
if (!devServer?.enabled || !devServer.restartRequired) return null;
const currentDevServer = devServer;
+20
View File
@@ -814,6 +814,26 @@ describe("MarkdownEditor", () => {
});
});
it("caps rendered mention matches while keeping the menu scrollable", async () => {
const handleChange = vi.fn();
const mentions = Array.from({ length: 60 }, (_, index) => ({
id: `project:project-${index}`,
kind: "project" as const,
name: `Paperclip App ${index}`,
projectId: `project-${index}`,
projectColor: "#336699",
}));
const { menu, root } = await openMentionMenuFor(handleChange, mentions);
const options = Array.from(menu.querySelectorAll('button[type="button"]'));
expect(options).toHaveLength(50);
expect(menu.className).toContain("overflow-y-auto");
await act(async () => {
root.unmount();
});
});
it("scrolls the active mention option into view during keyboard navigation", async () => {
const handleChange = vi.fn();
const scrollIntoView = vi.fn();
+6 -2
View File
@@ -212,6 +212,7 @@ const MENTION_MENU_HEIGHT = 208;
const MENTION_MENU_PADDING = 8;
const MENTION_MENU_ROW_HEIGHT = 34;
const MENTION_MENU_CHROME_HEIGHT = 8;
const MAX_AUTOCOMPLETE_OPTIONS = 50;
/** Roughly one space-width of breathing room between the caret and the menu. */
const MENTION_MENU_CARET_GAP = 10;
@@ -648,10 +649,13 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
.filter((command) => {
if (!q) return true;
return command.aliases.some((alias) => alias.toLowerCase().includes(q));
});
})
.slice(0, MAX_AUTOCOMPLETE_OPTIONS);
}
if (!mentions) return [];
return mentions.filter((m) => m.name.toLowerCase().includes(q));
return mentions
.filter((m) => m.name.toLowerCase().includes(q))
.slice(0, MAX_AUTOCOMPLETE_OPTIONS);
}, [mentionState, mentions, slashCommands]);
useImperativeHandle(forwardedRef, () => ({
@@ -0,0 +1,73 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { DevRestartBanner } from "@/components/DevRestartBanner";
import type { DevServerHealthStatus } from "@/api/health";
const restartRequired: DevServerHealthStatus = {
enabled: true,
restartRequired: true,
reason: "backend_changes_and_pending_migrations",
lastChangedAt: new Date(Date.now() - 7 * 60_000).toISOString(),
changedPathCount: 4,
changedPathsSample: [
"server/src/routes/health.ts",
"server/src/dev-runner.ts",
"packages/shared/src/api.ts",
],
pendingMigrations: ["0042_dev_server_health.sql"],
autoRestartEnabled: false,
activeRunCount: 0,
waitingForIdle: false,
lastRestartAt: new Date(Date.now() - 45 * 60_000).toISOString(),
};
const restartWaitingForIdle: DevServerHealthStatus = {
...restartRequired,
reason: "backend_changes",
pendingMigrations: [],
autoRestartEnabled: true,
activeRunCount: 2,
waitingForIdle: true,
};
function DevOpsSurfacesStory() {
return (
<div className="space-y-6 p-6">
<section className="overflow-hidden border border-border bg-background">
<div className="border-b border-border px-5 py-4">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Dev server restart banner
</div>
</div>
<DevRestartBanner devServer={restartRequired} />
</section>
<section className="max-w-[390px] overflow-hidden border border-border bg-background">
<div className="border-b border-border px-4 py-3">
<div className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
Mobile waiting state
</div>
</div>
<DevRestartBanner devServer={restartWaitingForIdle} />
</section>
</div>
);
}
const meta = {
title: "Product/Dev Ops Surfaces",
component: DevOpsSurfacesStory,
parameters: {
docs: {
description: {
component:
"Shows local development recovery surfaces, including the restart-required banner and its manual restart action.",
},
},
},
} satisfies Meta<typeof DevOpsSurfacesStory>;
export default meta;
type Story = StoryObj<typeof meta>;
export const DevOpsSurfaces: Story = {};