From 6726d4a2518a4cda2921e20a744c5650d028da1c Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 13 Apr 2023 14:49:38 +1000 Subject: [PATCH 1/7] Remove unnecessary code --- .../src/modules/preview-web/PreviewWithSelection.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx b/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx index af62eeea6e88..79c502998adf 100644 --- a/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx +++ b/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx @@ -89,12 +89,6 @@ export class PreviewWithSelection extends Preview) { - return super - .initializeWithProjectAnnotations(projectAnnotations) - .then(() => this.setInitialGlobals()); - } - async setInitialGlobals() { if (!this.storyStore.globals) throw new Error(`Cannot call setInitialGlobals before initialization`); From 01b4936a864520c9cf3821db7d1f6b74d499a62a Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 13 Apr 2023 14:50:49 +1000 Subject: [PATCH 2/7] Refactor prepareMeta to cache on store --- .../docs-context/DocsContext.test.ts | 2 ++ .../preview-web/docs-context/DocsContext.ts | 7 +------ .../src/modules/store/StoryStore.ts | 18 +++++++++++++++++- .../src/modules/store/csf/prepareStory.ts | 3 +-- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.test.ts b/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.test.ts index 8988307cf0d0..b36c08ede673 100644 --- a/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.test.ts +++ b/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.test.ts @@ -39,6 +39,7 @@ describe('resolveOf', () => { const projectAnnotations = { render: jest.fn() }; const store = { componentStoriesFromCSFFile: () => [story], + preparedMetaFromCSFFile: () => ({ prepareMeta: 'preparedMeta' }), projectAnnotations, } as unknown as StoryStore; const context = new DocsContext(channel, store, renderStoryToElement, [csfFile]); @@ -179,6 +180,7 @@ describe('resolveOf', () => { const projectAnnotations = { render: jest.fn() }; const store = { componentStoriesFromCSFFile: () => [story], + preparedMetaFromCSFFile: () => ({ prepareMeta: 'preparedMeta' }), projectAnnotations, } as unknown as StoryStore; const context = new DocsContext(channel, store, renderStoryToElement, [csfFile]); diff --git a/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.ts b/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.ts index 071d2ccf1d59..6796913596bd 100644 --- a/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.ts +++ b/code/lib/preview-api/src/modules/preview-web/docs-context/DocsContext.ts @@ -14,7 +14,6 @@ import type { Channel } from '@storybook/channels'; import dedent from 'ts-dedent'; import type { StoryStore } from '../../store'; -import { prepareMeta } from '../../store'; import type { DocsContextProps } from './DocsContextProps'; export class DocsContext implements DocsContextProps { @@ -181,11 +180,7 @@ export class DocsContext implements DocsContextProps case 'meta': { return { ...resolved, - preparedMeta: prepareMeta( - resolved.csfFile.meta, - this.projectAnnotations, - resolved.csfFile.moduleExports.default - ), + preparedMeta: this.store.preparedMetaFromCSFFile({ csfFile: resolved.csfFile }), }; } case 'story': diff --git a/code/lib/preview-api/src/modules/store/StoryStore.ts b/code/lib/preview-api/src/modules/store/StoryStore.ts index 20b13bbff693..e007c06c1a43 100644 --- a/code/lib/preview-api/src/modules/store/StoryStore.ts +++ b/code/lib/preview-api/src/modules/store/StoryStore.ts @@ -20,6 +20,7 @@ import type { StoryContextForEnhancers, StoryContextForLoaders, StoryId, + PreparedMeta, } from '@storybook/types'; import mapValues from 'lodash/mapValues.js'; import pick from 'lodash/pick.js'; @@ -29,7 +30,7 @@ import { HooksContext } from '../addons'; import { StoryIndexStore } from './StoryIndexStore'; import { ArgsStore } from './ArgsStore'; import { GlobalsStore } from './GlobalsStore'; -import { processCSFFile, prepareStory, normalizeProjectAnnotations } from './csf'; +import { processCSFFile, prepareStory, prepareMeta, normalizeProjectAnnotations } from './csf'; const CSF_CACHE_SIZE = 1000; const STORY_CACHE_SIZE = 10000; @@ -52,6 +53,8 @@ export class StoryStore { processCSFFileWithCache: typeof processCSFFile; + prepareMetaWithCache: typeof prepareMeta; + prepareStoryWithCache: typeof prepareStory; initializationPromise: SynchronousPromise; @@ -67,6 +70,7 @@ export class StoryStore { // 1. For performance // 2. To ensure that when the same story is prepared with the same inputs you get the same output this.processCSFFileWithCache = memoize(CSF_CACHE_SIZE)(processCSFFile) as typeof processCSFFile; + this.prepareMetaWithCache = memoize(CSF_CACHE_SIZE)(prepareMeta) as typeof prepareMeta; this.prepareStoryWithCache = memoize(STORY_CACHE_SIZE)(prepareStory) as typeof prepareStory; // We cannot call `loadStory()` until we've been initialized properly. But we can wait for it. @@ -190,6 +194,18 @@ export class StoryStore { ); } + preparedMetaFromCSFFile({ csfFile }: { csfFile: CSFFile }): PreparedMeta { + if (!this.projectAnnotations) throw new Error(`storyFromCSFFile called before initialization`); + + const componentAnnotations = csfFile.meta; + + return this.prepareMetaWithCache( + componentAnnotations, + this.projectAnnotations, + csfFile.moduleExports.default + ); + } + // Load the CSF file for a story and prepare the story from it and the project annotations. async loadStory({ storyId }: { storyId: StoryId }): Promise> { await this.initializationPromise; diff --git a/code/lib/preview-api/src/modules/store/csf/prepareStory.ts b/code/lib/preview-api/src/modules/store/csf/prepareStory.ts index 6370ebb4ffe8..1be2aa63607e 100644 --- a/code/lib/preview-api/src/modules/store/csf/prepareStory.ts +++ b/code/lib/preview-api/src/modules/store/csf/prepareStory.ts @@ -190,7 +190,6 @@ function preparePartialAnnotations( componentAnnotations.render || projectAnnotations.render; - if (!render) throw new Error(`No render function available for id '${id}'`); const passedArgTypes: StrictArgTypes = combineParameters( projectAnnotations.argTypes, componentAnnotations.argTypes, @@ -199,7 +198,7 @@ function preparePartialAnnotations( const { passArgsFirst = true } = parameters; // eslint-disable-next-line no-underscore-dangle - parameters.__isArgsStory = passArgsFirst && render.length > 0; + parameters.__isArgsStory = passArgsFirst && render && render.length > 0; // Pull out args[X] into initialArgs for argTypes enhancers const passedArgs: Args = { From 376733819f2b9e94afc19231b859746d0ea7082e Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 17 Apr 2023 13:42:35 +1000 Subject: [PATCH 3/7] Added new `DOCS_PREPARED` event with parameters. Parameters are pulled from the component (for csf docs entries or the project for mdx docs entries). --- code/lib/core-events/src/index.ts | 3 + .../preview-web/PreviewWeb.mockdata.ts | 18 ++++- .../modules/preview-web/PreviewWeb.test.ts | 81 ++++++++++++++++++- .../preview-web/PreviewWithSelection.tsx | 30 +++++++ .../preview-web/render/CsfDocsRender.ts | 4 +- .../preview-web/render/MdxDocsRender.ts | 4 +- .../modules/store/csf/prepareStory.test.ts | 6 +- .../src/modules/store/csf/prepareStory.ts | 27 ++++--- 8 files changed, 153 insertions(+), 20 deletions(-) diff --git a/code/lib/core-events/src/index.ts b/code/lib/core-events/src/index.ts index a995c2b8c80c..b65ebe7dbd07 100644 --- a/code/lib/core-events/src/index.ts +++ b/code/lib/core-events/src/index.ts @@ -25,6 +25,8 @@ enum events { PRELOAD_ENTRIES = 'preloadStories', // The story has been loaded into the store, we have parameters/args/etc STORY_PREPARED = 'storyPrepared', + // The a docs entry has been loaded into the store, we have parameters + DOCS_PREPARED = 'docsPrepared', // The next 6 events are emitted by the StoryRenderer when rendering the current story STORY_CHANGED = 'storyChanged', STORY_UNCHANGED = 'storyUnchanged', @@ -73,6 +75,7 @@ export const { CHANNEL_CREATED, CONFIG_ERROR, CURRENT_STORY_WAS_SET, + DOCS_PREPARED, DOCS_RENDERED, FORCE_RE_RENDER, FORCE_REMOUNT, diff --git a/code/lib/preview-api/src/modules/preview-web/PreviewWeb.mockdata.ts b/code/lib/preview-api/src/modules/preview-web/PreviewWeb.mockdata.ts index 5b6cbbaa0894..e136fc63b260 100644 --- a/code/lib/preview-api/src/modules/preview-web/PreviewWeb.mockdata.ts +++ b/code/lib/preview-api/src/modules/preview-web/PreviewWeb.mockdata.ts @@ -32,6 +32,9 @@ export const componentTwoExports = { default: { title: 'Component Two' }, c: { args: { foo: 'c' } }, }; +export const attachedDocsExports = { + default: jest.fn(), +}; export const unattachedDocsExports = { default: jest.fn(), }; @@ -49,6 +52,7 @@ export const importFn: jest.Mocked = jest.fn( async (path: string) => ({ './src/ComponentOne.stories.js': componentOneExports, + './src/ComponentOne.mdx': attachedDocsExports, './src/ComponentTwo.stories.js': componentTwoExports, './src/Introduction.mdx': unattachedDocsExports, './src/ExtraComponentOne.stories.js': extraComponentOneExports, @@ -80,7 +84,16 @@ export const storyIndex: StoryIndex = { name: 'Docs', importPath: './src/ComponentOne.stories.js', storiesImports: ['./src/ExtraComponentOne.stories.js'], - tags: ['autodocs'], + tags: ['autodocs', 'docs'], + }, + 'component-one--attached-docs': { + type: 'docs', + id: 'component-one--attached-docs', + title: 'Component One', + name: 'Attached Docs', + importPath: './src/ComponentOne.mdx', + storiesImports: ['./src/ComponentOne.stories.js'], + tags: ['attached-mdx', 'docs'], }, 'component-one--a': { type: 'story', @@ -110,7 +123,7 @@ export const storyIndex: StoryIndex = { name: 'Docs', importPath: './src/ComponentTwo.stories.js', storiesImports: [], - tags: ['autodocs'], + tags: ['autodocs', 'docs'], }, 'component-two--c': { type: 'story', @@ -126,6 +139,7 @@ export const storyIndex: StoryIndex = { name: 'Docs', importPath: './src/Introduction.mdx', storiesImports: ['./src/ComponentTwo.stories.js'], + tags: ['unattached-mdx', 'docs'], }, }, }; diff --git a/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts b/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts index 7f2c39c6dcd6..1a0188c1e55e 100644 --- a/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts +++ b/code/lib/preview-api/src/modules/preview-web/PreviewWeb.test.ts @@ -28,6 +28,7 @@ import { STORY_UNCHANGED, UPDATE_GLOBALS, UPDATE_STORY_ARGS, + DOCS_PREPARED, } from '@storybook/core-events'; import { logger } from '@storybook/client-logger'; import type { Renderer, ModuleImportFn, ProjectAnnotations } from '@storybook/types'; @@ -652,6 +653,19 @@ describe('PreviewWeb', () => { }); describe('CSF docs entries', () => { + it('emits DOCS_PREPARED', async () => { + document.location.search = '?id=component-one--docs'; + await createAndRenderPreview(); + + expect(mockChannel.emit).toHaveBeenCalledWith(DOCS_PREPARED, { + id: 'component-one--docs', + parameters: { + docs: expect.any(Object), + fileName: './src/ComponentOne.stories.js', + }, + }); + }); + it('always renders in docs viewMode', async () => { document.location.search = '?id=component-one--docs'; await createAndRenderPreview(); @@ -709,7 +723,7 @@ describe('PreviewWeb', () => { }); }); - describe('mdx docs entries', () => { + describe('MDX docs entries', () => { it('always renders in docs viewMode', async () => { document.location.search = '?id=introduction--docs'; await createAndRenderPreview(); @@ -717,6 +731,33 @@ describe('PreviewWeb', () => { expect(mockChannel.emit).toHaveBeenCalledWith(DOCS_RENDERED, 'introduction--docs'); }); + it('emits DOCS_PREPARED', async () => { + document.location.search = '?id=introduction--docs'; + await createAndRenderPreview(); + + expect(mockChannel.emit).toHaveBeenCalledWith(DOCS_PREPARED, { + id: 'introduction--docs', + parameters: { + docs: expect.any(Object), + }, + }); + }); + + describe('attached', () => { + it('emits DOCS_PREPARED with component parameters', async () => { + document.location.search = '?id=component-one--attached-docs'; + await createAndRenderPreview(); + + expect(mockChannel.emit).toHaveBeenCalledWith(DOCS_PREPARED, { + id: 'component-one--attached-docs', + parameters: { + docs: expect.any(Object), + fileName: './src/ComponentOne.stories.js', + }, + }); + }); + }); + it('calls view.prepareForDocs', async () => { document.location.search = '?id=component-one--docs&viewMode=docs'; const preview = await createAndRenderPreview(); @@ -2261,6 +2302,26 @@ describe('PreviewWeb', () => { }); describe('when changing from story viewMode to docs', () => { + it('emits DOCS_PREPARED', async () => { + document.location.search = '?id=component-one--a'; + await createAndRenderPreview(); + + mockChannel.emit.mockClear(); + emitter.emit(SET_CURRENT_STORY, { + storyId: 'component-one--docs', + viewMode: 'docs', + }); + await waitForSetCurrentStory(); + + expect(mockChannel.emit).toHaveBeenCalledWith(DOCS_PREPARED, { + id: 'component-one--docs', + parameters: { + docs: expect.any(Object), + fileName: './src/ComponentOne.stories.js', + }, + }); + }); + it('calls renderToCanvass teardown', async () => { document.location.search = '?id=component-one--a'; await createAndRenderPreview(); @@ -3182,6 +3243,24 @@ describe('PreviewWeb', () => { return path === './src/Introduction.mdx' ? newUnattachedDocsExports : importFn(path); }); + it('emits DOCS_PREPARED', async () => { + document.location.search = '?id=introduction--docs'; + const preview = await createAndRenderPreview(); + + mockChannel.emit.mockClear(); + docsRenderer.render.mockClear(); + + preview.onStoriesChanged({ importFn: newImportFn }); + await waitForRender(); + + expect(mockChannel.emit).toHaveBeenCalledWith(DOCS_PREPARED, { + id: 'introduction--docs', + parameters: { + docs: expect.any(Object), + }, + }); + }); + it('renders with the generated docs parameters', async () => { document.location.search = '?id=introduction--docs&viewMode=docs'; const preview = await createAndRenderPreview(); diff --git a/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx b/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx index 79c502998adf..269d05bf1e39 100644 --- a/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx +++ b/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx @@ -2,6 +2,7 @@ import { dedent } from 'ts-dedent'; import { global } from '@storybook/global'; import { CURRENT_STORY_WAS_SET, + DOCS_PREPARED, PRELOAD_ENTRIES, PREVIEW_KEYDOWN, SET_CURRENT_STORY, @@ -50,6 +51,7 @@ function focusInInput(event: Event) { export const AUTODOCS_TAG = 'autodocs'; export const STORIES_MDX_TAG = 'stories-mdx'; +export const ATTACHED_MDX_TAG = 'attached-mdx'; /** Was this docs entry generated by a .mdx file? (see discussion below) */ export function isMdxEntry({ tags }: DocsIndexEntry) { @@ -67,6 +69,18 @@ function isStoryRender( return r.type === 'story'; } +function isDocsRender( + r: PossibleRender +): r is CsfDocsRender | MdxDocsRender { + return r.type === 'docs'; +} + +function isCsfDocsRender( + r: PossibleRender +): r is CsfDocsRender { + return isDocsRender(r) && r.subtype === 'csf'; +} + export class PreviewWithSelection extends Preview { currentSelection?: Selection; @@ -386,6 +400,22 @@ export class PreviewWithSelection extends Preview implements Render { public readonly type: RenderType = 'docs'; + public readonly subtype = 'csf'; + public readonly id: StoryId; public story?: PreparedStory; @@ -45,7 +47,7 @@ export class CsfDocsRender implements Render[]; + public csfFiles?: CSFFile[]; constructor( protected channel: Channel, diff --git a/code/lib/preview-api/src/modules/preview-web/render/MdxDocsRender.ts b/code/lib/preview-api/src/modules/preview-web/render/MdxDocsRender.ts index 8486a9eb0ea5..ca81e01d438a 100644 --- a/code/lib/preview-api/src/modules/preview-web/render/MdxDocsRender.ts +++ b/code/lib/preview-api/src/modules/preview-web/render/MdxDocsRender.ts @@ -29,6 +29,8 @@ import { DocsContext } from '../docs-context/DocsContext'; export class MdxDocsRender implements Render { public readonly type: RenderType = 'docs'; + public readonly subtype = 'mdx'; + public readonly id: StoryId; private exports?: ModuleExports; @@ -43,7 +45,7 @@ export class MdxDocsRender implements Render[]; + public csfFiles?: CSFFile[]; constructor( protected channel: Channel, diff --git a/code/lib/preview-api/src/modules/store/csf/prepareStory.test.ts b/code/lib/preview-api/src/modules/store/csf/prepareStory.test.ts index f05d826fb8f8..4cf8f51a3860 100644 --- a/code/lib/preview-api/src/modules/store/csf/prepareStory.test.ts +++ b/code/lib/preview-api/src/modules/store/csf/prepareStory.test.ts @@ -724,10 +724,12 @@ describe('prepareMeta', () => { undecoratedStoryFn, playFunction, prepareContext, + // eslint-disable-next-line @typescript-eslint/naming-convention + parameters: { __isArgsStory, ...parameters }, ...expectedPreparedMeta } = preparedStory; - expect(preparedMeta).toMatchObject(expectedPreparedMeta); - expect(Object.keys(preparedMeta)).toHaveLength(Object.keys(expectedPreparedMeta).length); + expect(preparedMeta).toMatchObject({ ...expectedPreparedMeta, parameters }); + expect(Object.keys(preparedMeta)).toHaveLength(Object.keys(expectedPreparedMeta).length + 1); }); }); diff --git a/code/lib/preview-api/src/modules/store/csf/prepareStory.ts b/code/lib/preview-api/src/modules/store/csf/prepareStory.ts index 1be2aa63607e..e66161209323 100644 --- a/code/lib/preview-api/src/modules/store/csf/prepareStory.ts +++ b/code/lib/preview-api/src/modules/store/csf/prepareStory.ts @@ -169,8 +169,6 @@ function preparePartialAnnotations( // anything at render time. The assumption is that as we don't load all the stories at once, this // will have a limited cost. If this proves misguided, we can refactor it. - const id = storyAnnotations?.id || componentAnnotations.id; - const tags = [...(storyAnnotations?.tags || componentAnnotations.tags || []), 'story']; const parameters: Parameters = combineParameters( @@ -182,23 +180,26 @@ function preparePartialAnnotations( // Currently it is only possible to set these globally const { argTypesEnhancers = [], argsEnhancers = [] } = projectAnnotations; - // The render function on annotations *has* to be an `ArgsStoryFn`, so when we normalize - // CSFv1/2, we use a new field called `userStoryFn` so we know that it can be a LegacyStoryFn - const render = - storyAnnotations?.userStoryFn || - storyAnnotations?.render || - componentAnnotations.render || - projectAnnotations.render; - const passedArgTypes: StrictArgTypes = combineParameters( projectAnnotations.argTypes, componentAnnotations.argTypes, storyAnnotations?.argTypes ) as StrictArgTypes; - const { passArgsFirst = true } = parameters; - // eslint-disable-next-line no-underscore-dangle - parameters.__isArgsStory = passArgsFirst && render && render.length > 0; + if (storyAnnotations) { + // The render function on annotations *has* to be an `ArgsStoryFn`, so when we normalize + // CSFv1/2, we use a new field called `userStoryFn` so we know that it can be a LegacyStoryFn + const render = + storyAnnotations?.userStoryFn || + storyAnnotations?.render || + componentAnnotations.render || + projectAnnotations.render; + + const { passArgsFirst = true } = parameters; + + // eslint-disable-next-line no-underscore-dangle + parameters.__isArgsStory = passArgsFirst && render && render.length > 0; + } // Pull out args[X] into initialArgs for argTypes enhancers const passedArgs: Args = { From 8c6b20a9c8370246e620c46d5ea9b3e2bec2c690 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 17 Apr 2023 13:53:21 +1000 Subject: [PATCH 4/7] Store docs prepared parameters in the index --- code/lib/manager-api/src/lib/stories.ts | 2 +- code/lib/manager-api/src/modules/stories.ts | 38 ++++++++++++- .../lib/manager-api/src/tests/stories.test.ts | 53 ++++++++++++++++++- code/lib/types/src/modules/api-stories.ts | 5 ++ code/lib/types/src/modules/channelApi.ts | 13 +++++ 5 files changed, 107 insertions(+), 4 deletions(-) diff --git a/code/lib/manager-api/src/lib/stories.ts b/code/lib/manager-api/src/lib/stories.ts index 5b5f3d3b7191..51823a7cfa6b 100644 --- a/code/lib/manager-api/src/lib/stories.ts +++ b/code/lib/manager-api/src/lib/stories.ts @@ -261,7 +261,7 @@ export const transformStoryIndexToStoriesHash = ( depth: paths.length, parent: paths[paths.length - 1], renderLabel, - ...(item.type !== 'docs' && { prepared: !!item.parameters }), + prepared: !!item.parameters, // deprecated fields kind: item.title, diff --git a/code/lib/manager-api/src/modules/stories.ts b/code/lib/manager-api/src/modules/stories.ts index 16abb3a14a7f..c70ba2b2dfa3 100644 --- a/code/lib/manager-api/src/modules/stories.ts +++ b/code/lib/manager-api/src/modules/stories.ts @@ -15,6 +15,9 @@ import type { StoryIndex, API_LoadedRefData, API_IndexHash, + StoryPreparedPayload, + DocsPreparedPayload, + API_DocsEntry, } from '@storybook/types'; import { PRELOAD_ENTRIES, @@ -31,6 +34,7 @@ import { CONFIG_ERROR, CURRENT_STORY_WAS_SET, STORY_MISSING, + DOCS_PREPARED, } from '@storybook/core-events'; import { logger } from '@storybook/client-logger'; @@ -54,7 +58,10 @@ type Direction = -1 | 1; type ParameterName = string; type ViewMode = 'story' | 'info' | 'settings' | string | undefined; -type StoryUpdate = Pick; +type StoryUpdate = Partial< + Pick +>; +type DocsUpdate = Partial>; export interface SubState extends API_LoadedRefData { storyId: StoryId; @@ -93,6 +100,7 @@ export interface SubAPI { ): StoryId; fetchIndex: () => Promise; updateStory: (storyId: StoryId, update: StoryUpdate, ref?: API_ComposedRef) => Promise; + updateDocs: (storyId: StoryId, update: DocsUpdate, ref?: API_ComposedRef) => Promise; setPreviewInitialized: (ref?: ComposedRef) => Promise; } @@ -370,6 +378,27 @@ export const init: ModuleFn = ({ await fullAPI.updateRef(refId, { index }); } }, + updateDocs: async ( + docsId: StoryId, + update: DocsUpdate, + ref?: API_ComposedRef + ): Promise => { + if (!ref) { + const { index } = store.getState(); + index[docsId] = { + ...index[docsId], + ...update, + } as API_DocsEntry; + await store.setState({ index }); + } else { + const { id: refId, index } = ref; + index[docsId] = { + ...index[docsId], + ...update, + } as API_DocsEntry; + await fullAPI.updateRef(refId, { index }); + } + }, setPreviewInitialized: async (ref?: ComposedRef): Promise => { if (!ref) { store.setState({ previewInitialized: true }); @@ -428,7 +457,7 @@ export const init: ModuleFn = ({ } }); - fullAPI.on(STORY_PREPARED, function handler({ id, ...update }) { + fullAPI.on(STORY_PREPARED, function handler({ id, ...update }: StoryPreparedPayload) { const { ref, sourceType } = getEventMetadata(this, fullAPI); fullAPI.updateStory(id, { ...update, prepared: true }, ref); @@ -458,6 +487,11 @@ export const init: ModuleFn = ({ } }); + fullAPI.on(DOCS_PREPARED, function handler({ id, ...update }: DocsPreparedPayload) { + const { ref } = getEventMetadata(this, fullAPI); + fullAPI.updateStory(id, { ...update, prepared: true }, ref); + }); + fullAPI.on(SET_INDEX, function handler(index: API_PreparedStoryIndex) { const { ref } = getEventMetadata(this, fullAPI); diff --git a/code/lib/manager-api/src/tests/stories.test.ts b/code/lib/manager-api/src/tests/stories.test.ts index 60aa2a16e5e2..fc73f4598113 100644 --- a/code/lib/manager-api/src/tests/stories.test.ts +++ b/code/lib/manager-api/src/tests/stories.test.ts @@ -48,6 +48,14 @@ jest.mock('@storybook/global', () => ({ const getEventMetadataMock = getEventMetadata as ReturnType; const mockEntries: StoryIndex['entries'] = { + 'component-a--docs': { + type: 'docs', + id: 'component-a--docs', + title: 'Component A', + name: 'Docs', + importPath: './path/to/component-a.ts', + storiesImports: [], + }, 'component-a--story-1': { type: 'story', id: 'component-a--story-1', @@ -139,6 +147,7 @@ describe('stories API', () => { // We need exact key ordering, even if in theory JS doesn't guarantee it expect(Object.keys(index)).toEqual([ 'component-a', + 'component-a--docs', 'component-a--story-1', 'component-a--story-2', 'component-b', @@ -147,7 +156,17 @@ describe('stories API', () => { expect(index['component-a']).toMatchObject({ type: 'component', id: 'component-a', - children: ['component-a--story-1', 'component-a--story-2'], + children: ['component-a--docs', 'component-a--story-1', 'component-a--story-2'], + }); + + expect(index['component-a--docs']).toMatchObject({ + type: 'docs', + id: 'component-a--docs', + parent: 'component-a', + title: 'Component A', + name: 'Docs', + storiesImports: [], + prepared: false, }); expect(index['component-a--story-1']).toMatchObject({ @@ -232,6 +251,7 @@ describe('stories API', () => { // We need exact key ordering, even if in theory JS doesn't guarantee it expect(Object.keys(index)).toEqual([ 'component-a', + 'component-a--docs', 'component-a--story-1', 'component-a--story-2', 'component-b', @@ -1424,6 +1444,37 @@ describe('stories API', () => { }); }); + describe('DOCS_PREPARED', () => { + it('prepares the docs entry', async () => { + const navigate = jest.fn(); + const store = createMockStore(); + const fullAPI = Object.assign(new EventEmitter(), { + setStories: jest.fn(), + setOptions: jest.fn(), + }); + + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); + Object.assign(fullAPI, api); + + await init(); + fullAPI.emit(STORY_PREPARED, { + id: 'component-a--docs', + parameters: { a: 'b' }, + }); + + const { index } = store.getState(); + expect(index['component-a--docs']).toMatchObject({ + type: 'docs', + id: 'component-a--docs', + parent: 'component-a', + title: 'Component A', + name: 'Docs', + prepared: true, + parameters: { a: 'b' }, + }); + }); + }); + describe('CONFIG_ERROR', () => { it('sets previewInitialized to true, local', async () => { const navigate = jest.fn(); diff --git a/code/lib/types/src/modules/api-stories.ts b/code/lib/types/src/modules/api-stories.ts index 26999b6f22a6..7cfd32b22284 100644 --- a/code/lib/types/src/modules/api-stories.ts +++ b/code/lib/types/src/modules/api-stories.ts @@ -64,6 +64,11 @@ export interface API_DocsEntry extends API_BaseEntry { /** @deprecated */ kind: ComponentTitle; importPath: Path; + tags: Tag[]; + prepared: boolean; + parameters?: { + [parameterName: string]: any; + }; /** @deprecated */ isRoot: false; diff --git a/code/lib/types/src/modules/channelApi.ts b/code/lib/types/src/modules/channelApi.ts index 2020fb0409ad..0fe35baf824a 100644 --- a/code/lib/types/src/modules/channelApi.ts +++ b/code/lib/types/src/modules/channelApi.ts @@ -55,3 +55,16 @@ export interface SetGlobalsPayload { globals: Globals; globalTypes: GlobalTypes; } + +export interface StoryPreparedPayload { + id: StoryId; + parameters: Parameters; + argTypes: ArgTypes; + initialArgs: Args; + args: Args; +} + +export interface DocsPreparedPayload { + id: StoryId; + parameters: Parameters; +} From 38b13e83b2d0ad392f106ffd4bb8bddd33bc47a1 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 17 Apr 2023 13:54:15 +1000 Subject: [PATCH 5/7] Make `getParameter` work for both story and docs --- code/lib/manager-api/src/modules/stories.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/lib/manager-api/src/modules/stories.ts b/code/lib/manager-api/src/modules/stories.ts index c70ba2b2dfa3..0d8b7b87659c 100644 --- a/code/lib/manager-api/src/modules/stories.ts +++ b/code/lib/manager-api/src/modules/stories.ts @@ -165,7 +165,7 @@ export const init: ModuleFn = ({ : storyIdOrCombo; const data = api.getData(storyId, refId); - if (data?.type === 'story') { + if (['story', 'docs'].includes(data?.type)) { const { parameters } = data; if (parameters) { From a2801d10d68cdebcd50812383582e8243e59d562 Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Thu, 13 Apr 2023 21:47:15 +1000 Subject: [PATCH 6/7] Add an E2E test of parameters behaviour --- code/e2e-tests/addon-backgrounds.spec.ts | 32 ++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/code/e2e-tests/addon-backgrounds.spec.ts b/code/e2e-tests/addon-backgrounds.spec.ts index 5dfb3e46c05d..e7a7027f6c04 100644 --- a/code/e2e-tests/addon-backgrounds.spec.ts +++ b/code/e2e-tests/addon-backgrounds.spec.ts @@ -10,11 +10,14 @@ test.describe('addon-backgrounds', () => { await new SbPage(page).waitUntilLoaded(); }); + const backgroundToolbarSelector = '[title="Change the background of the preview"]'; + const gridToolbarSelector = '[title="Apply a grid to the preview"]'; + test('should have a dark background', async ({ page }) => { const sbPage = new SbPage(page); await sbPage.navigateToStory('example/button', 'primary'); - await sbPage.selectToolbar('[title="Change the background of the preview"]', '#list-item-dark'); + await sbPage.selectToolbar(backgroundToolbarSelector, '#list-item-dark'); await expect(sbPage.getCanvasBodyElement()).toHaveCSS('background-color', 'rgb(51, 51, 51)'); }); @@ -23,8 +26,33 @@ test.describe('addon-backgrounds', () => { const sbPage = new SbPage(page); await sbPage.navigateToStory('example/button', 'primary'); - await sbPage.selectToolbar('[title="Apply a grid to the preview"]'); + await sbPage.selectToolbar(gridToolbarSelector); await expect(sbPage.getCanvasBodyElement()).toHaveCSS('background-image', /linear-gradient/); }); + + test('button should appear for story pages', async ({ page }) => { + const sbPage = new SbPage(page); + + await sbPage.navigateToStory('example/button', 'primary'); + await expect(sbPage.page.locator(backgroundToolbarSelector)).toBeVisible(); + }); + + test('button should appear for attached docs pages', async ({ page }) => { + const sbPage = new SbPage(page); + + await sbPage.navigateToStory('example/button', 'docs'); + await expect(sbPage.page.locator(backgroundToolbarSelector)).toBeVisible(); + }); + + test('button should appear for unattached docs pages', async ({ page }) => { + const sbPage = new SbPage(page); + + // We start on the introduction page by default. + await sbPage.page.waitForURL((url) => + url.search.includes(`path=/docs/example-introduction--docs`) + ); + + await expect(sbPage.page.locator(backgroundToolbarSelector)).toBeVisible(); + }); }); From 03fecfe982630ae710c5ae00e0aa4f336992d7ee Mon Sep 17 00:00:00 2001 From: Tom Coleman Date: Mon, 17 Apr 2023 13:57:21 +1000 Subject: [PATCH 7/7] Update exports --- code/ui/manager/src/globals/exports.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/code/ui/manager/src/globals/exports.ts b/code/ui/manager/src/globals/exports.ts index c6319da64a47..f21d954b5191 100644 --- a/code/ui/manager/src/globals/exports.ts +++ b/code/ui/manager/src/globals/exports.ts @@ -118,6 +118,7 @@ export default { 'CHANNEL_CREATED', 'CONFIG_ERROR', 'CURRENT_STORY_WAS_SET', + 'DOCS_PREPARED', 'DOCS_RENDERED', 'FORCE_REMOUNT', 'FORCE_RE_RENDER',