Phase 4: Move screen selection state into the screens themselves
ToolsScreen, PromptsScreen, and ResourcesScreen each tracked the user's current selection via `selectedXxxName` / `onSelectXxx` props, hoisting screen-local UI state into whatever rendered them. InspectorView never threaded those callbacks anywhere, so clicks in the controls panel did not display the matching detail panel — the detail panel only rendered if a parent fed selection back in. Move selection into `useState` inside each screen and drop the `selectedXxxName` / `onSelectXxx` props. MCP-derived data (`callState`, `getPromptState`, `readState`) keeps flowing in from `InspectorView` as props. ResourcesScreen wraps `onReadResource` so a template-expanded URI becomes the selected resource for preview rendering. Update each screen's stories to drop the deleted props. Stories that need a pre-selected item use a `play` function to click it — no component coupling to Storybook. Add Phase 4.3 to the interfaces plan describing the rule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d9b2009804
commit
4c1d2f2c34
2 changed files with 171 additions and 86 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { fn } from "storybook/test";
|
||||
import { fn, userEvent, within } from "storybook/test";
|
||||
import { PromptsScreen } from "./PromptsScreen";
|
||||
import type { GetPromptState } from "./PromptsScreen";
|
||||
|
||||
|
|
@ -10,7 +10,6 @@ const meta: Meta<typeof PromptsScreen> = {
|
|||
parameters: { layout: "fullscreen" },
|
||||
args: {
|
||||
onRefreshList: fn(),
|
||||
onSelectPrompt: fn(),
|
||||
onGetPrompt: fn(),
|
||||
onCopyMessages: fn(),
|
||||
listChanged: false,
|
||||
|
|
@ -70,6 +69,11 @@ const translateResult: GetPromptState = {
|
|||
},
|
||||
};
|
||||
|
||||
async function selectPromptByName(canvasElement: HTMLElement, name: string) {
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(await canvas.findByText(name));
|
||||
}
|
||||
|
||||
export const NoSelection: Story = {
|
||||
args: {
|
||||
prompts: samplePrompts,
|
||||
|
|
@ -79,36 +83,44 @@ export const NoSelection: Story = {
|
|||
export const PromptSelected: Story = {
|
||||
args: {
|
||||
prompts: samplePrompts,
|
||||
selectedPromptName: "translate",
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
await selectPromptByName(canvasElement, "translate");
|
||||
},
|
||||
};
|
||||
|
||||
export const WithResult: Story = {
|
||||
args: {
|
||||
prompts: samplePrompts,
|
||||
selectedPromptName: "translate",
|
||||
getPromptState: translateResult,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
await selectPromptByName(canvasElement, "translate");
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
prompts: samplePrompts,
|
||||
selectedPromptName: "translate",
|
||||
getPromptState: { status: "pending" },
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
await selectPromptByName(canvasElement, "translate");
|
||||
},
|
||||
};
|
||||
|
||||
export const WithError: Story = {
|
||||
args: {
|
||||
prompts: samplePrompts,
|
||||
selectedPromptName: "translate",
|
||||
getPromptState: {
|
||||
status: "error",
|
||||
error:
|
||||
'Prompt "translate" requires argument "text" but none was provided. Please fill in all required arguments before submitting.',
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
await selectPromptByName(canvasElement, "translate");
|
||||
},
|
||||
};
|
||||
|
||||
export const ListChanged: Story = {
|
||||
|
|
|
|||
|
|
@ -25,11 +25,9 @@ export interface GetPromptState {
|
|||
|
||||
export interface PromptsScreenProps {
|
||||
prompts: Prompt[];
|
||||
selectedPromptName?: string;
|
||||
getPromptState?: GetPromptState;
|
||||
listChanged: boolean;
|
||||
onRefreshList: () => void;
|
||||
onSelectPrompt: (name: string) => void;
|
||||
onGetPrompt: (name: string, args: Record<string, string>) => void;
|
||||
onCopyMessages?: () => void;
|
||||
}
|
||||
|
|
@ -65,14 +63,15 @@ const EmptyState = Text.withProps({
|
|||
|
||||
export function PromptsScreen({
|
||||
prompts,
|
||||
selectedPromptName,
|
||||
getPromptState,
|
||||
listChanged,
|
||||
onRefreshList,
|
||||
onSelectPrompt,
|
||||
onGetPrompt,
|
||||
onCopyMessages,
|
||||
}: PromptsScreenProps) {
|
||||
const [selectedPromptName, setSelectedPromptName] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
const [argumentValues, setArgumentValues] = useState<Record<string, string>>(
|
||||
{},
|
||||
);
|
||||
|
|
@ -91,7 +90,7 @@ export function PromptsScreen({
|
|||
onRefreshList={onRefreshList}
|
||||
onSelectPrompt={(name) => {
|
||||
setArgumentValues({});
|
||||
onSelectPrompt(name);
|
||||
setSelectedPromptName(name);
|
||||
}}
|
||||
/>
|
||||
</SidebarCard>
|
||||
|
|
|
|||
Loading…
Reference in a new issue