mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 00:30:19 +02:00
Address PR UI QoL review feedback
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
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");
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 = {};
|
||||
Reference in new issue
Block a user