* feat(apps): add AppListItem and AppDetailPanel groups (#1261) Two presentational groups for the upcoming AppsScreen. AppListItem (clients/web/src/components/groups/AppListItem/): - Props { tool, selected, onClick } - Renders tool.icons[0] (sized sm) when present, label (title ?? name), two-line clamped description, and a trailing chevron affordance. - Reuses ToolListItem's selected-state highlight (UnstyledButton bg via --mantine-primary-color-light). AppDetailPanel (clients/web/src/components/groups/AppDetailPanel/): - Props { tool, formValues, isOpening, onFormChange, onOpenApp } - Layout: optional icon + title row, optional description, divider, SchemaForm bound to tool.inputSchema, full-width "Open App" button. - "Open App" is disabled while isOpening or while any required input field is empty (validation derived from tool.inputSchema.required). - No annotations, progress, or cancel — execution is delegated to the embedded app, not run as an MCP tool call from the panel. Convention notes: - Issue text references @tabler IconChevronRight / IconPlayerPlay; the v2 web client already uses react-icons (md/ri namespaces), so this PR uses MdChevronRight and MdPlayArrow for consistency rather than introducing a second icon library. Stories: AppListItem (Default, Selected, WithIcon, NoDescription, LongName); AppDetailPanel (NoFields, SimpleStringParam, MultipleParams, WithIcon, Opening, ComplexSchema). Tests: 22 cases covering rendering branches, callback wiring, and the disabled-button derivation. Per-file coverage 100% across lines, statements, functions, and branches for both new components. Closes #1261 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(stories): inline SVG for WithIcon stories so they render reliably The Wikipedia-hosted PNG used in the AppListItem and AppDetailPanel WithIcon stories returned a broken image in Storybook (rate-limited / hot-link blocked). Replace it with an inline SVG sun icon as a data URI so the stories render offline, in Chromatic, and on any network. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(apps): address PR #1273 review feedback - Extract resolveDisplayLabel(name, title?) to src/utils/toolUtils.ts and migrate all four call sites (AppListItem, AppDetailPanel, ToolListItem, ToolDetailPanel) to consume it. Avoids the fourth copy when the next list/detail screen arrives. Includes a small unit suite. - AppDetailPanel: only render the <Divider/> when the schema has at least one property — previously a no-input app showed an orphaned separator between the description and the Open App button. - AppDetailPanel: align PanelTitle truncate to `true` (matches the ItemLabel form used in AppListItem / ToolListItem); behaviorally identical, just less surprising for future readers. - AppDetailPanel: add an inline comment explaining that SchemaForm is disabled only while isOpening, not while validation fails — users should be able to keep filling required fields. The validation gate lives on the Open App button. - Extract the inline SUN_ICON_SVG data URI used by the WithIcon stories to src/test/fixtures/storyIcons.ts so the next story that needs it doesn't grow a third copy. - Tests: add the AppDetailPanel divider-conditional cases (rendered with fields, omitted without). Per-file coverage stays at 100% for AppListItem, AppDetailPanel, ToolListItem, ToolDetailPanel, and the new toolUtils. Suite: 70 files / 618 tests passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(toolUtils): rename empty-string test for readability Drops the doubled "is ... is preserved" phrase in the test name. Behavior unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
148 lines
3.5 KiB
TypeScript
148 lines
3.5 KiB
TypeScript
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
import { fn } from "storybook/test";
|
|
import { SUN_ICON_SVG } from "../../../test/fixtures/storyIcons";
|
|
import { AppDetailPanel } from "./AppDetailPanel";
|
|
|
|
const meta: Meta<typeof AppDetailPanel> = {
|
|
title: "Groups/AppDetailPanel",
|
|
component: AppDetailPanel,
|
|
args: {
|
|
formValues: {},
|
|
isOpening: false,
|
|
onFormChange: fn(),
|
|
onOpenApp: fn(),
|
|
},
|
|
};
|
|
|
|
export default meta;
|
|
type Story = StoryObj<typeof AppDetailPanel>;
|
|
|
|
const noFieldsTool: Tool = {
|
|
name: "no_input_app",
|
|
title: "No Input App",
|
|
description: "An app that takes no parameters.",
|
|
inputSchema: { type: "object" },
|
|
};
|
|
|
|
const simpleTool: Tool = {
|
|
name: "greeting_app",
|
|
title: "Greeting App",
|
|
description: "Renders a personalized greeting.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
name: { type: "string", description: "The name to greet" },
|
|
},
|
|
required: ["name"],
|
|
},
|
|
};
|
|
|
|
const multiParamTool: Tool = {
|
|
name: "report_builder",
|
|
title: "Report Builder",
|
|
description: "Builds a report from a query and date range.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
query: { type: "string", description: "SQL query" },
|
|
from: { type: "string", description: "Start date (YYYY-MM-DD)" },
|
|
to: { type: "string", description: "End date (YYYY-MM-DD)" },
|
|
includeChart: { type: "boolean", description: "Render an inline chart" },
|
|
},
|
|
required: ["query"],
|
|
},
|
|
};
|
|
|
|
const iconTool: Tool = {
|
|
name: "weather_widget",
|
|
title: "Weather Widget",
|
|
description: "Displays the current weather for a given city.",
|
|
icons: [{ src: SUN_ICON_SVG, mimeType: "image/svg+xml" }],
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
city: { type: "string", description: "City name" },
|
|
},
|
|
required: ["city"],
|
|
},
|
|
};
|
|
|
|
const complexSchemaTool: Tool = {
|
|
name: "advanced_search",
|
|
title: "Advanced Search",
|
|
description: "Run a structured search across multiple sources.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
query: { type: "string", description: "Free-text query" },
|
|
sources: {
|
|
type: "array",
|
|
items: {
|
|
anyOf: [
|
|
{ const: "web", title: "Web" },
|
|
{ const: "docs", title: "Docs" },
|
|
{ const: "code", title: "Code" },
|
|
],
|
|
},
|
|
description: "Sources to include",
|
|
},
|
|
maxResults: {
|
|
type: "number",
|
|
description: "Maximum number of results",
|
|
},
|
|
filters: {
|
|
type: "object",
|
|
properties: {
|
|
language: { type: "string", description: "Language filter" },
|
|
recency: {
|
|
type: "string",
|
|
enum: ["day", "week", "month", "year"],
|
|
description: "Recency",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
required: ["query"],
|
|
},
|
|
};
|
|
|
|
export const NoFields: Story = {
|
|
args: { tool: noFieldsTool },
|
|
};
|
|
|
|
export const SimpleStringParam: Story = {
|
|
args: {
|
|
tool: simpleTool,
|
|
formValues: { name: "Ada" },
|
|
},
|
|
};
|
|
|
|
export const MultipleParams: Story = {
|
|
args: {
|
|
tool: multiParamTool,
|
|
formValues: { query: "SELECT 1" },
|
|
},
|
|
};
|
|
|
|
export const WithIcon: Story = {
|
|
args: {
|
|
tool: iconTool,
|
|
formValues: { city: "Reykjavik" },
|
|
},
|
|
};
|
|
|
|
export const Opening: Story = {
|
|
args: {
|
|
tool: simpleTool,
|
|
formValues: { name: "Ada" },
|
|
isOpening: true,
|
|
},
|
|
};
|
|
|
|
export const ComplexSchema: Story = {
|
|
args: {
|
|
tool: complexSchemaTool,
|
|
formValues: { query: "" },
|
|
},
|
|
};
|