fix: improve console workspace usability
This commit is contained in:
@@ -270,13 +270,14 @@ tbody tr:hover {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* The workspace keeps connection, navigation, content, and receipts visible together. */
|
||||
/* Operation receipts belong to the route that produced them; the shell reserves
|
||||
its width for navigation and the active workflow surface. */
|
||||
.console-workspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(9rem, 13rem) minmax(0, 1fr) minmax(18rem, 24rem);
|
||||
grid-template-columns: minmax(9rem, 13rem) minmax(0, 1fr);
|
||||
grid-template-areas:
|
||||
"header header header"
|
||||
"nav main evidence";
|
||||
"header header"
|
||||
"nav main";
|
||||
gap: 0 1rem;
|
||||
min-height: 100vh;
|
||||
padding: 1rem;
|
||||
@@ -379,23 +380,6 @@ tbody tr:hover {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.console-workspace__evidence {
|
||||
grid-area: evidence;
|
||||
align-self: start;
|
||||
min-width: 0;
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
max-height: calc(100vh - 2rem);
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.console-workspace__evidence h2 {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--color-surface);
|
||||
}
|
||||
|
||||
.workspace-route-pending {
|
||||
min-height: 18rem;
|
||||
margin: 0;
|
||||
@@ -1054,9 +1038,13 @@ tbody tr:hover {
|
||||
}
|
||||
|
||||
.capability-discovery__results,
|
||||
.capability-discovery__detail {
|
||||
.capability-discovery__detail,
|
||||
.capability-playground {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
max-height: calc(100vh - 10rem);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.capability-discovery__section-heading {
|
||||
@@ -1803,6 +1791,8 @@ tbody tr:hover {
|
||||
|
||||
.authoring-graph {
|
||||
overflow: hidden;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(20rem, 1fr) minmax(0, auto);
|
||||
}
|
||||
|
||||
.authoring-graph__heading {
|
||||
@@ -1820,11 +1810,15 @@ tbody tr:hover {
|
||||
}
|
||||
|
||||
.authoring-graph .workflow-graph {
|
||||
height: 34rem;
|
||||
height: 100%;
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
.authoring-graph__routes {
|
||||
min-height: 0;
|
||||
max-height: 12rem;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.65rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
@@ -2002,13 +1996,11 @@ tbody tr:nth-child(10) { animation-delay: 270ms; }
|
||||
grid-template-areas:
|
||||
"header"
|
||||
"nav"
|
||||
"main"
|
||||
"evidence";
|
||||
"main";
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.console-workspace__nav,
|
||||
.console-workspace__evidence {
|
||||
.console-workspace__nav {
|
||||
position: static;
|
||||
}
|
||||
|
||||
@@ -2034,10 +2026,6 @@ tbody tr:nth-child(10) { animation-delay: 270ms; }
|
||||
border-bottom-color: var(--color-signal-green);
|
||||
}
|
||||
|
||||
.console-workspace__evidence {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.capability-discovery__panes {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -2048,10 +2036,12 @@ tbody tr:nth-child(10) { animation-delay: 270ms; }
|
||||
|
||||
.capability-discovery__detail {
|
||||
position: static;
|
||||
max-height: 70dvh;
|
||||
}
|
||||
|
||||
.capability-playground {
|
||||
position: static;
|
||||
max-height: 70dvh;
|
||||
}
|
||||
|
||||
.capability-discovery__filters {
|
||||
|
||||
@@ -3,13 +3,16 @@ import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { initialState } from "../app/state.js";
|
||||
import { ConsoleShell } from "./ConsoleShell.js";
|
||||
|
||||
const globalStyles = readFileSync("src/styles/global.css", "utf8");
|
||||
|
||||
afterEach(() => cleanup());
|
||||
|
||||
describe("ConsoleShell", () => {
|
||||
it("renders the connection header, lifecycle rail, main content, and evidence surface", () => {
|
||||
it("renders the connection header, lifecycle rail, and main content without a global evidence panel", () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/console/discover"]}>
|
||||
<ConsoleShell
|
||||
@@ -27,13 +30,21 @@ describe("ConsoleShell", () => {
|
||||
expect(screen.getByRole("main", { name: "Console workspace" })).toHaveTextContent(
|
||||
"Discover content",
|
||||
);
|
||||
expect(screen.getByRole("complementary", { name: "Operation evidence" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("complementary", { name: "Operation evidence" })).toBeNull();
|
||||
expect(screen.getByRole("link", { name: "Discover" })).toHaveAttribute(
|
||||
"aria-current",
|
||||
"page",
|
||||
);
|
||||
});
|
||||
|
||||
it("reserves the workspace width for navigation and route content", () => {
|
||||
expect(globalStyles).toMatch(
|
||||
/\.console-workspace\s*\{[\s\S]*?grid-template-columns:\s*minmax\(9rem, 13rem\)\s+minmax\(0, 1fr\)/,
|
||||
);
|
||||
expect(globalStyles).toContain('"nav main"');
|
||||
expect(globalStyles).not.toContain('"nav main evidence"');
|
||||
});
|
||||
|
||||
it("renders all lifecycle links with route destinations", () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { ReactNode } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import type { ConnectionState } from "../app/state.js";
|
||||
import { ConnectionHeader } from "../components/ConnectionHeader.js";
|
||||
import { EvidenceLedger } from "./EvidenceLedger.js";
|
||||
|
||||
type Props = {
|
||||
readonly connection: ConnectionState;
|
||||
@@ -59,9 +58,5 @@ export const ConsoleShell = ({
|
||||
<main id="console-workspace-main" aria-label="Console workspace" tabIndex={-1}>
|
||||
{children}
|
||||
</main>
|
||||
<aside aria-label="Operation evidence" className="console-workspace__evidence">
|
||||
<h2>Operation evidence</h2>
|
||||
<EvidenceLedger records={connection.evidence} />
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -149,13 +149,13 @@ describe("ConsoleWorkspace", () => {
|
||||
expect(screen.getByTestId("executor-state")).toHaveTextContent("available");
|
||||
expect(screen.getByTestId("write-executor-state")).toHaveTextContent("available");
|
||||
expect(screen.getByTestId("executor-stable")).toHaveTextContent("yes");
|
||||
expect(screen.getAllByText("Health check")).toHaveLength(1);
|
||||
expect(screen.getByTestId("evidence-ids")).toHaveTextContent("workflow.health-0");
|
||||
expect(mockedCallOperation).not.toHaveBeenCalled();
|
||||
|
||||
await userEvent.click(screen.getByRole("button", { name: "Navigate to drafts" }));
|
||||
|
||||
expect(await screen.findByText("Drafts route")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("Health check")).toHaveLength(1);
|
||||
expect(screen.getByTestId("evidence-ids")).toHaveTextContent("workflow.health-0");
|
||||
expect(screen.getByTestId("connected-target")).toHaveTextContent("http://one.example/rpc");
|
||||
});
|
||||
|
||||
@@ -273,6 +273,6 @@ describe("ConsoleWorkspace", () => {
|
||||
"http://two.example/rpc",
|
||||
);
|
||||
});
|
||||
expect(screen.getAllByText("Health check")).toHaveLength(1);
|
||||
expect(screen.getByTestId("evidence-ids")).toHaveTextContent("workflow.health-0");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -188,6 +188,21 @@ describe("DraftWorkbench", () => {
|
||||
expect(within(inspector as HTMLElement).getByText("demo.collect")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens the context inspector when a route outcome is selected on mobile", async () => {
|
||||
setViewport(390);
|
||||
const user = userEvent.setup();
|
||||
const { container } = render(<DraftWorkbench draft={workspace} />);
|
||||
|
||||
const routes = screen.getByLabelText("Route outcomes");
|
||||
await user.click(within(routes).getByRole("button", { name: /collect.*ok/i }));
|
||||
|
||||
const inspector = container.querySelector("#draft-workbench-inspector");
|
||||
expect(inspector).toHaveAttribute("open", "");
|
||||
expect(within(inspector as HTMLElement).getByRole("heading", { name: "Route inspector" })).toBeInTheDocument();
|
||||
expect(within(inspector as HTMLElement).getByRole("textbox", { name: "Source step" })).toHaveValue("collect");
|
||||
expect(within(inspector as HTMLElement).getByRole("textbox", { name: "Outcome" })).toHaveValue("ok");
|
||||
});
|
||||
|
||||
it("keeps a dirty inspector form mounted and intact across mobile close and reopen", async () => {
|
||||
setViewport(390);
|
||||
mockedUseAuthoringCapabilityDetail.mockReturnValue({
|
||||
|
||||
@@ -164,8 +164,11 @@ export const DraftWorkbench = ({
|
||||
(nextSelection: WorkbenchSelection): void => {
|
||||
controller.select(nextSelection);
|
||||
onSelectionChange?.(nextSelection);
|
||||
if (isMobile && (nextSelection.kind === "edge" || nextSelection.kind === "node")) {
|
||||
setOpenSheet("inspector");
|
||||
}
|
||||
},
|
||||
[controller, onSelectionChange],
|
||||
[controller, isMobile, onSelectionChange],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { cleanup, render, screen, within } from "@testing-library/react";
|
||||
import { readFileSync } from "node:fs";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
@@ -33,6 +34,7 @@ vi.mock("../domain/draft-authoring-client.js", () => ({
|
||||
|
||||
const mockedUseCapabilityDiscovery = vi.mocked(useCapabilityDiscovery);
|
||||
const mockedUseDraftWorkspace = vi.mocked(useDraftWorkspace);
|
||||
const globalStyles = readFileSync("src/styles/global.css", "utf8");
|
||||
const mockedUseConsoleWorkspace = vi.mocked(useConsoleWorkspace);
|
||||
const mockedCreateDraftAuthoringClient = vi.mocked(createDraftAuthoringClient);
|
||||
|
||||
@@ -335,4 +337,10 @@ describe("DiscoverRoute", () => {
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps both discovery panes scrollable within the viewport", () => {
|
||||
expect(globalStyles).toMatch(
|
||||
/\.capability-discovery__results,\s*\.capability-discovery__detail,\s*\.capability-playground\s*\{[\s\S]*?max-height:\s*calc\(100vh - 10rem\);[\s\S]*?overflow-y:\s*auto/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,6 +150,12 @@ describe("DraftDetailRoute", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps route outcomes scrollable inside the bounded graph panel", () => {
|
||||
expect(globalStyles).toMatch(
|
||||
/\.authoring-graph__routes\s*\{[\s\S]*?min-height:\s*0;[\s\S]*?overflow-y:\s*auto/,
|
||||
);
|
||||
});
|
||||
|
||||
it("explains when the full draft document was not returned", () => {
|
||||
mockedUseDraftWorkspace.mockReturnValue(
|
||||
controller({ selected: workspace({ draft: null }) }),
|
||||
|
||||
Reference in New Issue
Block a user