-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
Core: Add "open in editor" feature #32452
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
9 files reviewed, 2 comments
if (!(storyId && isLocal && importPath)) { | ||
return null; | ||
} | ||
const file = importPath; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: redundant variable assignment - importPath
can be used directly instead of assigning to file
const file = importPath; | |
return ( | |
<IconButton | |
key="open-in-editor" | |
onClick={() => openInEditor(importPath)} | |
title="Open in editor" | |
aria-label="Open in editor" | |
> | |
<VSCodeIcon /> | |
</IconButton> | |
); |
}) => { | ||
const buttonText = status === 'errored' ? 'Scroll to error' : 'Scroll to end'; | ||
const theme = useTheme(); | ||
const onOpenInEditor = async (file: string | undefined) => openInEditor(file); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: The async function wrapper is unnecessary since openInEditor
already returns a Promise and the result isn't awaited
const onOpenInEditor = async (file: string | undefined) => openInEditor(file); | |
const onOpenInEditor = (file: string | undefined) => openInEditor(file); |
View your CI Pipeline Execution ↗ for commit cc2ffba
☁️ Nx Cloud last updated this comment at |
Package BenchmarksCommit: The following packages have significant changes to their size or dependencies:
|
Before | After | Difference | |
---|---|---|---|
Dependency count | 43 | 43 | 0 |
Self size | 30.04 MB | 30.13 MB | 🚨 +94 KB 🚨 |
Dependency size | 17.30 MB | 17.30 MB | 0 B |
Bundle Size Analyzer | Link | Link |
@storybook/cli
Before | After | Difference | |
---|---|---|---|
Dependency count | 187 | 187 | 0 |
Self size | 886 KB | 886 KB | 0 B |
Dependency size | 79.64 MB | 79.73 MB | 🚨 +94 KB 🚨 |
Bundle Size Analyzer | Link | Link |
@storybook/codemod
Before | After | Difference | |
---|---|---|---|
Dependency count | 169 | 169 | 0 |
Self size | 35 KB | 35 KB | 0 B |
Dependency size | 76.06 MB | 76.16 MB | 🚨 +94 KB 🚨 |
Bundle Size Analyzer | Link | Link |
create-storybook
Before | After | Difference | |
---|---|---|---|
Dependency count | 44 | 44 | 0 |
Self size | 1.55 MB | 1.55 MB | 🚨 +30 B 🚨 |
Dependency size | 47.34 MB | 47.43 MB | 🚨 +94 KB 🚨 |
Bundle Size Analyzer | node | node |
8a3af8f
to
5ab5822
Compare
… open-in-editor functionality to Subnav and Preview components, allowing users to open files directly in their code editor. Updated package dependencies and added related types for improved type safety.
0933141
to
1853097
Compare
…xt menu, as well as shortcuts
bfb941e
to
b665876
Compare
…onent. Updated package dependencies to include 'react-qr-code' and added related stories for testing. Enhanced sidebar context menu with new shortcut keys.
…omposed Storybooks
code/core/src/manager/components/preview/tools/open-in-editor.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (5)
code/core/src/manager/components/preview/tools/share.tsx (2)
15-15
: Consider migrating to qrcode.react for better TypeScript support.The current import suggests you've already migrated to
qrcode.react
which provides better TypeScript support and maintenance than the previously problematicreact-qr-code
. This is the correct choice.
91-96
: Extract copyStoryLink to avoid potential undefined access.While the code has been updated to use optional chaining (
shortcutKeys?.copyStoryLink
), extracting it to a variable would be cleaner and ensure consistency.Apply this diff for clarity:
const api = useStorybookApi(); const shortcutKeys = api.getShortcutKeys(); const enableShortcuts = !!shortcutKeys; const [copied, setCopied] = useState(false); -const copyStoryLink = shortcutKeys?.copyStoryLink; +const copyLinkKeys = shortcutKeys?.copyStoryLink;And update line 105:
-right: enableShortcuts ? <Shortcut keys={copyStoryLink} /> : null, +right: enableShortcuts ? <Shortcut keys={copyLinkKeys} /> : null,And line 141:
-}, [baseUrl, storyId, queryParams, copied, qrUrl, enableShortcuts, copyStoryLink]); +}, [baseUrl, storyId, queryParams, copied, qrUrl, enableShortcuts, copyLinkKeys]);code/core/src/component-testing/components/Subnav.tsx (2)
51-53
: Collapse conditions into a single, intention‑revealing boolean.Compute isEditorOpenable = Boolean(canOpenInEditor && importPath) and use it in the render. Keeps logic centralized and avoids accidental regressions.
Example (outside the diffed lines):
const isEditorOpenable = Boolean(canOpenInEditor && importPath); ... {isEditorOpenable ? (/* clickable */) : (/* text */)}Also applies to: 127-129
192-214
: Guard importPath and use a semantic button; avoid asserting undefined.Clickable branch should render only when a valid importPath exists; otherwise you risk firing a request with “undefined” and creating a dangling listener. Also, render a real button for a11y. Add a label fallback when storyFileName is absent.
- {canOpenInEditor ? ( + {canOpenInEditor && Boolean(importPath) ? ( <WithTooltip trigger="hover" hasChrome={false} tooltip={<Note note="Open in editor" />} > - <StyledLocation - aria-label="Open in editor" - onClick={() => { - openInEditor({ - file: importPath as string, - }); - }} - > - {storyFileName} - </StyledLocation> + <StyledLocation + as="button" + type="button" + aria-label="Open in editor" + onClick={() => openInEditor({ file: importPath! })} + > + {storyFileName ?? importPath!.split(/[\\/]/).pop()} + </StyledLocation> </WithTooltip> ) : ( - <StyledLocation isText={true}>{storyFileName}</StyledLocation> + <StyledLocation isText> + {storyFileName ?? importPath?.split(/[\\/]/).pop()} + </StyledLocation> )}code/core/src/manager-api/lib/open-in-editor.ts (1)
14-25
: Prevent dangling listeners: add timeout/cleanup and guard empty file.If no response arrives (e.g., no middleware in prod/composed), the promise never resolves and the listener leaks. Also bail out early on empty file.
export async function openInEditor(payload: { file: string; line?: number; column?: number; }): Promise<OpenInEditorResponsePayload> { - return new Promise((resolve) => { - const channel = addons.getChannel(); - const { file, line, column } = payload; - const handler = (res: OpenInEditorResponsePayload) => { - if (res.file === file && res.line === line && res.column === column) { - channel.off(OPEN_IN_EDITOR_RESPONSE, handler); - resolve(res); - } - }; - channel.on(OPEN_IN_EDITOR_RESPONSE, handler); - channel.emit(OPEN_IN_EDITOR_REQUEST, payload); - }); + return new Promise((resolve) => { + const channel = addons.getChannel(); + const { file, line, column } = payload; + if (!file?.trim()) { + resolve({ file, line, column, error: 'Missing file path' }); + return; + } + let timer: ReturnType<typeof setTimeout>; + const handler = (res: OpenInEditorResponsePayload) => { + if (res.file === file && res.line === line && res.column === column) { + channel.off(OPEN_IN_EDITOR_RESPONSE, handler); + clearTimeout(timer); + resolve(res); + } + }; + channel.on(OPEN_IN_EDITOR_RESPONSE, handler); + channel.emit(OPEN_IN_EDITOR_REQUEST, payload); + timer = setTimeout(() => { + channel.off(OPEN_IN_EDITOR_RESPONSE, handler); + resolve({ file, line, column, error: 'Timed out waiting for editor response' }); + }, 10000); + }); }
🧹 Nitpick comments (9)
code/core/src/manager-api/lib/stories.ts (1)
334-338
: Add defensive check before accessing children array.While the component node should always have children (established by prior logic), adding a safety check prevents potential runtime errors.
Apply this diff to add a safety check:
if (item.type === 'component') { // attach importPath to the component node which should be the same for all children // this way we can add "open in editor" to the component node - item.importPath = acc[item.children[0]].importPath; + if (item.children?.length > 0 && acc[item.children[0]]?.importPath) { + item.importPath = acc[item.children[0]].importPath; + } }code/core/src/manager/components/preview/tools/share.tsx (1)
155-157
: Consider URL construction robustness.The logic assumes
STORYBOOK_NETWORK_ADDRESS
is a complete URL with protocol. Consider validating or normalizing it to handle edge cases where the address might be provided without a protocol.Consider adding validation:
-const storyUrl = global.STORYBOOK_NETWORK_ADDRESS - ? `${global.STORYBOOK_NETWORK_ADDRESS}${window.location.search}` - : window.location.href; +const networkAddress = global.STORYBOOK_NETWORK_ADDRESS; +const storyUrl = networkAddress + ? networkAddress.startsWith('http') + ? `${networkAddress}${window.location.search}` + : `http://${networkAddress}${window.location.search}` + : window.location.href;code/e2e-tests/manager.spec.ts (1)
48-49
: Update selector to match renamed label.The test still references the old "Shortcuts" label but should use "Settings" to match the UI change.
Apply this diff:
// toggle with menu item -await sbPage.page.locator('[aria-label="Shortcuts"]').click(); +await sbPage.page.locator('[aria-label="Settings"]').click(); await sbPage.page.locator('#list-item-S').click();Also applies to: 132-133, 171-171, 222-222, 251-251
code/core/src/component-testing/components/Subnav.tsx (1)
79-83
: Reset native button chrome for StyledLocation.When rendered “as=button”, ensure no default button styling leaks through.
const StyledLocation = styled(P)<{ isText?: boolean }>(({ theme, isText }) => ({ color: isText ? theme.textMutedColor : theme.color.secondary, cursor: isText ? 'default' : 'pointer', fontWeight: isText ? theme.typography.weight.regular : theme.typography.weight.bold, + background: 'transparent', + border: 0, + padding: 0, justifyContent: 'flex-end', textAlign: 'right', whiteSpace: 'nowrap', marginTop: 'auto', marginBottom: 1, paddingRight: 15, fontSize: 13, }));code/core/src/manager-api/lib/open-in-editor.ts (1)
15-15
: Optional: avoid getChannel() to improve determinism/testability.Consider accepting an optional channel/api in args, defaulting to addons.getChannel(), so callers can inject and tests can stub.
code/core/src/manager/components/sidebar/Menu.tsx (4)
84-93
: Mobile label is inconsistent; use “Settings” for parityThe mobile button still says “About Storybook” while the desktop one is “Settings”. Align for consistency and a11y.
Apply:
- title="About Storybook" - aria-label="About Storybook" + title="Settings" + aria-label="Settings"
108-126
: Add aria-expanded/aria-haspopup for a11yExpose tooltip/menu state to assistive tech.
Apply:
<SidebarIconButton title="Settings" aria-label="Settings" + aria-haspopup="menu" + aria-expanded={isTooltipVisible} highlighted={!!isHighlighted} active={isTooltipVisible} size="medium" isMobile={false} >
17-22
: Avoid leaking styling-only props to the DOMhighlighted/isMobile may get forwarded to the DOM via IconButton. Use transient props and filter.
Apply:
-export const SidebarIconButton = styled(IconButton)< - ComponentProps<typeof Button> & { - highlighted: boolean; - isMobile: boolean; - } ->(({ highlighted, theme, isMobile }) => ({ +export const SidebarIconButton = styled(IconButton, { + // emotion option; supported by storybook/theming’s styled + shouldForwardProp: (prop) => prop !== '$highlighted' && prop !== '$isMobile', +})< + ComponentProps<typeof Button> & { + $highlighted: boolean; + $isMobile: boolean; + } +>(({ $highlighted: highlighted, theme, $isMobile: isMobile }) => ({Also update usages in this file to pass
$highlighted
/$isMobile
instead ofhighlighted
/isMobile
.
70-74
: Remove ts-expect-error by aligning onClick typesUse the Button/IconButton onClick type to satisfy TS and drop the suppression.
Apply:
export interface SidebarMenuProps { menu: MenuList; isHighlighted?: boolean; - onClick?: React.MouseEventHandler<HTMLButtonElement>; + onClick?: ComponentProps<typeof Button>['onClick']; }And remove the suppression:
- // @ts-expect-error (non strict) onClick={onClick}
Also applies to: 88-90
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
code/core/package.json
(3 hunks)code/core/src/component-testing/components/Subnav.stories.tsx
(2 hunks)code/core/src/component-testing/components/Subnav.tsx
(5 hunks)code/core/src/core-server/server-channel/open-in-editor-channel.ts
(1 hunks)code/core/src/manager-api/lib/open-in-editor.ts
(1 hunks)code/core/src/manager-api/lib/stories.ts
(1 hunks)code/core/src/manager-api/modules/shortcuts.ts
(4 hunks)code/core/src/manager/components/preview/tools/open-in-editor.tsx
(1 hunks)code/core/src/manager/components/preview/tools/share.tsx
(1 hunks)code/core/src/manager/components/sidebar/ContextMenu.tsx
(6 hunks)code/core/src/manager/components/sidebar/Menu.tsx
(1 hunks)code/e2e-tests/manager.spec.ts
(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
- code/core/src/core-server/server-channel/open-in-editor-channel.ts
- code/core/src/manager-api/modules/shortcuts.ts
- code/core/src/manager/components/preview/tools/open-in-editor.tsx
- code/core/package.json
- code/core/src/manager/components/sidebar/ContextMenu.tsx
🧰 Additional context used
📓 Path-based instructions (1)
code/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
code/**/*.{test,spec}.{ts,tsx}
: Place all test files under the code/ directory
Name test files as *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx
Files:
code/e2e-tests/manager.spec.ts
🧬 Code graph analysis (4)
code/e2e-tests/manager.spec.ts (1)
code/e2e-tests/util.ts (1)
SbPage
(8-203)
code/core/src/component-testing/components/Subnav.tsx (1)
code/core/src/manager-api/lib/open-in-editor.ts (1)
openInEditor
(9-26)
code/core/src/manager-api/lib/open-in-editor.ts (3)
code/core/src/manager-api/index.mock.ts (1)
openInEditor
(5-5)code/core/src/manager-api/index.ts (1)
openInEditor
(3-3)code/core/src/core-events/data/open-in-editor.ts (1)
OpenInEditorResponsePayload
(3-8)
code/core/src/manager/components/preview/tools/share.tsx (2)
code/core/src/manager-api/root.tsx (2)
Combo
(112-115)useStorybookApi
(292-295)code/core/src/manager/container/Menu.tsx (1)
Shortcut
(44-50)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: normal
- GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (8)
code/core/src/component-testing/components/Subnav.stories.tsx (2)
1-1
: LGTM!The React import is necessary for JSX syntax in the story file.
138-151
: LGTM! Story properly demonstrates the open-in-editor feature.The new story correctly showcases the "Open in editor" functionality with appropriate args configuration.
code/e2e-tests/manager.spec.ts (3)
19-34
: Comprehensive test coverage for Settings tooltip behavior.The test thoroughly validates tooltip visibility, keyboard handling (especially Escape key), and click-outside behavior. Well structured test case.
57-96
: Excellent coverage of context menu actions.The test comprehensively validates "Open in editor" functionality across component, docs, and story nodes, plus clipboard verification for "Copy story name". The hover/click interactions are well sequenced.
98-109
: Well-structured share actions test.The test properly validates the share functionality, including visibility checks and clipboard content verification after copying the story link.
code/core/src/component-testing/components/Subnav.tsx (1)
22-22
: LGTM: correct manager‑api import for openInEditor.code/core/src/manager-api/lib/open-in-editor.ts (1)
1-26
: All call sites use the {file: …} payload — guard potentially undefined file values.No bare-string calls found. Callers pass expressions that may be undefined: fullAPI.getCurrentStoryData().importPath, context.importPath, importPath (including "importPath as string"). Audit/guard these call sites or noop if file is falsy:
- code/core/src/manager/components/preview/tools/open-in-editor.tsx — openInEditor({ file: importPath })
- code/core/src/manager-api/modules/shortcuts.ts — openInEditor({ file: fullAPI.getCurrentStoryData().importPath })
- code/core/src/manager/components/sidebar/ContextMenu.tsx — openInEditor({ file: context.importPath })
- code/core/src/component-testing/components/Subnav.tsx — openInEditor({ file: importPath as string })
code/core/src/manager/components/sidebar/Menu.tsx (1)
116-117
: LGTM: “Settings” labelDesktop rename looks good and matches the new UX.
Run to ensure no lingering “Shortcuts” labels remain:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (8)
code/e2e-tests/manager.spec.ts (1)
93-95
: Consider adding assertion for exact clipboard content.The test currently uses
toContain('Primary')
which could match partial strings. Consider using a more precise assertion to ensure the exact story name is copied.- await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain( - 'Primary' - ); + await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toBe('Primary');code/core/src/manager-api/modules/open-in-editor.tsx (5)
16-18
: Remove redundant SubState interface.The
SubState
interface withnotifications
field appears to be unnecessary since this module doesn't manage notifications state - it only uses the notifications API to display errors. The notifications state is already managed by the notifications module.-export interface SubState { - notifications: API_Notification[]; -}
42-46
: Potential race condition with concurrent requests.The current implementation matches responses based on file, line, and column. If multiple requests for the same location are made concurrently, only the first one will resolve while others may hang indefinitely since the handler is removed after the first match.
Consider using a unique request ID to properly match request-response pairs:
- const handler = (res: OpenInEditorResponsePayload) => { - if (res.file === file && res.line === line && res.column === column) { - provider.channel?.off(OPEN_IN_EDITOR_RESPONSE, handler); - resolve(res); - } + const requestId = `${Date.now()}-${Math.random()}`; + const handler = (res: OpenInEditorResponsePayload & { requestId?: string }) => { + if (res.requestId === requestId || + (res.file === file && res.line === line && res.column === column)) { + provider.channel?.off(OPEN_IN_EDITOR_RESPONSE, handler); + resolve(res); + } }; provider.channel?.on(OPEN_IN_EDITOR_RESPONSE, handler); - provider.channel?.emit(OPEN_IN_EDITOR_REQUEST, payload); + provider.channel?.emit(OPEN_IN_EDITOR_REQUEST, { ...payload, requestId });
40-50
: Add timeout handling for unresponsive requests.The promise never rejects if no response is received, which could leave the UI in a pending state indefinitely.
Consider adding a timeout:
return new Promise((resolve) => { const { file, line, column } = payload; + let timeoutId: NodeJS.Timeout; const handler = (res: OpenInEditorResponsePayload) => { if (res.file === file && res.line === line && res.column === column) { + clearTimeout(timeoutId); provider.channel?.off(OPEN_IN_EDITOR_RESPONSE, handler); resolve(res); } }; + timeoutId = setTimeout(() => { + provider.channel?.off(OPEN_IN_EDITOR_RESPONSE, handler); + resolve({ file, line, column, error: 'Request timed out' }); + }, 5000); provider.channel?.on(OPEN_IN_EDITOR_RESPONSE, handler); provider.channel?.emit(OPEN_IN_EDITOR_REQUEST, payload); });
54-54
: Remove unused state declaration.The
state
variable initializes notifications but this module doesn't manage notification state.- const state: SubState = { notifications: [] };
And update the return statement:
return { api, - state, + state: {}, init: async () => {
67-68
: Improve error message fallback.The fallback uses logical OR which could show "Check the Storybook process..." even when
payload.error
is an empty string.- subHeadline: - payload.error || - 'Check the Storybook process on the command line for more details.', + subHeadline: payload.error || undefined, + ...(payload.error ? {} : { + subHeadline: 'Check the Storybook process on the command line for more details.' + }),Or more simply:
- subHeadline: - payload.error || - 'Check the Storybook process on the command line for more details.', + subHeadline: payload.error ? + payload.error : + 'Check the Storybook process on the command line for more details.',code/core/src/manager-api/modules/shortcuts.ts (2)
393-398
: Add null safety for getCurrentStoryData().The code assumes
getCurrentStoryData()
returns a valid object with animportPath
property, but it might return null or undefined.case 'openInEditor': { if (global.CONFIG_TYPE === 'DEVELOPMENT') { - fullAPI.openInEditor({ - file: fullAPI.getCurrentStoryData().importPath, - }); + const storyData = fullAPI.getCurrentStoryData(); + if (storyData?.importPath) { + fullAPI.openInEditor({ + file: storyData.importPath, + }); + } } break; }
409-410
: Consider adding user feedback for successful copy.The copy operation happens silently. Consider adding a notification to confirm the link was copied to clipboard.
case 'copyStoryLink': { - copy(window.location.href); + const success = copy(window.location.href); + if (success && fullAPI.addNotification) { + fullAPI.addNotification({ + id: 'copy-story-link', + content: { + headline: 'Story link copied to clipboard' + }, + duration: 2000, + }); + } break; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
code/yarn.lock
is excluded by!**/yarn.lock
,!**/*.lock
📒 Files selected for processing (12)
code/core/src/component-testing/components/InteractionsPanel.tsx
(4 hunks)code/core/src/component-testing/components/Panel.tsx
(3 hunks)code/core/src/component-testing/components/Subnav.tsx
(5 hunks)code/core/src/core-server/server-channel/open-in-editor-channel.ts
(1 hunks)code/core/src/manager-api/modules/notifications.ts
(1 hunks)code/core/src/manager-api/modules/open-in-editor.tsx
(1 hunks)code/core/src/manager-api/modules/shortcuts.ts
(4 hunks)code/core/src/manager-api/root.tsx
(2 hunks)code/core/src/manager/components/preview/tools/open-in-editor.tsx
(1 hunks)code/core/src/manager/components/sidebar/ContextMenu.tsx
(6 hunks)code/core/src/manager/globals/exports.ts
(2 hunks)code/e2e-tests/manager.spec.ts
(7 hunks)
✅ Files skipped from review due to trivial changes (1)
- code/core/src/manager-api/modules/notifications.ts
🚧 Files skipped from review as they are similar to previous changes (7)
- code/core/src/manager/globals/exports.ts
- code/core/src/component-testing/components/Subnav.tsx
- code/core/src/core-server/server-channel/open-in-editor-channel.ts
- code/core/src/component-testing/components/InteractionsPanel.tsx
- code/core/src/manager/components/preview/tools/open-in-editor.tsx
- code/core/src/component-testing/components/Panel.tsx
- code/core/src/manager/components/sidebar/ContextMenu.tsx
🧰 Additional context used
📓 Path-based instructions (1)
code/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
code/**/*.{test,spec}.{ts,tsx}
: Place all test files under the code/ directory
Name test files as *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx
Files:
code/e2e-tests/manager.spec.ts
🧬 Code graph analysis (3)
code/core/src/manager-api/root.tsx (1)
code/core/src/manager-api/modules/open-in-editor.tsx (1)
openInEditor
(35-51)
code/core/src/manager-api/modules/open-in-editor.tsx (2)
code/core/src/manager-api/modules/notifications.ts (3)
SubState
(7-9)SubAPI
(12-27)init
(29-62)code/core/src/core-events/data/open-in-editor.ts (1)
OpenInEditorResponsePayload
(3-8)
code/e2e-tests/manager.spec.ts (1)
code/e2e-tests/util.ts (1)
SbPage
(8-203)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: normal
- GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (7)
code/e2e-tests/manager.spec.ts (4)
19-34
: LGTM! Well-structured test for Settings tooltip behavior.The test covers the important scenarios: keyboard dismissal with Escape, persistence with other keys, and dismissal on outside clicks.
98-109
: Good test coverage for share actions.The test properly verifies both the UI presence and the clipboard functionality for the story link feature.
48-49
: Consistent replacement of 'Shortcuts' with 'Settings'.All references to the Shortcuts menu have been properly updated to use the Settings menu instead, maintaining test consistency with the UI changes.
Also applies to: 132-133, 135-136, 171-172, 222-223, 251-252
57-96
: Verify the open-in-editor functionality works across different environments.The test correctly verifies that "Open in editor" options are available in the UI for component, docs, and story nodes. However, since this is a development-only feature, ensure it's properly hidden in production builds.
code/core/src/manager-api/root.tsx (1)
56-56
: LGTM! Clean integration of the openInEditor module.The new module is properly imported and integrated into the modules array following the established pattern.
Also applies to: 180-180
code/core/src/manager-api/modules/shortcuts.ts (2)
115-118
: LGTM! New shortcut type definitions.The new shortcuts are properly added to the API_Shortcuts interface.
154-155
: Good default shortcut choices.The keyboard shortcuts follow platform conventions and don't conflict with existing shortcuts. Alt+Shift+E for editor and Alt+Shift+L for link are intuitive choices.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
🧹 Nitpick comments (8)
code/e2e-tests/manager.spec.ts (7)
8-8
: Normalize STORYBOOK_TYPE and accept “development”Make env check case‑insensitive and handle common alias.
-const isStorybookDev = (process.env.STORYBOOK_TYPE || 'dev') === 'dev'; +const isStorybookDev = ['dev', 'development'].includes( + (process.env.STORYBOOK_TYPE ?? 'dev').toLowerCase() +);
20-35
: Reduce flakiness when dismissing the tooltipClicking 'body' can hit overlays. Prefer a guaranteed page‑outside click.
- await page.click('body'); + await page.mouse.click(0, 0);
49-49
: Prefer role+name over aria‑label CSS selectorMore resilient to attribute drift and improves readability.
- await sbPage.page.locator('[aria-label="Settings"]').click(); + await sbPage.page.getByRole('button', { name: 'Settings' }).click();
136-136
: Use role+name selector for Settings triggerKeep selectors consistent and accessible.
- await sbPage.page.locator('[aria-label="Settings"]').click(); + await sbPage.page.getByRole('button', { name: 'Settings' }).click(); @@ - await sbPage.page.locator('[aria-label="Settings"]').click(); + await sbPage.page.getByRole('button', { name: 'Settings' }).click();Also applies to: 139-139
175-175
: Use role+name for Settings openerSame rationale as above.
- await sbPage.page.locator('[aria-label="Settings"]').click(); + await sbPage.page.getByRole('button', { name: 'Settings' }).click();
225-225
: Use role+name for Settings triggerConsistency and robustness.
- await sbPage.page.locator('[aria-label="Settings"]').click(); + await sbPage.page.getByRole('button', { name: 'Settings' }).click();
255-255
: Use role+name for Settings triggerSame improvement here.
- await sbPage.page.locator('[aria-label="Settings"]').click(); + await sbPage.page.getByRole('button', { name: 'Settings' }).click();code/core/src/manager-api/tests/stories.test.ts (1)
1539-1581
: Optional: make assertions less snapshot‑fragileTo reduce churn, consider asserting only the critical bits (presence of importPath on 'a' and on its children) with toMatchObject, keeping the rest as-is.
Apply this focused assertion after the snapshot:
+ // Ensure editor integration metadata is present without over-specifying the structure + expect(filteredIndex!.a).toMatchObject({ type: 'component', importPath: './a.ts' }); + expect(filteredIndex!['a--1']).toMatchObject({ type: 'story', importPath: './a.ts' }); + expect(filteredIndex!['a--2']).toMatchObject({ type: 'story', importPath: './a.ts' });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
code/core/src/component-testing/components/Subnav.tsx
(5 hunks)code/core/src/manager-api/tests/stories.test.ts
(3 hunks)code/e2e-tests/manager.spec.ts
(8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- code/core/src/component-testing/components/Subnav.tsx
🧰 Additional context used
📓 Path-based instructions (2)
code/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
code/**/*.{test,spec}.{ts,tsx}
: Place all test files under the code/ directory
Name test files as *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx
Files:
code/core/src/manager-api/tests/stories.test.ts
code/e2e-tests/manager.spec.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursor/rules/spy-mocking.mdc)
**/*.test.{ts,tsx,js,jsx}
: Use vi.mock() with the spy: true option for all package and file mocks in Vitest tests
Place all mocks at the top of the test file before any test cases
Use vi.mocked() to type and access mocked functions
Implement mock behaviors in beforeEach blocks
Mock all required dependencies that the test subject uses
Mock implementations should be placed in beforeEach blocks
Each mock implementation should return a Promise for async functions
Mock implementations should match the expected return type of the original function
Use vi.mocked() to access and implement mock behaviors
Mock all required properties and methods that the test subject uses
Avoid direct function mocking without vi.mocked()
Avoid mock implementations outside of beforeEach blocks
Avoid mocking without the spy: true option
Avoid inline mock implementations within test cases
Avoid mocking only a subset of required dependencies
Mock at the highest level of abstraction needed
Keep mock implementations simple and focused
Use type-safe mocking with vi.mocked()
Document complex mock behaviors
Group related mocks together
Files:
code/core/src/manager-api/tests/stories.test.ts
🧬 Code graph analysis (1)
code/e2e-tests/manager.spec.ts (1)
code/e2e-tests/util.ts (1)
SbPage
(8-203)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: normal
- GitHub Check: Core Unit Tests, windows-latest
🔇 Additional comments (1)
code/core/src/manager-api/tests/stories.test.ts (1)
1415-1456
: LGTM — verify importPath exposure is consistentTests now assert importPath on component entries in filteredIndex. Confirm that the index types and the indexer → manager mapping always set importPath for component entries (and never add importPath to root/group) to avoid snapshot churn.
Quick grep to run locally:
rg -nP --type=ts -C2 '\b(interface|type)\s+.*(IndexEntry|.*Entry)\b' code rg -nP --type=ts -C3 "type:\\s*'component'.*importPath" code rg -nP --type=ts -C3 "type:\\s*'(root|group)'.*importPath" code
{ | ||
"a": { | ||
"children": [ | ||
"a--1", | ||
], | ||
"depth": 0, | ||
"id": "a", | ||
"importPath": "./a.ts", | ||
"name": "a", | ||
"parent": undefined, | ||
"renderLabel": undefined, | ||
"tags": [], | ||
"type": "component", | ||
}, | ||
"a--1": { | ||
"depth": 1, | ||
"id": "a--1", | ||
"importPath": "./a.ts", | ||
"name": "1", | ||
"parent": "a", | ||
"prepared": false, | ||
"renderLabel": undefined, | ||
"tags": [], | ||
"title": "a", | ||
"type": "story", | ||
}, | ||
} | ||
`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Minor: define deterministic source for component.importPath when children differ
Snapshot looks good, but if a component’s stories span multiple files, a single component.importPath can be ambiguous and nondeterministic across invalidations. Prefer a stable rule (e.g., first child by stable sort, or prefer the current/selected story’s importPath in UI).
If the UI’s open-in-editor uses component.importPath as a fallback, confirm it prefers story.importPath when available:
🏁 Script executed:
#!/bin/bash
# Find open-in-editor callsites and how importPath is chosen
rg -nP -C3 "openInEditor\\(|importPath\\b" code
Length of output: 299943
Minor: make component.importPath deterministic or prefer story.importPath for Open in editor
Verified: component.importPath is populated from the first child (core/src/manager-api/lib/stories.ts:335–338) — nondeterministic when a component's stories live in multiple files. Sidebar ContextMenu uses context.importPath for "Open in editor" (core/src/manager/components/sidebar/ContextMenu.tsx:51–61), while preview/shortcuts call Open in editor with the story's importPath (core/src/manager/components/preview/tools/open-in-editor.tsx:21–45; core/src/manager-api/modules/shortcuts.ts:393–396). Action: either set component.importPath using a deterministic rule (stable sort or file-order) or change the UI to prefer the selected/story.importPath when opening the editor.
🤖 Prompt for AI Agents
In code/core/src/manager-api/tests/stories.test.ts around lines 1496–1523 the
test fixtures expose that component.importPath is derived nondeterministically
from the first child, which can cause "Open in editor" to open the wrong file;
fix by changing the UI logic (where ContextMenu/preview/shortcuts call Open in
editor) to prefer the story's importPath when present and only fall back to
component.importPath, or alternatively make component.importPath deterministic
(stable sort or file-order) when building the component object—implement the
former by updating the ContextMenu and any Open-in-editor call sites to use
selectedStory.importPath || component.importPath.
test('Story context menu actions', async ({ page }) => { | ||
test.skip(!isStorybookDev, 'These actions are only applicable in dev mode'); | ||
const sbPage = new SbPage(page, expect); | ||
await sbPage.navigateToStory('example/button', 'docs'); | ||
|
||
// Context menu should contain open in editor for component node | ||
await page.locator('[data-item-id="example-button"]').hover(); | ||
await page | ||
.locator('[data-item-id="example-button"] div[data-testid="context-menu"] button') | ||
.click(); | ||
const sidebarContextMenu = page.getByTestId('tooltip'); | ||
await expect( | ||
sidebarContextMenu.getByRole('button', { name: /open in editor/i }) | ||
).toBeVisible(); | ||
await page.click('body'); | ||
|
||
// Context menu should contain open in editor for docs node | ||
await page.locator('[data-item-id="example-button--docs"]').hover(); | ||
await page | ||
.locator('[data-item-id="example-button--docs"] div[data-testid="context-menu"] button') | ||
.click(); | ||
await expect( | ||
page.getByTestId('tooltip').getByRole('button', { name: /open in editor/i }) | ||
).toBeVisible(); | ||
await page.click('body'); | ||
|
||
// Context menu should contain open in editor and copy story name for story node | ||
await page.locator('[data-item-id="example-button--primary"]').hover(); | ||
await page | ||
.locator('[data-item-id="example-button--primary"] div[data-testid="context-menu"] button') | ||
.click(); | ||
await expect( | ||
page.getByTestId('tooltip').getByRole('button', { name: /open in editor/i }) | ||
).toBeVisible(); | ||
await page | ||
.getByTestId('tooltip') | ||
.getByRole('button', { name: /copy story name/i }) | ||
.click(); | ||
|
||
await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain( | ||
'Primary' | ||
); | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clipboard assertions may fail without permissions; grant and poll
Reading from navigator.clipboard often needs clipboard permissions and a brief delay. Grant at test setup and use expect.poll for stability.
- await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain(
- 'Primary'
- );
+ await expect
+ .poll(() => page.evaluate(() => navigator.clipboard.readText()))
+ .toContain('Primary');
Add this once in test.beforeEach (outside this hunk):
// before page.goto(storybookUrl)
await page.context().grantPermissions(['clipboard-read', 'clipboard-write'], { origin: storybookUrl });
I can submit a follow‑up patch wiring this into the suite if you’d like.
🤖 Prompt for AI Agents
In code/e2e-tests/manager.spec.ts around lines 58 to 101, the clipboard
assertion reads navigator.clipboard directly which can fail without clipboard
permissions and timing; grant clipboard-read/clipboard-write for the Storybook
origin once in a test.beforeEach (before navigating to the story) using
page.context().grantPermissions([...], { origin: storybookUrl }) and replace the
direct evaluate read+expect with a stable poll-based assertion (expect.poll or a
short retry loop) that reads navigator.clipboard.readText() until it contains
'Primary' to avoid flaky failures.
code/e2e-tests/manager.spec.ts
Outdated
test('Story share actions', async ({ page }) => { | ||
const sbPage = new SbPage(page, expect); | ||
await sbPage.navigateToStory('example/button', 'primary'); | ||
await expect(page.getByRole('button', { name: 'Open in editor' })).toBeVisible(); | ||
await page.getByRole('button', { name: 'Share' }).click(); | ||
await expect(page.getByRole('button', { name: /copy story link/i })).toBeVisible(); | ||
await page.getByRole('button', { name: /copy story link/i }).click(); | ||
|
||
await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain( | ||
`${storybookUrl}/?path=/story/example-button--primary` | ||
); | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gate “Open in editor” visibility by dev mode; fix clipboard read flake
Open‑in‑editor is dev‑only per PR; assert conditionally. Also switch to polling clipboard.
- await expect(page.getByRole('button', { name: 'Open in editor' })).toBeVisible();
+ if (isStorybookDev) {
+ await expect(page.getByRole('button', { name: 'Open in editor' })).toBeVisible();
+ } else {
+ await expect(page.getByRole('button', { name: 'Open in editor' })).toHaveCount(0);
+ }
@@
- await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain(
- `${storybookUrl}/?path=/story/example-button--primary`
- );
+ await expect
+ .poll(() => page.evaluate(() => navigator.clipboard.readText()))
+ .toContain(`${storybookUrl}/?path=/story/example-button--primary`);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
test('Story share actions', async ({ page }) => { | |
const sbPage = new SbPage(page, expect); | |
await sbPage.navigateToStory('example/button', 'primary'); | |
await expect(page.getByRole('button', { name: 'Open in editor' })).toBeVisible(); | |
await page.getByRole('button', { name: 'Share' }).click(); | |
await expect(page.getByRole('button', { name: /copy story link/i })).toBeVisible(); | |
await page.getByRole('button', { name: /copy story link/i }).click(); | |
await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain( | |
`${storybookUrl}/?path=/story/example-button--primary` | |
); | |
}); | |
test('Story share actions', async ({ page }) => { | |
const sbPage = new SbPage(page, expect); | |
await sbPage.navigateToStory('example/button', 'primary'); | |
if (isStorybookDev) { | |
await expect(page.getByRole('button', { name: 'Open in editor' })).toBeVisible(); | |
} else { | |
await expect(page.getByRole('button', { name: 'Open in editor' })).toHaveCount(0); | |
} | |
await page.getByRole('button', { name: 'Share' }).click(); | |
await expect(page.getByRole('button', { name: /copy story link/i })).toBeVisible(); | |
await page.getByRole('button', { name: /copy story link/i }).click(); | |
await expect | |
.poll(() => page.evaluate(() => navigator.clipboard.readText())) | |
.toContain(`${storybookUrl}/?path=/story/example-button--primary`); | |
}); |
🤖 Prompt for AI Agents
In code/e2e-tests/manager.spec.ts around lines 102 to 114, the test
unconditionally asserts the "Open in editor" button (which is present only in
dev mode) and reads the clipboard directly (causing flakiness); change the test
to first check whether the suite is running in dev mode and only assert the
"Open in editor" visibility when dev mode is true, and replace the single
navigator.clipboard.readText() call with a polling/retry-based clipboard read
(e.g., loop or waitFor/waitForFunction that repeatedly reads
navigator.clipboard.readText until the expected URL fragment appears or a
timeout elapses) so the test waits for the clipboard to be populated before
asserting the expected story link.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (4)
code/e2e-tests/manager.spec.ts (4)
11-15
: Grant clipboard permissions before navigation to deflake clipboard assertionsWithout explicit permissions,
navigator.clipboard.readText()
is flaky across browsers. Grant permissions for the Storybook origin beforepage.goto()
.Apply this diff:
test.beforeEach(async ({ page }) => { - await page.goto(storybookUrl); + await page.context().grantPermissions(['clipboard-read', 'clipboard-write'], { + origin: storybookUrl, + }); + await page.goto(storybookUrl); await new SbPage(page, expect).waitUntilLoaded(); });
58-101
: Use polling for clipboard; keep context‑menu checks as-isGreat dev-only gating via
test.skip
. Replace direct clipboard read withexpect.poll
for stability.Apply this diff:
- await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain( - 'Primary' - ); + await expect + .poll(() => page.evaluate(() => navigator.clipboard.readText())) + .toContain('Primary');
102-114
: Dev share test: poll clipboard resultThe dev gating is correct. Swap to polling to avoid timing flakes after clicking “Copy story link”.
Apply this diff:
- await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain( - `${storybookUrl}/?path=/story/example-button--primary` - ); + await expect + .poll(() => page.evaluate(() => navigator.clipboard.readText())) + .toContain(`${storybookUrl}/?path=/story/example-button--primary`);
116-128
: Build share test: poll clipboard resultSame clipboard flake here; use polling.
Apply this diff:
- await expect(page.evaluate(() => navigator.clipboard.readText())).resolves.toContain( - `${storybookUrl}/?path=/story/example-button--primary` - ); + await expect + .poll(() => page.evaluate(() => navigator.clipboard.readText())) + .toContain(`${storybookUrl}/?path=/story/example-button--primary`);
🧹 Nitpick comments (1)
code/e2e-tests/manager.spec.ts (1)
8-8
: Name the env flag more explicitlyConsider renaming
type
tostorybookType
for clarity and to avoid confusion with the TStype
keyword in declarations.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
code/e2e-tests/manager.spec.ts
(8 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
code/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
code/**/*.{test,spec}.{ts,tsx}
: Place all test files under the code/ directory
Name test files as *.test.ts, *.test.tsx, *.spec.ts, or *.spec.tsx
Files:
code/e2e-tests/manager.spec.ts
🧬 Code graph analysis (1)
code/e2e-tests/manager.spec.ts (1)
code/e2e-tests/util.ts (1)
SbPage
(8-203)
🔇 Additional comments (6)
code/e2e-tests/manager.spec.ts (6)
20-35
: Settings tooltip test reads wellThe visibility/hide flows (Escape and outside click) look solid.
49-49
: UI trigger switch to Settings: LGTMSwitching the menu trigger to Settings aligns with the new UX.
150-150
: Toolbar toggling via Settings: LGTMAlso applies to: 154-154
189-189
: Panel toggling via Settings: LGTM
241-241
: Fullscreen toggling via Settings: LGTM
269-269
: Settings page navigation: LGTM
Closes #
What I did
This PR introduces an API in
storybook/manager-api
to open a file in the editor:openInEditor(filePath, lineNumber?, columnNumber?)
.This new API is used in a few places to open a story in the editor:
Such buttons will not be shown in production builds, as this is a dev only feature.
Context Menu
Upon clicking on the three dots in stories only
Interactions panel
Presented in local stories only. In composed Storybooks or published Storybooks the text will just be a normal text and not a link

New share button
This new button groups the actions of copying the story URL, opening in isolation mode and introduces a QR Code for opening the network address from your phone

This PR also fixes an issue where the shortcut font was broken in a few places:

How it works
There is a very nice package called launch-editor which comes out of the box with Vite, and once it is present, you can fetch a specific route to open a file in the editor, it will figure out your existing editors and open the most active one, like magic. For Webpack projects, we need to include the launch-editor-middleware, but it works the same way.
Checklist for Contributors
Testing
The changes in this PR are covered in the following automated tests:
Manual testing
yarn storybook:ui
Documentation
MIGRATION.MD
Checklist for Maintainers
When this PR is ready for testing, make sure to add
ci:normal
,ci:merged
orci:daily
GH label to it to run a specific set of sandboxes. The particular set of sandboxes can be found incode/lib/cli-storybook/src/sandbox-templates.ts
Make sure this PR contains one of the labels below:
Available labels
bug
: Internal changes that fixes incorrect behavior.maintenance
: User-facing maintenance tasks.dependencies
: Upgrading (sometimes downgrading) dependencies.build
: Internal-facing build tooling & test updates. Will not show up in release changelog.cleanup
: Minor cleanup style change. Will not show up in release changelog.documentation
: Documentation only changes. Will not show up in release changelog.feature request
: Introducing a new feature.BREAKING CHANGE
: Changes that break compatibility in some way with current major version.other
: Changes that don't fit in the above categories.🦋 Canary release
This pull request has been released as version
0.0.0-pr-32452-sha-d0653ada
. Try it out in a new sandbox by runningnpx storybook@0.0.0-pr-32452-sha-d0653ada sandbox
or in an existing project withnpx storybook@0.0.0-pr-32452-sha-d0653ada upgrade
.More information
0.0.0-pr-32452-sha-d0653ada
yann/open-in-editor-feature
d0653ada
1758124134
)To request a new release of this pull request, mention the
@storybookjs/core
team.core team members can create a new canary release here or locally with
gh workflow run --repo storybookjs/storybook canary-release-pr.yml --field pr=32452
Greptile Summary
Updated On: 2025-09-15 08:36:29 UTC
This PR implements an "open in editor" feature that allows developers to quickly open story files directly in their code editor from the Storybook UI. The implementation introduces a new
openInEditor(filePath, lineNumber?, columnNumber?)
API that leverages the launch-editor ecosystem to automatically detect and open files in the user's preferred editor.Core Implementation:
openInEditor
function incode/core/src/manager-api/lib/open-in-editor.ts
makes POST requests to a/__open-in-editor
endpoint with file path, line, and column informationUI Integration Points:
openInEditorTool
to the default toolbar tools array, appearing in both story and docs view modesHow It Works:
The feature uses the launch-editor package ecosystem which automatically detects available editors and opens the most recently used one. For Vite projects, this works out of the box. For Webpack projects, the PR adds launch-editor-middleware to provide the same functionality. The implementation follows Storybook's established patterns for addon tools and maintains consistency across different UI contexts.
The change integrates seamlessly with Storybook's existing architecture, using the Consumer pattern to access story metadata and following established conventions for toolbar tools and API exports.
Confidence score: 3/5
Summary by CodeRabbit
New Features
Improvements
Documentation
Chores