-
-
Notifications
You must be signed in to change notification settings - Fork 9.8k
Core: Add getStoryHrefs manager API and add hotkey for "open in isolation"
#33416
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
base: next
Are you sure you want to change the base?
Changes from all commits
a7982fd
2a864fd
628f670
b9b78dd
2800dc6
72174ff
8ea95a9
7bf5c42
e5d6628
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // Only for internal use in addon-docs code, because the parent util in `core` cannot be imported. | ||
| // Unlike the parent util, this one only returns the preview URL. | ||
| export const getStoryHref = (storyId: string, additionalParams: Record<string, string> = {}) => { | ||
| const baseUrl = globalThis.PREVIEW_URL || 'iframe.html'; | ||
| const [url, paramsStr] = baseUrl.split('?'); | ||
| const params = new URLSearchParams(paramsStr || ''); | ||
|
|
||
| Object.entries(additionalParams).forEach(([key, value]) => { | ||
| params.set(key, value); | ||
| }); | ||
|
|
||
| params.set('id', storyId); | ||
|
|
||
| return `${url}?${params.toString()}`; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,17 +7,16 @@ import { | |||||||||||||||||||||||||||||
| } from 'storybook/internal/core-events'; | ||||||||||||||||||||||||||||||
| import { buildArgsParam, queryFromLocation } from 'storybook/internal/router'; | ||||||||||||||||||||||||||||||
| import type { NavigateOptions } from 'storybook/internal/router'; | ||||||||||||||||||||||||||||||
| import type { API_Layout, API_UI, Args } from 'storybook/internal/types'; | ||||||||||||||||||||||||||||||
| import type { API_Layout, API_UI, API_ViewMode, Args } from 'storybook/internal/types'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import { global } from '@storybook/global'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import { dequal as deepEqual } from 'dequal'; | ||||||||||||||||||||||||||||||
| import { stringify } from 'picoquery'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import type { ModuleArgs, ModuleFn } from '../lib/types'; | ||||||||||||||||||||||||||||||
| import { defaultLayoutState } from './layout'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const { window: globalWindow } = global; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| export interface SubState { | ||||||||||||||||||||||||||||||
| customQueryParams: QueryParams; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
@@ -33,6 +32,24 @@ const parseBoolean = (value: string) => { | |||||||||||||||||||||||||||||
| return undefined; | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const parseSerializedParam = (param: string) => | ||||||||||||||||||||||||||||||
| Object.fromEntries( | ||||||||||||||||||||||||||||||
| param | ||||||||||||||||||||||||||||||
| .split(';') | ||||||||||||||||||||||||||||||
| .map((pair) => pair.split(':')) | ||||||||||||||||||||||||||||||
| // Encoding values ensures we don't break already encoded args/globals but also don't encode our own special characters like ; and :. | ||||||||||||||||||||||||||||||
| .map(([key, value]) => [key, encodeURIComponent(value)]) | ||||||||||||||||||||||||||||||
| .filter(([key, value]) => key && value) | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const mergeSerializedParams = (params: string, extraParams: string) => { | ||||||||||||||||||||||||||||||
| const pairs = parseSerializedParam(params); | ||||||||||||||||||||||||||||||
| const extra = parseSerializedParam(extraParams); | ||||||||||||||||||||||||||||||
| return Object.entries({ ...pairs, ...extra }) | ||||||||||||||||||||||||||||||
| .map(([key, value]) => `${key}:${value}`) | ||||||||||||||||||||||||||||||
| .join(';'); | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Initialize the state based on the URL. | ||||||||||||||||||||||||||||||
| // NOTE: | ||||||||||||||||||||||||||||||
| // Although we don't change the URL when you change the state, we do support setting initial state | ||||||||||||||||||||||||||||||
|
|
@@ -121,6 +138,33 @@ export interface SubAPI { | |||||||||||||||||||||||||||||
| * @returns {void} | ||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||
| navigateUrl: (url: string, options: NavigateOptions) => void; | ||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
| * Get the manager and preview hrefs for a story. | ||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||
| * @param {string} storyId - The ID of the story to get the URL for. | ||||||||||||||||||||||||||||||
| * @param {Object} options - Options for the URL. | ||||||||||||||||||||||||||||||
| * @param {string} [options.base] - Return an absolute href based on the current origin or network | ||||||||||||||||||||||||||||||
| * address. | ||||||||||||||||||||||||||||||
| * @param {boolean} [options.inheritArgs] - Inherit args from the current URL. If storyId matches | ||||||||||||||||||||||||||||||
| * current story, inheritArgs defaults to true. | ||||||||||||||||||||||||||||||
| * @param {boolean} [options.inheritGlobals] - Inherit globals from the current URL. Defaults to | ||||||||||||||||||||||||||||||
| * true. | ||||||||||||||||||||||||||||||
| * @param {QueryParams} [options.queryParams] - Query params to add to the URL. | ||||||||||||||||||||||||||||||
| * @param {string} [options.refId] - ID of the ref to get the URL for (for composed Storybooks) | ||||||||||||||||||||||||||||||
| * @param {string} [options.viewMode] - The view mode to use, defaults to 'story'. | ||||||||||||||||||||||||||||||
| * @returns {Object} Manager and preview hrefs for the story. | ||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||
| getStoryHrefs( | ||||||||||||||||||||||||||||||
| storyId: string, | ||||||||||||||||||||||||||||||
| options?: { | ||||||||||||||||||||||||||||||
| base?: 'origin' | 'network'; | ||||||||||||||||||||||||||||||
| inheritArgs?: boolean; | ||||||||||||||||||||||||||||||
| inheritGlobals?: boolean; | ||||||||||||||||||||||||||||||
| queryParams?: QueryParams; | ||||||||||||||||||||||||||||||
| refId?: string; | ||||||||||||||||||||||||||||||
| viewMode?: API_ViewMode; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| ): { managerHref: string; previewHref: string }; | ||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||
| * Get the value of a query parameter from the current URL. | ||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||
|
|
@@ -183,6 +227,54 @@ export const init: ModuleFn<SubAPI, SubState> = (moduleArgs) => { | |||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const api: SubAPI = { | ||||||||||||||||||||||||||||||
| getStoryHrefs(storyId, options = {}) { | ||||||||||||||||||||||||||||||
| const { id: currentStoryId, refId: currentRefId } = fullAPI.getCurrentStoryData() ?? {}; | ||||||||||||||||||||||||||||||
| const isCurrentStory = storyId === currentStoryId && options.refId === currentRefId; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const { customQueryParams, location, refs } = store.getState(); | ||||||||||||||||||||||||||||||
| const { | ||||||||||||||||||||||||||||||
| base, | ||||||||||||||||||||||||||||||
| inheritArgs = isCurrentStory, | ||||||||||||||||||||||||||||||
| inheritGlobals = true, | ||||||||||||||||||||||||||||||
| queryParams = {}, | ||||||||||||||||||||||||||||||
| refId, | ||||||||||||||||||||||||||||||
| viewMode = 'story', | ||||||||||||||||||||||||||||||
| } = options; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if (refId && !refs[refId]) { | ||||||||||||||||||||||||||||||
| throw new Error(`Invalid refId: ${refId}`); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const originAddress = global.window.location.origin + location.pathname; | ||||||||||||||||||||||||||||||
| const networkAddress = global.STORYBOOK_NETWORK_ADDRESS ?? originAddress; | ||||||||||||||||||||||||||||||
| const managerBase = | ||||||||||||||||||||||||||||||
| base === 'origin' ? originAddress : base === 'network' ? networkAddress : location.pathname; | ||||||||||||||||||||||||||||||
| const previewBase = refId | ||||||||||||||||||||||||||||||
| ? refs[refId].url + '/iframe.html' | ||||||||||||||||||||||||||||||
| : global.PREVIEW_URL || `${managerBase}iframe.html`; | ||||||||||||||||||||||||||||||
Sidnioulz marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const refParam = refId ? `&refId=${encodeURIComponent(refId)}` : ''; | ||||||||||||||||||||||||||||||
| const { args = '', globals = '', ...otherParams } = queryParams; | ||||||||||||||||||||||||||||||
| let argsParam = inheritArgs | ||||||||||||||||||||||||||||||
| ? mergeSerializedParams(customQueryParams?.args ?? '', args) | ||||||||||||||||||||||||||||||
| : args; | ||||||||||||||||||||||||||||||
| let globalsParam = inheritGlobals | ||||||||||||||||||||||||||||||
| ? mergeSerializedParams(customQueryParams?.globals ?? '', globals) | ||||||||||||||||||||||||||||||
| : globals; | ||||||||||||||||||||||||||||||
| let customParams = stringify(otherParams, { | ||||||||||||||||||||||||||||||
| nesting: true, | ||||||||||||||||||||||||||||||
| nestingSyntax: 'js', | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| argsParam = argsParam && `&args=${argsParam}`; | ||||||||||||||||||||||||||||||
| globalsParam = globalsParam && `&globals=${globalsParam}`; | ||||||||||||||||||||||||||||||
| customParams = customParams && `&${customParams}`; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||
| managerHref: `${managerBase}?path=/${viewMode}/${refId ? `${refId}_` : ''}${storyId}${argsParam}${globalsParam}${customParams}`, | ||||||||||||||||||||||||||||||
| previewHref: `${previewBase}?id=${storyId}&viewMode=${viewMode}${refParam}${argsParam}${refId ? '' : globalsParam}${customParams}`, | ||||||||||||||||||||||||||||||
|
Comment on lines
+269
to
+275
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. URL-encode serialized parameters and path segments. The serialized 🔎 Proposed fix to add URL encoding- argsParam = argsParam && `&args=${argsParam}`;
- globalsParam = globalsParam && `&globals=${globalsParam}`;
+ argsParam = argsParam && `&args=${encodeURIComponent(argsParam)}`;
+ globalsParam = globalsParam && `&globals=${encodeURIComponent(globalsParam)}`;
customParams = customParams && `&${customParams}`;
return {
- managerHref: `${managerBase}?path=/${viewMode}/${refId ? `${refId}_` : ''}${storyId}${argsParam}${globalsParam}${customParams}`,
+ managerHref: `${managerBase}?path=/${viewMode}/${refId ? `${encodeURIComponent(refId)}_` : ''}${encodeURIComponent(storyId)}${argsParam}${globalsParam}${customParams}`,
previewHref: `${previewBase}?id=${storyId}&viewMode=${viewMode}${refParam}${argsParam}${refId ? '' : globalsParam}${customParams}`,
};Note: Verify that 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| getQueryParam(key) { | ||||||||||||||||||||||||||||||
| const { customQueryParams } = store.getState(); | ||||||||||||||||||||||||||||||
| return customQueryParams ? customQueryParams[key] : undefined; | ||||||||||||||||||||||||||||||
|
|
@@ -253,11 +345,11 @@ export const init: ModuleFn<SubAPI, SubState> = (moduleArgs) => { | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| let handleOrId: any; | ||||||||||||||||||||||||||||||
| provider.channel?.on(STORY_ARGS_UPDATED, () => { | ||||||||||||||||||||||||||||||
| if ('requestIdleCallback' in globalWindow) { | ||||||||||||||||||||||||||||||
| if ('requestIdleCallback' in global.window) { | ||||||||||||||||||||||||||||||
| if (handleOrId) { | ||||||||||||||||||||||||||||||
| globalWindow.cancelIdleCallback(handleOrId); | ||||||||||||||||||||||||||||||
| global.window.cancelIdleCallback(handleOrId); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| handleOrId = globalWindow.requestIdleCallback(updateArgsParam, { timeout: 1000 }); | ||||||||||||||||||||||||||||||
| handleOrId = global.window.requestIdleCallback(updateArgsParam, { timeout: 1000 }); | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| if (handleOrId) { | ||||||||||||||||||||||||||||||
| clearTimeout(handleOrId); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.