diff --git a/code/lib/manager-api/src/index.tsx b/code/lib/manager-api/src/index.tsx index d1c1096e74fc..28ba051652a0 100644 --- a/code/lib/manager-api/src/index.tsx +++ b/code/lib/manager-api/src/index.tsx @@ -18,13 +18,13 @@ import type { API_DocsEntry, API_GroupEntry, API_HashEntry, + API_IndexHash, API_LeafEntry, API_OptionsData, API_ProviderData, API_Refs, API_RootEntry, API_StateMerger, - API_StoriesHash, API_StoryEntry, Parameters, StoryId, @@ -326,7 +326,9 @@ export function useStorybookApi(): API { } export type { - API_StoriesHash as StoriesHash, + /** @deprecated now IndexHash */ + API_IndexHash as StoriesHash, + API_IndexHash as IndexHash, API_RootEntry as RootEntry, API_GroupEntry as GroupEntry, API_ComponentEntry as ComponentEntry, diff --git a/code/lib/manager-api/src/lib/stories.ts b/code/lib/manager-api/src/lib/stories.ts index 955579139292..3db7b6480e07 100644 --- a/code/lib/manager-api/src/lib/stories.ts +++ b/code/lib/manager-api/src/lib/stories.ts @@ -15,7 +15,7 @@ import type { API_RootEntry, API_GroupEntry, API_ComponentEntry, - API_StoriesHash, + API_IndexHash, API_DocsEntry, API_StoryEntry, API_HashEntry, @@ -122,7 +122,7 @@ export const transformStoryIndexToStoriesHash = ( provider: API_Provider; docsOptions: DocsOptions; } -): API_StoriesHash => { +): API_IndexHash => { if (!index.v) throw new Error('Composition: Missing stories.json version'); const v4Index = index.v === 4 ? index : transformStoryIndexV3toV4(index as any); @@ -241,10 +241,10 @@ export const transformStoryIndexToStoriesHash = ( } as API_DocsEntry | API_StoryEntry; return acc; - }, {} as API_StoriesHash); + }, {} as API_IndexHash); // This function adds a "root" or "orphan" and all of its descendents to the hash. - function addItem(acc: API_StoriesHash, item: API_HashEntry) { + function addItem(acc: API_IndexHash, item: API_HashEntry) { // If we were already inserted as part of a group, that's great. if (acc[item.id]) { return acc; @@ -268,7 +268,7 @@ export const transformStoryIndexToStoriesHash = ( .reduce(addItem, orphanHash); }; -export const addPreparedStories = (newHash: API_StoriesHash, oldHash?: API_StoriesHash) => { +export const addPreparedStories = (newHash: API_IndexHash, oldHash?: API_IndexHash) => { if (!oldHash) return newHash; return Object.fromEntries( @@ -283,7 +283,7 @@ export const addPreparedStories = (newHash: API_StoriesHash, oldHash?: API_Stori ); }; -export const getComponentLookupList = memoize(1)((hash: API_StoriesHash) => { +export const getComponentLookupList = memoize(1)((hash: API_IndexHash) => { return Object.entries(hash).reduce((acc, i) => { const value = i[1]; if (value.type === 'component') { @@ -293,6 +293,6 @@ export const getComponentLookupList = memoize(1)((hash: API_StoriesHash) => { }, [] as StoryId[][]); }); -export const getStoriesLookupList = memoize(1)((hash: API_StoriesHash) => { +export const getStoriesLookupList = memoize(1)((hash: API_IndexHash) => { return Object.keys(hash).filter((k) => ['story', 'docs'].includes(hash[k].type)); }); diff --git a/code/lib/manager-api/src/modules/refs.ts b/code/lib/manager-api/src/modules/refs.ts index e8016da9e80d..c56f08fed9e3 100644 --- a/code/lib/manager-api/src/modules/refs.ts +++ b/code/lib/manager-api/src/modules/refs.ts @@ -6,7 +6,7 @@ import type { API_Refs, API_SetRefData, SetStoriesStoryData, - API_StoriesHash, + API_IndexHash, API_StoryMapper, } from '@storybook/types'; // eslint-disable-next-line import/no-cycle @@ -33,7 +33,7 @@ export interface SubAPI { getRefs: () => API_Refs; checkRef: (ref: API_SetRefData) => Promise; changeRefVersion: (id: string, url: string) => void; - changeRefState: (id: string, ready: boolean) => void; + changeRefState: (id: string, previewInitialized: boolean) => void; } export const getSourceType = (source: string, refId: string) => { @@ -56,10 +56,10 @@ export const defaultStoryMapper: API_StoryMapper = (b, a) => { return { ...a, kind: a.kind.replace('|', '/') }; }; -const addRefIds = (input: API_StoriesHash, ref: API_ComposedRef): API_StoriesHash => { +const addRefIds = (input: API_IndexHash, ref: API_ComposedRef): API_IndexHash => { return Object.entries(input).reduce((acc, [id, item]) => { return { ...acc, [id]: { ...item, refId: ref.id } }; - }, {} as API_StoriesHash); + }, {} as API_IndexHash); }; async function handleRequest( @@ -83,8 +83,8 @@ async function handleRequest( } return json as API_SetRefData; - } catch (error) { - return { error }; + } catch (err) { + return { indexError: err }; } } @@ -139,10 +139,10 @@ export const init: ModuleFn = ( api.checkRef(ref); }, - changeRefState: (id, ready) => { + changeRefState: (id, previewInitialized) => { const { [id]: ref, ...updated } = api.getRefs(); - updated[id] = { ...ref, ready }; + updated[id] = { ...ref, previewInitialized }; store.setState({ refs: updated, @@ -205,7 +205,7 @@ export const init: ModuleFn = ( // In theory the `/iframe.html` could be private and the `stories.json` could not exist, but in practice // the only private servers we know about (Chromatic) always include `stories.json`. So we can tell // if the ref actually exists by simply checking `stories.json` w/ credentials. - loadedData.error = { + loadedData.indexError = { message: dedent` Error: Loading of ref failed at fetch (lib/api/src/modules/refs.ts) @@ -245,18 +245,18 @@ export const init: ModuleFn = ( const { storyMapper = defaultStoryMapper } = provider.getConfig(); const ref = api.getRefs()[id]; - let storiesHash: API_StoriesHash; + let index: API_IndexHash; if (setStoriesData) { - storiesHash = transformSetStoriesStoryDataToStoriesHash( + index = transformSetStoriesStoryDataToStoriesHash( map(setStoriesData, ref, { storyMapper }), { provider, docsOptions } ); } else if (storyIndex) { - storiesHash = transformStoryIndexToStoriesHash(storyIndex, { provider, docsOptions }); + index = transformStoryIndexToStoriesHash(storyIndex, { provider, docsOptions }); } - if (storiesHash) storiesHash = addRefIds(storiesHash, ref); + if (index) index = addRefIds(index, ref); - api.updateRef(id, { stories: storiesHash, ...rest, ready }); + api.updateRef(id, { index, ...rest }); }, updateRef: (id, data) => { diff --git a/code/lib/manager-api/src/modules/stories.ts b/code/lib/manager-api/src/modules/stories.ts index d9a70c6c56da..415b801d788f 100644 --- a/code/lib/manager-api/src/modules/stories.ts +++ b/code/lib/manager-api/src/modules/stories.ts @@ -13,8 +13,10 @@ import { STORY_SPECIFIED, STORY_INDEX_INVALIDATED, CONFIG_ERROR, + CURRENT_STORY_WAS_SET, + STORY_MISSING, } from '@storybook/core-events'; -import { logger } from '@storybook/client-logger'; +import { deprecate, logger } from '@storybook/client-logger'; import type { StoryId, @@ -24,9 +26,10 @@ import type { API_LeafEntry, API_PreparedStoryIndex, SetStoriesPayload, - API_StoriesHash, API_StoryEntry, StoryIndex, + API_LoadedRefData, + API_IndexHash, } from '@storybook/types'; // eslint-disable-next-line import/no-cycle import { getEventMetadata } from '../lib/events'; @@ -39,7 +42,7 @@ import { addPreparedStories, } from '../lib/stories'; -import type { ModuleFn } from '../index'; +import type { ComposedRef, ModuleFn } from '../index'; const { FEATURES, fetch } = global; const STORY_INDEX_PATH = './index.json'; @@ -50,11 +53,21 @@ type ParameterName = string; type ViewMode = 'story' | 'info' | 'settings' | string | undefined; type StoryUpdate = Pick; -export interface SubState { - storiesHash: API_StoriesHash; +export interface SubState extends API_LoadedRefData { storyId: StoryId; viewMode: ViewMode; + + /** + * @deprecated use index + */ + storiesHash: API_IndexHash; + /** + * @deprecated use previewInitialized + */ storiesConfigured: boolean; + /** + * @deprecated use indexError + */ storiesFailed?: Error; } @@ -80,16 +93,17 @@ export interface SubAPI { getCurrentParameter(parameterName?: ParameterName): S; updateStoryArgs(story: API_StoryEntry, newArgs: Args): void; resetStoryArgs: (story: API_StoryEntry, argNames?: string[]) => void; - findLeafEntry(StoriesHash: API_StoriesHash, storyId: StoryId): API_LeafEntry; - findLeafStoryId(StoriesHash: API_StoriesHash, storyId: StoryId): StoryId; + findLeafEntry(index: API_IndexHash, storyId: StoryId): API_LeafEntry; + findLeafStoryId(index: API_IndexHash, storyId: StoryId): StoryId; findSiblingStoryId( storyId: StoryId, - hash: API_StoriesHash, + index: API_IndexHash, direction: Direction, toSiblingGroup: boolean // when true, skip over leafs within the same group ): StoryId; fetchIndex: () => Promise; updateStory: (storyId: StoryId, update: StoryUpdate, ref?: API_ComposedRef) => Promise; + setPreviewInitialized: (ref?: ComposedRef) => Promise; } const removedOptions = ['enableShortcuts', 'theme', 'showRoots']; @@ -132,11 +146,11 @@ export const init: ModuleFn = ({ return data.type === 'story' ? data.prepared : true; }, resolveStory: (storyId, refId) => { - const { refs, storiesHash } = store.getState(); + const { refs, index } = store.getState(); if (refId) { - return refs[refId].stories ? refs[refId].stories[storyId] : undefined; + return refs[refId].index ? refs[refId].index[storyId] : undefined; } - return storiesHash ? storiesHash[storyId] : undefined; + return index ? index[storyId] : undefined; }, getCurrentStoryData: () => { const { storyId, refId } = store.getState(); @@ -168,7 +182,7 @@ export const init: ModuleFn = ({ return parameters || undefined; }, jumpToComponent: (direction) => { - const { storiesHash, storyId, refs, refId } = store.getState(); + const { index, storyId, refs, refId } = store.getState(); const story = api.getData(storyId, refId); // cannot navigate when there's no current selection @@ -176,7 +190,7 @@ export const init: ModuleFn = ({ return; } - const hash = refId ? refs[refId].stories || {} : storiesHash; + const hash = refId ? refs[refId].index || {} : index; const result = api.findSiblingStoryId(storyId, hash, direction, true); if (result) { @@ -184,7 +198,7 @@ export const init: ModuleFn = ({ } }, jumpToStory: (direction) => { - const { storiesHash, storyId, refs, refId } = store.getState(); + const { index, storyId, refs, refId } = store.getState(); const story = api.getData(storyId, refId); // cannot navigate when there's no current selection @@ -192,7 +206,7 @@ export const init: ModuleFn = ({ return; } - const hash = story.refId ? refs[story.refId].stories : storiesHash; + const hash = story.refId ? refs[story.refId].index : index; const result = api.findSiblingStoryId(storyId, hash, direction, false); if (result) { @@ -200,8 +214,8 @@ export const init: ModuleFn = ({ } }, selectFirstStory: () => { - const { storiesHash } = store.getState(); - const firstStory = Object.keys(storiesHash).find((id) => storiesHash[id].type === 'story'); + const { index } = store.getState(); + const firstStory = Object.keys(index).find((id) => index[id].type === 'story'); if (firstStory) { api.selectStory(firstStory); @@ -212,9 +226,9 @@ export const init: ModuleFn = ({ }, selectStory: (titleOrId = undefined, name = undefined, options = {}) => { const { ref } = options; - const { storyId, storiesHash, refs } = store.getState(); + const { storyId, index, refs } = store.getState(); - const hash = ref ? refs[ref].stories : storiesHash; + const hash = ref ? refs[ref].index : index; const kindSlug = storyId?.split('--', 2)[0]; @@ -249,50 +263,50 @@ export const init: ModuleFn = ({ } } }, - findLeafEntry(storiesHash, storyId) { - const entry = storiesHash[storyId]; + findLeafEntry(index, storyId) { + const entry = index[storyId]; if (entry.type === 'docs' || entry.type === 'story') { return entry; } const childStoryId = entry.children[0]; - return api.findLeafEntry(storiesHash, childStoryId); + return api.findLeafEntry(index, childStoryId); }, - findLeafStoryId(storiesHash, storyId) { - return api.findLeafEntry(storiesHash, storyId)?.id; + findLeafStoryId(index, storyId) { + return api.findLeafEntry(index, storyId)?.id; }, - findSiblingStoryId(storyId, hash, direction, toSiblingGroup) { + findSiblingStoryId(storyId, index, direction, toSiblingGroup) { if (toSiblingGroup) { - const lookupList = getComponentLookupList(hash); - const index = lookupList.findIndex((i) => i.includes(storyId)); + const lookupList = getComponentLookupList(index); + const position = lookupList.findIndex((i) => i.includes(storyId)); // cannot navigate beyond fist or last - if (index === lookupList.length - 1 && direction > 0) { + if (position === lookupList.length - 1 && direction > 0) { return; } - if (index === 0 && direction < 0) { + if (position === 0 && direction < 0) { return; } - if (lookupList[index + direction]) { + if (lookupList[position + direction]) { // eslint-disable-next-line consistent-return - return lookupList[index + direction][0]; + return lookupList[position + direction][0]; } return; } - const lookupList = getStoriesLookupList(hash); - const index = lookupList.indexOf(storyId); + const lookupList = getStoriesLookupList(index); + const position = lookupList.indexOf(storyId); // cannot navigate beyond fist or last - if (index === lookupList.length - 1 && direction > 0) { + if (position === lookupList.length - 1 && direction > 0) { return; } - if (index === 0 && direction < 0) { + if (position === 0 && direction < 0) { return; } // eslint-disable-next-line consistent-return - return lookupList[index + direction]; + return lookupList[position + direction]; }, updateStoryArgs: (story, updatedArgs) => { const { id: storyId, refId } = story; @@ -325,10 +339,7 @@ export const init: ModuleFn = ({ await fullAPI.setIndex(storyIndex); } catch (err) { - store.setState({ - storiesConfigured: true, - storiesFailed: err, - }); + await store.setState({ indexError: err }); } }, // The story index we receive on SET_INDEX is "prepared" in that it has parameters @@ -341,13 +352,9 @@ export const init: ModuleFn = ({ }); // Now we need to patch in the existing prepared stories - const oldHash = store.getState().storiesHash; + const oldHash = store.getState().index; - await store.setState({ - storiesHash: addPreparedStories(newHash, oldHash), - storiesConfigured: true, - storiesFailed: null, - }); + await store.setState({ index: addPreparedStories(newHash, oldHash) }); }, updateStory: async ( storyId: StoryId, @@ -355,19 +362,26 @@ export const init: ModuleFn = ({ ref?: API_ComposedRef ): Promise => { if (!ref) { - const { storiesHash } = store.getState(); - storiesHash[storyId] = { - ...storiesHash[storyId], + const { index } = store.getState(); + index[storyId] = { + ...index[storyId], ...update, } as API_StoryEntry; - await store.setState({ storiesHash }); + await store.setState({ index }); } else { - const { id: refId, stories } = ref; - stories[storyId] = { - ...stories[storyId], + const { id: refId, index } = ref; + index[storyId] = { + ...index[storyId], ...update, } as API_StoryEntry; - await fullAPI.updateRef(refId, { stories }); + await fullAPI.updateRef(refId, { index }); + } + }, + setPreviewInitialized: async (ref?: ComposedRef): Promise => { + if (!ref) { + store.setState({ previewInitialized: true }); + } else { + fullAPI.updateRef(ref.id, { previewInitialized: true }); } }, }; @@ -387,9 +401,9 @@ export const init: ModuleFn = ({ }) { const { sourceType } = getEventMetadata(this, fullAPI); - if (fullAPI.isSettingsScreenActive()) return; - if (sourceType === 'local') { + if (fullAPI.isSettingsScreenActive()) return; + // Special case -- if we are already at the story being specified (i.e. the user started at a given story), // we don't need to change URL. See https://github.com/storybookjs/storybook/issues/11677 const state = store.getState(); @@ -400,6 +414,15 @@ export const init: ModuleFn = ({ } ); + // The CURRENT_STORY_WAS_SET event is the best event to use to tell if a ref is ready. + // Until the ref has a selection, it will not render anything (e.g. while waiting for + // the preview.js file or the index to load). Once it has a selection, it will render its own + // preparing spinner. + fullAPI.on(CURRENT_STORY_WAS_SET, function handler() { + const { ref } = getEventMetadata(this, fullAPI); + fullAPI.setPreviewInitialized(ref); + }); + fullAPI.on(STORY_CHANGED, function handler() { const { sourceType } = getEventMetadata(this, fullAPI); @@ -422,18 +445,16 @@ export const init: ModuleFn = ({ fullAPI.setOptions(removeRemovedOptions(options)); store.setState({ hasCalledSetOptions: true }); } - } else { - fullAPI.updateRef(ref.id, { ready: true }); } if (sourceType === 'local') { - const { storyId, storiesHash, refId } = store.getState(); + const { storyId, index, refId } = store.getState(); // create a list of related stories to be preloaded const toBePreloaded = Array.from( new Set([ - api.findSiblingStoryId(storyId, storiesHash, 1, true), - api.findSiblingStoryId(storyId, storiesHash, -1, true), + api.findSiblingStoryId(storyId, index, 1, true), + api.findSiblingStoryId(storyId, index, -1, true), ]) ).filter(Boolean); @@ -499,11 +520,15 @@ export const init: ModuleFn = ({ } ); + // When there's a preview error, we don't show it in the manager, but simply fullAPI.on(CONFIG_ERROR, function handleConfigError(err) { - store.setState({ - storiesConfigured: true, - storiesFailed: err, - }); + const { ref } = getEventMetadata(this, fullAPI); + fullAPI.setPreviewInitialized(ref); + }); + + fullAPI.on(STORY_MISSING, function handleConfigError(err) { + const { ref } = getEventMetadata(this, fullAPI); + fullAPI.setPreviewInitialized(ref); }); if (FEATURES?.storyStoreV7) { @@ -515,11 +540,24 @@ export const init: ModuleFn = ({ return { api, state: { - storiesHash: {}, storyId: initialStoryId, viewMode: initialViewMode, - storiesConfigured: false, hasCalledSetOptions: false, + previewInitialized: false, + + // deprecated fields for back-compat + get storiesHash() { + deprecate('state.storiesHash is deprecated, please use state.index'); + return this.index || {}; + }, + get storiesConfigured() { + deprecate('state.storiesConfigured is deprecated, please use state.previewInitialized'); + return this.previewInitialized; + }, + get storiesFailed() { + deprecate('state.storiesFailed is deprecated, please use state.indexError'); + return this.indexError; + }, }, init: initModule, }; diff --git a/code/lib/manager-api/src/tests/refs.test.js b/code/lib/manager-api/src/tests/refs.test.js index 8e7d542da427..d2ddd297e397 100644 --- a/code/lib/manager-api/src/tests/refs.test.js +++ b/code/lib/manager-api/src/tests/refs.test.js @@ -252,7 +252,9 @@ describe('Refs API', () => { Object { "refs": Object { "fake": Object { - "error": Object { + "id": "fake", + "index": undefined, + "indexError": Object { "message": "Error: Loading of ref failed at fetch (lib/api/src/modules/refs.ts) @@ -263,9 +265,6 @@ describe('Refs API', () => { Please check your dev-tools network tab.", }, - "id": "fake", - "ready": false, - "stories": undefined, "title": "Fake", "type": "auto-inject", "url": "https://example.com", @@ -340,8 +339,7 @@ describe('Refs API', () => { "refs": Object { "fake": Object { "id": "fake", - "ready": false, - "stories": Object {}, + "index": Object {}, "title": "Fake", "type": "lazy", "url": "https://example.com", @@ -418,8 +416,7 @@ describe('Refs API', () => { "refs": Object { "fake": Object { "id": "fake", - "ready": false, - "stories": Object {}, + "index": Object {}, "title": "Fake", "type": "lazy", "url": "https://example.com", @@ -496,9 +493,8 @@ describe('Refs API', () => { "refs": Object { "fake": Object { "id": "fake", + "index": undefined, "loginUrl": "https://example.com/login", - "ready": false, - "stories": undefined, "title": "Fake", "type": "auto-inject", "url": "https://example.com", @@ -638,9 +634,8 @@ describe('Refs API', () => { "refs": Object { "fake": Object { "id": "fake", + "index": undefined, "loginUrl": "https://example.com/login", - "ready": false, - "stories": undefined, "title": "Fake", "type": "auto-inject", "url": "https://example.com", @@ -720,8 +715,7 @@ describe('Refs API', () => { "refs": Object { "fake": Object { "id": "fake", - "ready": false, - "stories": Object {}, + "index": Object {}, "title": "Fake", "type": "lazy", "url": "https://example.com", @@ -798,8 +792,7 @@ describe('Refs API', () => { "refs": Object { "fake": Object { "id": "fake", - "ready": false, - "stories": Object {}, + "index": Object {}, "title": "Fake", "type": "lazy", "url": "https://example.com", @@ -866,7 +859,7 @@ describe('Refs API', () => { }); const { refs } = store.setState.mock.calls[0][0]; - const hash = refs.fake.stories; + const hash = refs.fake.index; // We need exact key ordering, even if in theory JS doesn't guarantee it expect(Object.keys(hash)).toEqual([ @@ -922,7 +915,7 @@ describe('Refs API', () => { }); const { refs } = store.setState.mock.calls[0][0]; - const hash = refs.fake.stories; + const hash = refs.fake.index; // We need exact key ordering, even if in theory JS doesn't guarantee it expect(Object.keys(hash)).toEqual(['component-a', 'component-a--docs']); diff --git a/code/lib/manager-api/src/tests/stories.test.ts b/code/lib/manager-api/src/tests/stories.test.ts index 25bd3355c245..83410d1604e2 100644 --- a/code/lib/manager-api/src/tests/stories.test.ts +++ b/code/lib/manager-api/src/tests/stories.test.ts @@ -9,6 +9,8 @@ import { STORY_INDEX_INVALIDATED, CONFIG_ERROR, SET_INDEX, + CURRENT_STORY_WAS_SET, + STORY_MISSING, } from '@storybook/core-events'; import { EventEmitter } from 'events'; import { global } from '@storybook/global'; @@ -80,6 +82,18 @@ function createMockStore(initialState = {}) { } as any as Store; } +function initStoriesAndSetState({ store, ...options }: any) { + const { state, ...result } = initStories({ store, ...options } as any); + + // Remove deprecated fields (which would trigger warnings) + delete state.storiesHash; + delete state.storiesConfigured; + delete state.storiesFailed; + store?.setState(state); + + return { state, ...result }; +} + const provider = { getConfig: jest.fn().mockReturnValue({}), serverChannel: mockChannel() }; beforeEach(() => { @@ -101,14 +115,18 @@ beforeEach(() => { describe('stories API', () => { it('sets a sensible initialState', () => { - const { state } = initStories({ + const { state } = initStoriesAndSetState({ storyId: 'id', viewMode: 'story', } as ModuleArgs); + // Remove deprecated fields (which would trigger warnings) + delete state.storiesHash; + delete state.storiesConfigured; + delete state.storiesFailed; + expect(state).toEqual({ - storiesConfigured: false, - storiesHash: {}, + previewInitialized: false, storyId: 'id', viewMode: 'story', hasCalledSetOptions: false, @@ -121,27 +139,27 @@ describe('stories API', () => { const store = createMockStore(); const fullAPI = Object.assign(new EventEmitter()); - const { api } = initStories({ store, navigate, provider, fullAPI } as any); + const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); api.setIndex({ v: 4, entries: mockEntries }); - const { storiesHash: storedStoriesHash } = store.getState(); + const { index } = store.getState(); // We need exact key ordering, even if in theory JS doesn't guarantee it - expect(Object.keys(storedStoriesHash)).toEqual([ + expect(Object.keys(index)).toEqual([ 'component-a', 'component-a--story-1', 'component-a--story-2', 'component-b', 'component-b--story-3', ]); - expect(storedStoriesHash['component-a']).toMatchObject({ + expect(index['component-a']).toMatchObject({ type: 'component', id: 'component-a', children: ['component-a--story-1', 'component-a--story-2'], }); - expect(storedStoriesHash['component-a--story-1']).toMatchObject({ + expect(index['component-a--story-1']).toMatchObject({ type: 'story', id: 'component-a--story-1', parent: 'component-a', @@ -150,7 +168,7 @@ describe('stories API', () => { prepared: false, }); expect( - (storedStoriesHash['component-a--story-1'] as API_StoryEntry as API_StoryEntry).args + (index['component-a--story-1'] as API_StoryEntry as API_StoryEntry).args ).toBeUndefined(); }); @@ -159,7 +177,7 @@ describe('stories API', () => { const store = createMockStore(); const fullAPI = Object.assign(new EventEmitter()); - const { api } = initStories({ store, navigate, provider, fullAPI } as any); + const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); api.setIndex({ @@ -174,23 +192,23 @@ describe('stories API', () => { }, }, }); - const { storiesHash: storedStoriesHash } = store.getState(); + const { index } = store.getState(); // We need exact key ordering, even if in theory JS doesn't guarantee it - expect(Object.keys(storedStoriesHash)).toEqual([ + expect(Object.keys(index)).toEqual([ 'design-system', 'design-system-some-component', 'design-system-some-component--my-story', ]); - expect(storedStoriesHash['design-system']).toMatchObject({ + expect(index['design-system']).toMatchObject({ type: 'root', name: 'Design System', // root name originates from `kind`, so it gets trimmed }); - expect(storedStoriesHash['design-system-some-component']).toMatchObject({ + expect(index['design-system-some-component']).toMatchObject({ type: 'component', name: 'Some Component', // component name originates from `kind`, so it gets trimmed }); - expect(storedStoriesHash['design-system-some-component--my-story']).toMatchObject({ + expect(index['design-system-some-component--my-story']).toMatchObject({ type: 'story', title: ' Design System / Some Component ', // title is kept as-is, because it may be used as identifier name: ' My Story ', // story name is kept as-is, because it's set directly on the story @@ -202,7 +220,7 @@ describe('stories API', () => { const store = createMockStore(); const fullAPI = Object.assign(new EventEmitter()); - const { api } = initStories({ store, navigate, provider, fullAPI } as any); + const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); api.setIndex({ @@ -218,10 +236,10 @@ describe('stories API', () => { ...mockEntries, }, }); - const { storiesHash: storedStoriesHash } = store.getState(); + const { index } = store.getState(); // We need exact key ordering, even if in theory JS doesn't guarantee it - expect(Object.keys(storedStoriesHash)).toEqual([ + expect(Object.keys(index)).toEqual([ 'component-a', 'component-a--story-1', 'component-a--story-2', @@ -231,7 +249,7 @@ describe('stories API', () => { 'root-first', 'root-first--story-1', ]); - expect(storedStoriesHash.root).toMatchObject({ + expect(index.root).toMatchObject({ type: 'root', id: 'root', children: ['root-first'], @@ -243,7 +261,7 @@ describe('stories API', () => { const store = createMockStore(); const fullAPI = Object.assign(new EventEmitter()); - const { api } = initStories({ store, navigate, provider, fullAPI } as any); + const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); provider.getConfig.mockReturnValue({ sidebar: { showRoots: true } }); @@ -260,22 +278,22 @@ describe('stories API', () => { }, }); - const { storiesHash: storedStoriesHash } = store.getState(); + const { index } = store.getState(); // We need exact key ordering, even if in theory JS doens't guarantee it - expect(Object.keys(storedStoriesHash)).toEqual(['a', 'a-b', 'a-b--1']); - expect(storedStoriesHash.a).toMatchObject({ + expect(Object.keys(index)).toEqual(['a', 'a-b', 'a-b--1']); + expect(index.a).toMatchObject({ type: 'root', id: 'a', children: ['a-b'], }); - expect(storedStoriesHash['a-b']).toMatchObject({ + expect(index['a-b']).toMatchObject({ type: 'component', id: 'a-b', parent: 'a', children: ['a-b--1'], }); - expect(storedStoriesHash['a-b--1']).toMatchObject({ + expect(index['a-b--1']).toMatchObject({ type: 'story', id: 'a-b--1', parent: 'a-b', @@ -289,7 +307,7 @@ describe('stories API', () => { const store = createMockStore(); const fullAPI = Object.assign(new EventEmitter()); - const { api } = initStories({ store, navigate, provider, fullAPI } as any); + const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); provider.getConfig.mockReturnValue({ sidebar: { showRoots: true } }); @@ -306,16 +324,16 @@ describe('stories API', () => { }, }); - const { storiesHash: storedStoriesHash } = store.getState(); + const { index } = store.getState(); // We need exact key ordering, even if in theory JS doens't guarantee it - expect(Object.keys(storedStoriesHash)).toEqual(['a', 'a--1']); - expect(storedStoriesHash.a).toMatchObject({ + expect(Object.keys(index)).toEqual(['a', 'a--1']); + expect(index.a).toMatchObject({ type: 'component', id: 'a', children: ['a--1'], }); - expect(storedStoriesHash['a--1']).toMatchObject({ + expect(index['a--1']).toMatchObject({ type: 'story', id: 'a--1', parent: 'a', @@ -331,7 +349,7 @@ describe('stories API', () => { const store = createMockStore(); const fullAPI = Object.assign(new EventEmitter()); - const { api } = initStories({ store, navigate, provider, fullAPI } as any); + const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); provider.getConfig.mockReturnValue({ sidebar: { showRoots: true } }); @@ -344,17 +362,17 @@ describe('stories API', () => { }, }); - const { storiesHash: storedStoriesHash } = store.getState(); + const { index } = store.getState(); // We need exact key ordering, even if in theory JS doens't guarantee it - expect(Object.keys(storedStoriesHash)).toEqual(['a', 'a--1', 'a--2', 'b', 'b--1']); - expect(storedStoriesHash.a).toMatchObject({ + expect(Object.keys(index)).toEqual(['a', 'a--1', 'a--2', 'b', 'b--1']); + expect(index.a).toMatchObject({ type: 'component', id: 'a', children: ['a--1', 'a--2'], }); - expect(storedStoriesHash.b).toMatchObject({ + expect(index.b).toMatchObject({ type: 'component', id: 'b', children: ['b--1'], @@ -367,7 +385,7 @@ describe('stories API', () => { const store = createMockStore(); const fullAPI = Object.assign(new EventEmitter()); - const { api } = initStories({ store, navigate, provider, fullAPI } as any); + const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); api.setIndex({ @@ -385,9 +403,9 @@ describe('stories API', () => { }, }); - const { storiesHash: storedStoriesHash } = store.getState(); + const { index } = store.getState(); - expect(storedStoriesHash['prepared--story']).toMatchObject({ + expect(index['prepared--story']).toMatchObject({ type: 'story', id: 'prepared--story', parent: 'prepared', @@ -404,7 +422,7 @@ describe('stories API', () => { const store = createMockStore(); const fullAPI = Object.assign(new EventEmitter(), { setOptions: jest.fn() }); - const { api, init } = initStories({ store, navigate, provider, fullAPI } as any); + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); init(); @@ -417,7 +435,7 @@ describe('stories API', () => { }); // Let the promise/await chain resolve await new Promise((r) => setTimeout(r, 0)); - expect(store.getState().storiesHash['component-a--story-1'] as API_StoryEntry).toMatchObject({ + expect(store.getState().index['component-a--story-1'] as API_StoryEntry).toMatchObject({ prepared: true, parameters: { a: 'b' }, args: { c: 'd' }, @@ -427,7 +445,7 @@ describe('stories API', () => { // Let the promise/await chain resolve await new Promise((r) => setTimeout(r, 0)); - expect(store.getState().storiesHash['component-a--story-1'] as API_StoryEntry).toMatchObject({ + expect(store.getState().index['component-a--story-1'] as API_StoryEntry).toMatchObject({ prepared: true, parameters: { a: 'b' }, args: { c: 'd' }, @@ -473,15 +491,15 @@ describe('stories API', () => { const store = createMockStore(); const fullAPI = Object.assign(new EventEmitter()); - const { api } = initStories({ store, navigate, provider, fullAPI } as any); + const { api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); api.setIndex({ v: 4, entries: docsEntries }); - const { storiesHash: storedStoriesHash } = store.getState(); + const { index } = store.getState(); // We need exact key ordering, even if in theory JS doesn't guarantee it - expect(Object.keys(storedStoriesHash)).toEqual([ + expect(Object.keys(index)).toEqual([ 'component-a', 'component-a--page', 'component-a--story-2', @@ -490,10 +508,10 @@ describe('stories API', () => { 'component-c', 'component-c--story-4', ]); - expect(storedStoriesHash['component-a--page'].type).toBe('story'); - expect(storedStoriesHash['component-a--story-2'].type).toBe('story'); - expect(storedStoriesHash['component-b--docs'].type).toBe('docs'); - expect(storedStoriesHash['component-c--story-4'].type).toBe('story'); + expect(index['component-a--page'].type).toBe('story'); + expect(index['component-a--story-2'].type).toBe('story'); + expect(index['component-b--docs'].type).toBe('docs'); + expect(index['component-c--story-4'].type).toBe('story'); }); describe('when DOCS_MODE = true', () => { @@ -502,7 +520,7 @@ describe('stories API', () => { const store = createMockStore(); const fullAPI = Object.assign(new EventEmitter()); - const { api } = initStories({ + const { api } = initStoriesAndSetState({ store, navigate, provider, @@ -513,9 +531,9 @@ describe('stories API', () => { api.setIndex({ v: 4, entries: docsEntries }); - const { storiesHash: storedStoriesHash } = store.getState(); + const { index } = store.getState(); - expect(Object.keys(storedStoriesHash)).toEqual(['component-b', 'component-b--docs']); + expect(Object.keys(index)).toEqual(['component-b', 'component-b--docs']); }); }); }); @@ -527,7 +545,7 @@ describe('stories API', () => { const navigate = jest.fn(); const store = createMockStore(); - const { init, api } = initStories({ store, navigate, provider, fullAPI } as any); + const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api, { setIndex: jest.fn(), setOptions: jest.fn(), @@ -544,7 +562,7 @@ describe('stories API', () => { const navigate = jest.fn(); const store = createMockStore(); - const { init, api } = initStories({ store, navigate, provider, fullAPI } as any); + const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api, { setIndex: jest.fn(), setOptions: jest.fn(), @@ -570,14 +588,13 @@ describe('stories API', () => { text: async () => new Error('sorting error'), } as any as Response) ); - const { api, init } = initStories({ store, navigate, provider, fullAPI } as any); + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); await init(); - const { storiesConfigured, storiesFailed } = store.getState(); - expect(storiesConfigured).toBe(true); - expect(storiesFailed?.message).toMatch(/sorting error/); + const { indexError } = store.getState(); + expect(indexError).toBeDefined(); }); it('watches for the INVALIDATE event and refetches -- and resets the hash', async () => { @@ -587,7 +604,7 @@ describe('stories API', () => { setIndex: jest.fn(), }); - const { api, init } = initStories({ store, navigate, provider, fullAPI } as any); + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); (global.fetch as jest.Mock>).mockClear(); @@ -609,13 +626,12 @@ describe('stories API', () => { // Let the promise/await chain resolve await new Promise((r) => setTimeout(r, 0)); - const { storiesHash: storedStoriesHash } = store.getState(); + const { index } = store.getState(); - expect(Object.keys(storedStoriesHash)).toEqual(['component-a', 'component-a--story-1']); + expect(Object.keys(index)).toEqual(['component-a', 'component-a--story-1']); }); }); - // Can't currently run these tests as cannot set this on the events describe('STORY_SPECIFIED event', () => { it('navigates to the story', async () => { const navigate = jest.fn(); @@ -625,7 +641,7 @@ describe('stories API', () => { }, }); const store = createMockStore({}); - const { init, api } = initStories({ store, navigate, provider, fullAPI } as any); + const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); init(); @@ -642,7 +658,7 @@ describe('stories API', () => { }, }); const store = createMockStore({ viewMode: 'story', storyId: 'a--1' }); - initStories({ store, navigate, provider, fullAPI } as any); + initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); fullAPI.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' }); @@ -657,7 +673,7 @@ describe('stories API', () => { }, }); const store = createMockStore({ viewMode: 'settings', storyId: 'about' }); - initStories({ store, navigate, provider, fullAPI } as any); + initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); fullAPI.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' }); @@ -665,6 +681,43 @@ describe('stories API', () => { }); }); + describe('CURRENT_STORY_WAS_SET event', () => { + it('sets previewInitialized', async () => { + const navigate = jest.fn(); + const fullAPI = Object.assign(new EventEmitter()); + const store = createMockStore({}); + const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); + + Object.assign(fullAPI, api); + await init(); + fullAPI.emit(CURRENT_STORY_WAS_SET, { id: 'a--1' }); + + expect(store.getState().previewInitialized).toBe(true); + }); + + it('sets a ref to previewInitialized', async () => { + const navigate = jest.fn(); + const fullAPI = Object.assign(new EventEmitter(), { updateRef: jest.fn() }); + const store = createMockStore(); + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); + + Object.assign(fullAPI, api); + + getEventMetadataMock.mockReturnValueOnce({ + sourceType: 'external', + ref: { id: 'refId', index: { 'a--1': { args: { a: 'b' } } } }, + } as any); + await init(); + fullAPI.emit(CURRENT_STORY_WAS_SET, { id: 'a--1' }); + + expect(fullAPI.updateRef.mock.calls.length).toBe(1); + + expect(fullAPI.updateRef.mock.calls[0][1]).toEqual({ + previewInitialized: true, + }); + }); + }); + describe('args handling', () => { const parameters = {}; const preparedEntries: API_PreparedStoryIndex['entries'] = { @@ -693,21 +746,21 @@ describe('stories API', () => { const store = createMockStore(); const fullAPI = new EventEmitter(); - const { api, init } = initStories({ store, navigate, provider, fullAPI } as any); + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); const { setIndex } = Object.assign(fullAPI, api); setIndex({ v: 4, entries: preparedEntries }); - const { storiesHash: initialStoriesHash } = store.getState(); - expect((initialStoriesHash['a--1'] as API_StoryEntry).args).toEqual({ a: 'b' }); - expect((initialStoriesHash['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); + const { index } = store.getState(); + expect((index['a--1'] as API_StoryEntry).args).toEqual({ a: 'b' }); + expect((index['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); init(); fullAPI.emit(STORY_ARGS_UPDATED, { storyId: 'a--1', args: { foo: 'bar' } }); - const { storiesHash: changedStoriesHash } = store.getState(); - expect((changedStoriesHash['a--1'] as API_StoryEntry).args).toEqual({ foo: 'bar' }); - expect((changedStoriesHash['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); + const { index: changedIndex } = store.getState(); + expect((changedIndex['a--1'] as API_StoryEntry).args).toEqual({ foo: 'bar' }); + expect((changedIndex['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); }); it('changes reffed args properly, per story when receiving STORY_ARGS_UPDATED', () => { @@ -715,7 +768,7 @@ describe('stories API', () => { const store = createMockStore(); const fullAPI = new EventEmitter(); - const { init, api } = initStories({ store, navigate, provider, fullAPI } as any); + const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api, { updateRef: jest.fn(), }); @@ -723,11 +776,11 @@ describe('stories API', () => { init(); getEventMetadataMock.mockReturnValueOnce({ sourceType: 'external', - ref: { id: 'refId', stories: { 'a--1': { args: { a: 'b' } } } }, + ref: { id: 'refId', index: { 'a--1': { args: { a: 'b' } } } }, } as any); fullAPI.emit(STORY_ARGS_UPDATED, { storyId: 'a--1', args: { foo: 'bar' } }); expect((fullAPI as any).updateRef).toHaveBeenCalledWith('refId', { - stories: { 'a--1': { args: { foo: 'bar' } } }, + index: { 'a--1': { args: { foo: 'bar' } } }, }); }); @@ -738,7 +791,7 @@ describe('stories API', () => { const fullAPI = { emit, on }; const store = createMockStore(); - const { api, init } = initStories({ store, navigate, provider, fullAPI } as any); + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); const { setIndex } = Object.assign(fullAPI, api); setIndex({ v: 4, entries: preparedEntries }); @@ -753,9 +806,9 @@ describe('stories API', () => { }, }); - const { storiesHash: changedStoriesHash } = store.getState(); - expect((changedStoriesHash['a--1'] as API_StoryEntry).args).toEqual({ a: 'b' }); - expect((changedStoriesHash['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); + const { index } = store.getState(); + expect((index['a--1'] as API_StoryEntry).args).toEqual({ a: 'b' }); + expect((index['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); }); it('updateStoryArgs emits UPDATE_STORY_ARGS to the right frame', () => { @@ -765,7 +818,7 @@ describe('stories API', () => { const fullAPI = { emit, on }; const store = createMockStore(); - const { api, init } = initStories({ store, navigate, provider, fullAPI } as any); + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); const { setIndex } = Object.assign(fullAPI, api); setIndex({ v: 4, entries: preparedEntries }); @@ -789,7 +842,7 @@ describe('stories API', () => { const fullAPI = { emit, on }; const store = createMockStore(); - const { api, init } = initStories({ store, navigate, provider, fullAPI } as any); + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); const { setIndex } = Object.assign(fullAPI, api); setIndex({ v: 4, entries: preparedEntries }); @@ -804,9 +857,9 @@ describe('stories API', () => { }, }); - const { storiesHash: changedStoriesHash } = store.getState(); - expect((changedStoriesHash['a--1'] as API_StoryEntry).args).toEqual({ a: 'b' }); - expect((changedStoriesHash['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); + const { index } = store.getState(); + expect((index['a--1'] as API_StoryEntry).args).toEqual({ a: 'b' }); + expect((index['b--1'] as API_StoryEntry).args).toEqual({ x: 'y' }); }); it('resetStoryArgs emits RESET_STORY_ARGS to the right frame', () => { @@ -816,7 +869,7 @@ describe('stories API', () => { const fullAPI = { emit, on }; const store = createMockStore(); - const { api, init } = initStories({ store, navigate, provider, fullAPI } as any); + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); const { setIndex } = Object.assign(fullAPI, api); setIndex({ v: 4, entries: preparedEntries }); @@ -881,11 +934,17 @@ describe('stories API', () => { describe('jumpToStory', () => { it('works forward', () => { const navigate = jest.fn(); - const store = createMockStore({ storyId: 'a--1', viewMode: 'story' }); + const store = createMockStore(); const { api: { setIndex, jumpToStory }, - } = initStories({ store, navigate, provider } as any); + } = initStoriesAndSetState({ + store, + storyId: 'a--1', + viewMode: 'story', + navigate, + provider, + } as any); setIndex({ v: 4, entries: navigationEntries }); jumpToStory(1); @@ -894,11 +953,17 @@ describe('stories API', () => { it('works backwards', () => { const navigate = jest.fn(); - const store = createMockStore({ storyId: 'a--2', viewMode: 'story' }); + const store = createMockStore(); const { api: { setIndex, jumpToStory }, - } = initStories({ store, navigate, provider } as any); + } = initStoriesAndSetState({ + store, + storyId: 'a--2', + viewMode: 'story', + navigate, + provider, + } as any); setIndex({ v: 4, entries: navigationEntries }); jumpToStory(-1); @@ -907,15 +972,14 @@ describe('stories API', () => { it('does nothing if you are at the last story and go forward', () => { const navigate = jest.fn(); - const store = createMockStore({ - storyId: 'custom-id--1', - viewMode: 'story', - }); + const store = createMockStore(); const { api: { setIndex, jumpToStory }, - } = initStories({ + } = initStoriesAndSetState({ store, + storyId: 'custom-id--1', + viewMode: 'story', navigate, provider, } as any); @@ -927,11 +991,17 @@ describe('stories API', () => { it('does nothing if you are at the first story and go backward', () => { const navigate = jest.fn(); - const store = createMockStore({ storyId: 'a--1', viewMode: 'story' }); + const store = createMockStore(); const { api: { setIndex, jumpToStory }, - } = initStories({ store, navigate, provider } as any); + } = initStoriesAndSetState({ + store, + storyId: 'a--1', + viewMode: 'story', + navigate, + provider, + } as any); setIndex({ v: 4, entries: navigationEntries }); jumpToStory(-1); @@ -944,7 +1014,7 @@ describe('stories API', () => { const { api: { setIndex, jumpToStory }, - } = initStories({ store, navigate, provider } as any); + } = initStoriesAndSetState({ store, navigate, provider } as any); setIndex({ v: 4, entries: navigationEntries }); jumpToStory(1); @@ -960,12 +1030,10 @@ describe('stories API', () => { const storyId = 'a--1'; const { api: { setIndex, findSiblingStoryId }, - state, - } = initStories({ store, navigate, storyId, viewMode: 'story', provider } as any); - store.setState(state); + } = initStoriesAndSetState({ store, navigate, storyId, viewMode: 'story', provider } as any); setIndex({ v: 4, entries: navigationEntries }); - const result = findSiblingStoryId(storyId, store.getState().storiesHash, 1, false); + const result = findSiblingStoryId(storyId, store.getState().index, 1, false); expect(result).toBe('a--2'); }); it('works forward toSiblingGroup', () => { @@ -975,12 +1043,10 @@ describe('stories API', () => { const storyId = 'a--1'; const { api: { setIndex, findSiblingStoryId }, - state, - } = initStories({ store, navigate, storyId, viewMode: 'story', provider } as any); - store.setState(state); + } = initStoriesAndSetState({ store, navigate, storyId, viewMode: 'story', provider } as any); setIndex({ v: 4, entries: navigationEntries }); - const result = findSiblingStoryId(storyId, store.getState().storiesHash, 1, true); + const result = findSiblingStoryId(storyId, store.getState().index, 1, true); expect(result).toBe('b-c--1'); }); }); @@ -991,9 +1057,13 @@ describe('stories API', () => { const { api: { setIndex, jumpToComponent }, - state, - } = initStories({ store, navigate, storyId: 'a--1', viewMode: 'story', provider } as any); - store.setState(state); + } = initStoriesAndSetState({ + store, + navigate, + storyId: 'a--1', + viewMode: 'story', + provider, + } as any); setIndex({ v: 4, entries: navigationEntries }); jumpToComponent(1); @@ -1006,9 +1076,13 @@ describe('stories API', () => { const { api: { setIndex, jumpToComponent }, - state, - } = initStories({ store, navigate, storyId: 'b-c--1', viewMode: 'story', provider } as any); - store.setState(state); + } = initStoriesAndSetState({ + store, + navigate, + storyId: 'b-c--1', + viewMode: 'story', + provider, + } as any); setIndex({ v: 4, entries: navigationEntries }); jumpToComponent(-1); @@ -1021,15 +1095,13 @@ describe('stories API', () => { const { api: { setIndex, jumpToComponent }, - state, - } = initStories({ + } = initStoriesAndSetState({ store, navigate, storyId: 'custom-id--1', viewMode: 'story', provider, } as any); - store.setState(state); setIndex({ v: 4, entries: navigationEntries }); jumpToComponent(1); @@ -1042,9 +1114,13 @@ describe('stories API', () => { const { api: { setIndex, jumpToComponent }, - state, - } = initStories({ store, navigate, storyId: 'a--2', viewMode: 'story', provider } as any); - store.setState(state); + } = initStoriesAndSetState({ + store, + navigate, + storyId: 'a--2', + viewMode: 'story', + provider, + } as any); setIndex({ v: 4, entries: navigationEntries }); jumpToComponent(-1); @@ -1058,7 +1134,7 @@ describe('stories API', () => { const store = createMockStore({ storyId: 'a--1', viewMode: 'story' }); const { api: { setIndex, selectStory }, - } = initStories({ store, navigate, provider } as any); + } = initStoriesAndSetState({ store, navigate, provider } as any); setIndex({ v: 4, entries: navigationEntries }); selectStory('a--2'); @@ -1070,7 +1146,7 @@ describe('stories API', () => { const store = createMockStore({ storyId: 'a--1', viewMode: 'docs' }); const { api: { setIndex, selectStory }, - } = initStories({ store, navigate, provider } as any); + } = initStoriesAndSetState({ store, navigate, provider } as any); setIndex({ v: 4, entries: { @@ -1093,10 +1169,16 @@ describe('stories API', () => { describe('legacy api', () => { it('allows navigating to a combination of title + name', () => { const navigate = jest.fn(); - const store = createMockStore({ storyId: 'a--1', viewMode: 'story' }); + const store = createMockStore(); const { api: { setIndex, selectStory }, - } = initStories({ store, navigate, provider } as any); + } = initStoriesAndSetState({ + store, + storyId: 'a--1', + viewMode: 'story', + navigate, + provider, + } as any); setIndex({ v: 4, entries: navigationEntries }); selectStory('a', '2'); @@ -1105,10 +1187,16 @@ describe('stories API', () => { it('allows navigating to a given name (in the current component)', () => { const navigate = jest.fn(); - const store = createMockStore({ storyId: 'a--1', viewMode: 'story' }); + const store = createMockStore(); const { api: { setIndex, selectStory }, - } = initStories({ store, navigate, provider } as any); + } = initStoriesAndSetState({ + store, + storyId: 'a--1', + viewMode: 'story', + navigate, + provider, + } as any); setIndex({ v: 4, entries: navigationEntries }); selectStory(undefined, '2'); @@ -1121,7 +1209,7 @@ describe('stories API', () => { const store = createMockStore({ storyId: 'a--1', viewMode: 'settings' }); const { api: { setIndex, selectStory }, - } = initStories({ store, navigate, provider } as any); + } = initStoriesAndSetState({ store, navigate, provider } as any); setIndex({ v: 4, entries: navigationEntries }); selectStory('a--2'); @@ -1130,10 +1218,16 @@ describe('stories API', () => { it('allows navigating to first story in component on call by component id', () => { const navigate = jest.fn(); - const store = createMockStore({ storyId: 'a--1', viewMode: 'story' }); + const store = createMockStore(); const { api: { setIndex, selectStory }, - } = initStories({ store, navigate, provider } as any); + } = initStoriesAndSetState({ + store, + storyId: 'a--1', + viewMode: 'story', + navigate, + provider, + } as any); setIndex({ v: 4, entries: navigationEntries }); selectStory('a'); @@ -1142,10 +1236,16 @@ describe('stories API', () => { it('allows navigating to first story in group on call by group id', () => { const navigate = jest.fn(); - const store = createMockStore({ storyId: 'a--1', viewMode: 'story' }); + const store = createMockStore(); const { api: { setIndex, selectStory }, - } = initStories({ store, navigate, provider } as any); + } = initStoriesAndSetState({ + store, + storyId: 'a--1', + viewMode: 'story', + navigate, + provider, + } as any); setIndex({ v: 4, entries: navigationEntries }); selectStory('b'); @@ -1154,10 +1254,16 @@ describe('stories API', () => { it('allows navigating to first story in component on call by title', () => { const navigate = jest.fn(); - const store = createMockStore({ storyId: 'a--1', viewMode: 'story' }); + const store = createMockStore(); const { api: { setIndex, selectStory }, - } = initStories({ store, navigate, provider } as any); + } = initStoriesAndSetState({ + store, + storyId: 'a--1', + viewMode: 'story', + navigate, + provider, + } as any); setIndex({ v: 4, entries: navigationEntries }); selectStory('A'); @@ -1166,10 +1272,16 @@ describe('stories API', () => { it('allows navigating to the first story of the current component if passed nothing', () => { const navigate = jest.fn(); - const store = createMockStore({ storyId: 'a--2', viewMode: 'story' }); + const store = createMockStore(); const { api: { setIndex, selectStory }, - } = initStories({ store, navigate, provider } as any); + } = initStoriesAndSetState({ + store, + storyId: 'a--2', + viewMode: 'story', + navigate, + provider, + } as any); setIndex({ v: 4, entries: navigationEntries }); selectStory(); @@ -1183,9 +1295,7 @@ describe('stories API', () => { const { api: { selectStory, setIndex }, - state, - } = initStories({ store, navigate, provider } as any); - store.setState(state); + } = initStoriesAndSetState({ store, navigate, provider } as any); setIndex({ v: 4, entries: navigationEntries }); selectStory('b/e', '1'); @@ -1198,9 +1308,7 @@ describe('stories API', () => { const { api: { selectStory, setIndex }, - state, - } = initStories({ store, navigate, provider } as any); - store.setState(state); + } = initStoriesAndSetState({ store, navigate, provider } as any); setIndex({ v: 4, entries: navigationEntries }); selectStory('custom-id', '1'); @@ -1213,9 +1321,7 @@ describe('stories API', () => { const { api: { selectStory, setIndex }, - state, - } = initStories({ store, navigate, provider } as any); - store.setState(state); + } = initStoriesAndSetState({ store, navigate, provider } as any); setIndex({ v: 4, entries: navigationEntries }); selectStory('b/e'); @@ -1233,7 +1339,7 @@ describe('stories API', () => { setOptions: jest.fn(), }); - const { api, init } = initStories({ store, navigate, provider, fullAPI } as any); + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); await init(); @@ -1243,8 +1349,8 @@ describe('stories API', () => { args: { c: 'd' }, }); - const { storiesHash: storedStoriesHash } = store.getState(); - expect(storedStoriesHash['component-a--story-1']).toMatchObject({ + const { index } = store.getState(); + expect(index['component-a--story-1']).toMatchObject({ type: 'story', id: 'component-a--story-1', parent: 'component-a', @@ -1264,7 +1370,7 @@ describe('stories API', () => { setOptions: jest.fn(), }); - const { api, init } = initStories({ store, navigate, provider, fullAPI } as any); + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); await init(); @@ -1283,16 +1389,31 @@ describe('stories API', () => { expect(fullAPI.setOptions).not.toHaveBeenCalled(); }); + }); - it('sets the ref to ready when it is an external story', async () => { + describe('CONFIG_ERROR', () => { + it('sets previewInitialized to true, local', async () => { const navigate = jest.fn(); const store = createMockStore(); - const fullAPI = Object.assign(new EventEmitter(), { - setStories: jest.fn(), - updateRef: jest.fn(), - }); + const fullAPI = Object.assign(new EventEmitter(), {}); + + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); + Object.assign(fullAPI, api); + + await init(); + + fullAPI.emit(CONFIG_ERROR, { message: 'Failed to run configure' }); + + const { previewInitialized } = store.getState(); + expect(previewInitialized).toBe(true); + }); + + it('sets previewInitialized to true, ref', async () => { + const navigate = jest.fn(); + const fullAPI = Object.assign(new EventEmitter(), { updateRef: jest.fn() }); + const store = createMockStore(); + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); - const { api, init } = initStories({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); getEventMetadataMock.mockReturnValueOnce({ @@ -1300,39 +1421,51 @@ describe('stories API', () => { ref: { id: 'refId', stories: { 'a--1': { args: { a: 'b' } } } }, } as any); await init(); + fullAPI.emit(CONFIG_ERROR, { message: 'Failed to run configure' }); - fullAPI.emit(STORY_PREPARED, { - id: 'a--1', - }); - - expect(fullAPI.updateRef.mock.calls.length).toBe(2); - + expect(fullAPI.updateRef.mock.calls.length).toBe(1); expect(fullAPI.updateRef.mock.calls[0][1]).toEqual({ - stories: { 'a--1': { args: { a: 'b' }, prepared: true } }, - }); - - expect(fullAPI.updateRef.mock.calls[1][1]).toEqual({ - ready: true, + previewInitialized: true, }); }); }); - describe('CONFIG_ERROR', () => { - it('shows error to user', async () => { + describe('STORY_MISSING', () => { + it('sets previewInitialized to true, local', async () => { const navigate = jest.fn(); const store = createMockStore(); const fullAPI = Object.assign(new EventEmitter(), {}); - const { api, init } = initStories({ store, navigate, provider, fullAPI } as any); + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api); await init(); - fullAPI.emit(CONFIG_ERROR, { message: 'Failed to run configure' }); + fullAPI.emit(STORY_MISSING, { message: 'Failed to run configure' }); + + const { previewInitialized } = store.getState(); + expect(previewInitialized).toBe(true); + }); + + it('sets previewInitialized to true, ref', async () => { + const navigate = jest.fn(); + const fullAPI = Object.assign(new EventEmitter(), { updateRef: jest.fn() }); + const store = createMockStore(); + const { api, init } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); + + Object.assign(fullAPI, api); - const { storiesConfigured, storiesFailed } = store.getState(); - expect(storiesConfigured).toBe(true); - expect(storiesFailed?.message).toMatch(/Failed to run configure/); + getEventMetadataMock.mockReturnValueOnce({ + sourceType: 'external', + ref: { id: 'refId', stories: { 'a--1': { args: { a: 'b' } } } }, + } as any); + await init(); + fullAPI.emit(STORY_MISSING, { message: 'Failed to run configure' }); + + expect(fullAPI.updateRef.mock.calls.length).toBe(1); + expect(fullAPI.updateRef.mock.calls[0][1]).toEqual({ + previewInitialized: true, + }); }); }); @@ -1342,7 +1475,7 @@ describe('stories API', () => { const navigate = jest.fn(); const store = createMockStore(); - const { init, api } = initStories({ store, navigate, provider, fullAPI } as any); + const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api, { setIndex: jest.fn(), findRef: jest.fn(), @@ -1381,7 +1514,7 @@ describe('stories API', () => { const navigate = jest.fn(); const store = createMockStore(); - const { init, api } = initStories({ store, navigate, provider, fullAPI } as any); + const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any); Object.assign(fullAPI, api, { setIndex: jest.fn(), findRef: jest.fn(), 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 9492003f38f3..c6c861087631 100644 --- a/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx +++ b/code/lib/preview-api/src/modules/preview-web/PreviewWithSelection.tsx @@ -277,7 +277,7 @@ export class PreviewWithSelection extends Preview { showNoPreview(): void; - showPreparingStory(options: { immediate: boolean }): void; + showPreparingStory(options?: { immediate: boolean }): void; - showPreparingDocs(): void; + showPreparingDocs(options?: { immediate: boolean }): void; showMain(): void; diff --git a/code/lib/preview-api/src/modules/preview-web/WebView.ts b/code/lib/preview-api/src/modules/preview-web/WebView.ts index 05483fe4ac8c..fa6a823bb0ba 100644 --- a/code/lib/preview-api/src/modules/preview-web/WebView.ts +++ b/code/lib/preview-api/src/modules/preview-web/WebView.ts @@ -165,9 +165,13 @@ export class WebView implements View { } } - showPreparingDocs() { + showPreparingDocs({ immediate = false } = {}) { clearTimeout(this.preparingTimeout); - this.preparingTimeout = setTimeout(() => this.showMode(Mode.PREPARING_DOCS), PREPARING_DELAY); + if (immediate) { + this.showMode(Mode.PREPARING_DOCS); + } else { + this.preparingTimeout = setTimeout(() => this.showMode(Mode.PREPARING_DOCS), PREPARING_DELAY); + } } showMain() { diff --git a/code/lib/types/src/modules/api-stories.ts b/code/lib/types/src/modules/api-stories.ts index b571e9faa00c..26999b6f22a6 100644 --- a/code/lib/types/src/modules/api-stories.ts +++ b/code/lib/types/src/modules/api-stories.ts @@ -115,12 +115,12 @@ export type API_Group = API_GroupEntry | API_ComponentEntry; export type API_Story = API_LeafEntry; /** - * The `StoriesHash` is our manager-side representation of the `StoryIndex`. + * The `IndexHash` is our manager-side representation of the `StoryIndex`. * We create entries in the hash not only for each story or docs entry, but * also for each "group" of the component (split on '/'), as that's how things * are manipulated in the manager (i.e. in the sidebar) */ -export interface API_StoriesHash { +export interface API_IndexHash { [id: string]: API_HashEntry; } // We used to received a bit more data over the channel on the SET_STORIES event, including diff --git a/code/lib/types/src/modules/api.ts b/code/lib/types/src/modules/api.ts index 11a0d74793f7..d9d2c54ec5de 100644 --- a/code/lib/types/src/modules/api.ts +++ b/code/lib/types/src/modules/api.ts @@ -5,7 +5,7 @@ import type { Channel } from '../../../channels/src'; import type { ThemeVars } from '../../../theming/src/types'; import type { ViewMode } from './csf'; import type { DocsOptions } from './core-common'; -import type { API_HashEntry, API_StoriesHash } from './api-stories'; +import type { API_HashEntry, API_IndexHash } from './api-stories'; import type { SetStoriesStory, SetStoriesStoryData } from './channelApi'; import type { Addon_Types } from './addons'; import type { StoryIndex } from './storyIndex'; @@ -142,18 +142,22 @@ export type API_SetRefData = Partial< >; export type API_StoryMapper = (ref: API_ComposedRef, story: SetStoriesStory) => SetStoriesStory; -export interface API_ComposedRef { + +export interface API_LoadedRefData { + index?: API_IndexHash; + indexError?: Error; + previewInitialized: boolean; +} + +export interface API_ComposedRef extends API_LoadedRefData { id: string; title?: string; url: string; type?: 'auto-inject' | 'unknown' | 'lazy' | 'server-checked'; expanded?: boolean; - stories: API_StoriesHash; versions?: API_Versions; loginUrl?: string; version?: string; - ready?: boolean; - error?: any; } export type API_ComposedRefUpdate = Partial< @@ -162,12 +166,12 @@ export type API_ComposedRefUpdate = Partial< | 'title' | 'type' | 'expanded' - | 'stories' + | 'index' | 'versions' | 'loginUrl' | 'version' - | 'ready' - | 'error' + | 'indexError' + | 'previewInitialized' > >; diff --git a/code/ui/.storybook/main.ts b/code/ui/.storybook/main.ts index ca2f9534cf72..6709b1bf460b 100644 --- a/code/ui/.storybook/main.ts +++ b/code/ui/.storybook/main.ts @@ -80,6 +80,7 @@ const config: StorybookConfig = { sourcemap: process.env.CI !== 'true', }, }), + logLevel: 'debug', }; export default config; diff --git a/code/ui/components/src/Colors/colorpalette.stories.mdx b/code/ui/components/src/Colors/colorpalette.mdx similarity index 100% rename from code/ui/components/src/Colors/colorpalette.stories.mdx rename to code/ui/components/src/Colors/colorpalette.mdx diff --git a/code/ui/manager/src/app.stories.tsx b/code/ui/manager/src/app.stories.tsx index f55af04a6560..c213f64faabe 100644 --- a/code/ui/manager/src/app.stories.tsx +++ b/code/ui/manager/src/app.stories.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { Provider as ManagerProvider } from '@storybook/manager-api'; +import type { API } from '@storybook/manager-api'; +import { Consumer, Provider as ManagerProvider } from '@storybook/manager-api'; import { LocationProvider } from '@storybook/router'; import { HelmetProvider } from 'react-helmet-async'; import { styled } from '@storybook/theming'; @@ -37,32 +38,41 @@ const ThemeStack = styled.div( }) ); -export const Default = () => ( - {}} - docsOptions={{ docsMode: false }} - > - - -); +function setPreviewInitialized({ api }: { api: API }) { + api.setPreviewInitialized(); + return {}; +} + +export const Default = () => { + const provider = new FakeProvider(); + return ( + {}} + docsOptions={{ docsMode: false }} + > + {() => <>} + + + ); +}; export const LoadingState = () => ( ({ diff --git a/code/ui/manager/src/components/preview/FramesRenderer.tsx b/code/ui/manager/src/components/preview/FramesRenderer.tsx index bf2ff9b916d8..be4d83c6e376 100644 --- a/code/ui/manager/src/components/preview/FramesRenderer.tsx +++ b/code/ui/manager/src/components/preview/FramesRenderer.tsx @@ -80,7 +80,7 @@ export const FramesRenderer: FC = ({ useEffect(() => { const newFrames = Object.values(refs) .filter((r) => { - if (r.error) { + if (r.indexError) { return false; } if (r.type === 'auto-inject') { diff --git a/code/ui/manager/src/components/preview/preview.tsx b/code/ui/manager/src/components/preview/preview.tsx index e9d5a21ddf68..9d01ed28eaf5 100644 --- a/code/ui/manager/src/components/preview/preview.tsx +++ b/code/ui/manager/src/components/preview/preview.tsx @@ -37,8 +37,7 @@ const canvasMapper = ({ state, api }: Combo) => ({ queryParams: state.customQueryParams, getElements: api.getElements, entry: api.getData(state.storyId, state.refId), - storiesConfigured: state.storiesConfigured, - storiesFailed: state.storiesFailed, + previewInitialized: state.previewInitialized, refs: state.refs, active: !!(state.viewMode && state.viewMode.match(/^(story|docs)$/)), }); @@ -60,8 +59,7 @@ const createCanvas = (id: string, baseUrl = 'iframe.html', withLoader = true): A viewMode, queryParams, getElements, - storiesConfigured, - storiesFailed, + previewInitialized, active, }) => { const wrappers = useMemo( @@ -70,7 +68,6 @@ const createCanvas = (id: string, baseUrl = 'iframe.html', withLoader = true): A ); const [progress, setProgress] = useState(undefined); - useEffect(() => { if (FEATURES?.storyStoreV7 && global.CONFIG_TYPE === 'DEVELOPMENT') { try { @@ -84,12 +81,12 @@ const createCanvas = (id: string, baseUrl = 'iframe.html', withLoader = true): A } } }, []); - - const refLoading = !!refs[refId] && !refs[refId].ready; - const rootLoading = !refId && !(progress?.value === 1 || progress === undefined); - const isLoading = entry - ? refLoading || rootLoading - : (!storiesFailed && !storiesConfigured) || rootLoading; + // A ref simply depends on its readiness + const refLoading = !!refs[refId] && !refs[refId].previewInitialized; + // The root also might need to wait on webpack + const isBuilding = !(progress?.value === 1 || progress === undefined); + const rootLoading = !refId && (!previewInitialized || isBuilding); + const isLoading = entry ? refLoading || rootLoading : rootLoading; return ( diff --git a/code/ui/manager/src/components/sidebar/Explorer.stories.tsx b/code/ui/manager/src/components/sidebar/Explorer.stories.tsx index f8ac82068824..6e33e63b4ad8 100644 --- a/code/ui/manager/src/components/sidebar/Explorer.stories.tsx +++ b/code/ui/manager/src/components/sidebar/Explorer.stories.tsx @@ -25,9 +25,9 @@ const simple: Record = { title: undefined, id: 'storybook_internal', url: 'iframe.html', - ready: true, + previewInitialized: true, // @ts-expect-error (invalid input) - stories: mockDataset.withRoot, + index: mockDataset.withRoot, }, }; @@ -37,37 +37,37 @@ const withRefs: Record = { id: 'basic', title: 'Basic ref', url: 'https://example.com', - ready: true, + previewInitialized: true, type: 'auto-inject', // @ts-expect-error (invalid input) - stories: mockDataset.noRoot, + index: mockDataset.noRoot, }, injected: { id: 'injected', title: 'Not ready', url: 'https://example.com', - ready: false, + previewInitialized: false, type: 'auto-inject', // @ts-expect-error (invalid input) - stories: mockDataset.noRoot, + index: mockDataset.noRoot, }, unknown: { id: 'unknown', title: 'Unknown ref', url: 'https://example.com', - ready: true, + previewInitialized: true, type: 'unknown', // @ts-expect-error (invalid input) - stories: mockDataset.noRoot, + index: mockDataset.noRoot, }, lazy: { id: 'lazy', title: 'Lazy loaded ref', url: 'https://example.com', - ready: false, + previewInitialized: false, type: 'lazy', // @ts-expect-error (invalid input) - stories: mockDataset.withRoot, + index: mockDataset.withRoot, }, }; diff --git a/code/ui/manager/src/components/sidebar/RefIndicator.tsx b/code/ui/manager/src/components/sidebar/RefIndicator.tsx index fad585fabd4c..0d116a63f5c0 100644 --- a/code/ui/manager/src/components/sidebar/RefIndicator.tsx +++ b/code/ui/manager/src/components/sidebar/RefIndicator.tsx @@ -168,7 +168,7 @@ export const RefIndicator = React.memo( forwardRef }>( ({ state, ...ref }, forwardedRef) => { const api = useStorybookApi(); - const list = useMemo(() => Object.values(ref.stories || {}), [ref.stories]); + const list = useMemo(() => Object.values(ref.index || {}), [ref.index]); const componentCount = useMemo( () => list.filter((v) => v.type === 'component').length, [list] diff --git a/code/ui/manager/src/components/sidebar/Refs.stories.tsx b/code/ui/manager/src/components/sidebar/Refs.stories.tsx index d50812586301..f765f62daff0 100644 --- a/code/ui/manager/src/components/sidebar/Refs.stories.tsx +++ b/code/ui/manager/src/components/sidebar/Refs.stories.tsx @@ -22,13 +22,13 @@ export default { }; const { menu } = standardHeaderData; -const stories = mockDataset.withRoot; +const index = mockDataset.withRoot; const storyId = '1-12-121'; -export const simpleData = { menu, stories, storyId }; -export const loadingData = { menu, stories: {} }; +export const simpleData = { menu, index, storyId }; +export const loadingData = { menu, index: {} }; -const error: Error = (() => { +const indexError: Error = (() => { try { throw new Error('There was a severe problem'); } catch (e) { @@ -41,45 +41,45 @@ const refs: Record = { id: 'optimized', title: 'It is optimized', url: 'https://example.com', - ready: false, + previewInitialized: false, type: 'lazy', // @ts-expect-error (invalid input) - stories, + index, }, empty: { id: 'empty', title: 'It is empty because no stories were loaded', url: 'https://example.com', - ready: false, type: 'lazy', - stories: {}, + index: {}, + previewInitialized: false, }, startInjected_unknown: { id: 'startInjected_unknown', title: 'It started injected and is unknown', url: 'https://example.com', type: 'unknown', - ready: false, + previewInitialized: false, // @ts-expect-error (invalid input) - stories, + index, }, startInjected_loading: { id: 'startInjected_loading', title: 'It started injected and is loading', url: 'https://example.com', type: 'auto-inject', - ready: false, + previewInitialized: false, // @ts-expect-error (invalid input) - stories, + index, }, startInjected_ready: { id: 'startInjected_ready', title: 'It started injected and is ready', url: 'https://example.com', type: 'auto-inject', - ready: true, + previewInitialized: true, // @ts-expect-error (invalid input) - stories, + index, }, versions: { id: 'versions', @@ -87,8 +87,9 @@ const refs: Record = { url: 'https://example.com', type: 'lazy', // @ts-expect-error (invalid input) - stories, + index, versions: { '1.0.0': 'https://example.com/v1', '2.0.0': 'https://example.com' }, + previewInitialized: true, }, versionsMissingCurrent: { id: 'versions_missing_current', @@ -96,36 +97,38 @@ const refs: Record = { url: 'https://example.com', type: 'lazy', // @ts-expect-error (invalid input) - stories, + index, versions: { '1.0.0': 'https://example.com/v1', '2.0.0': 'https://example.com/v2' }, + previewInitialized: true, }, error: { id: 'error', title: 'This has problems', url: 'https://example.com', type: 'lazy', - stories: {}, - error, + indexError, + previewInitialized: true, }, auth: { id: 'Authentication', title: 'This requires a login', url: 'https://example.com', type: 'lazy', - stories: {}, loginUrl: 'https://example.com', + previewInitialized: true, }, long: { id: 'long', title: 'This storybook has a very very long name for some reason', url: 'https://example.com', // @ts-expect-error (invalid input) - stories, + index, type: 'lazy', versions: { '111.111.888-new': 'https://example.com/new', '111.111.888': 'https://example.com', }, + previewInitialized: true, }, }; diff --git a/code/ui/manager/src/components/sidebar/Refs.tsx b/code/ui/manager/src/components/sidebar/Refs.tsx index 793244517fb9..29599dd393a2 100644 --- a/code/ui/manager/src/components/sidebar/Refs.tsx +++ b/code/ui/manager/src/components/sidebar/Refs.tsx @@ -99,7 +99,7 @@ export const Ref: FC = React.memo(function Ref(props) { const { docsOptions } = useStorybookState(); const api = useStorybookApi(); const { - stories, + index, id: refId, title = refId, isLoading: isLoadingMain, @@ -110,16 +110,16 @@ export const Ref: FC = React.memo(function Ref(props) { loginUrl, type, expanded = true, - ready, - error, + indexError, + previewInitialized, } = props; - const length = useMemo(() => (stories ? Object.keys(stories).length : 0), [stories]); + const length = useMemo(() => (index ? Object.keys(index).length : 0), [index]); const indicatorRef = useRef(null); const isMain = refId === DEFAULT_REF_ID; - const isLoadingInjected = type === 'auto-inject' && !ready; + const isLoadingInjected = type === 'auto-inject' && !previewInitialized; const isLoading = isLoadingMain || isLoadingInjected || type === 'unknown'; - const isError = !!error; + const isError = !!indexError; const isEmpty = !isLoading && length === 0; const isAuthRequired = !!loginUrl && length === 0; @@ -153,7 +153,7 @@ export const Ref: FC = React.memo(function Ref(props) { {isExpanded && ( {state === 'auth' && } - {state === 'error' && } + {state === 'error' && } {state === 'loading' && } {state === 'empty' && } {state === 'ready' && ( @@ -161,7 +161,7 @@ export const Ref: FC = React.memo(function Ref(props) { isBrowsing={isBrowsing} isMain={isMain} refId={refId} - data={stories} + data={index} docsMode={docsOptions.docsMode} selectedStoryId={selectedStoryId} onSelectStoryId={onSelectStoryId} diff --git a/code/ui/manager/src/components/sidebar/Search.stories.tsx b/code/ui/manager/src/components/sidebar/Search.stories.tsx index f4cfda731a28..00f8ba61d3c5 100644 --- a/code/ui/manager/src/components/sidebar/Search.stories.tsx +++ b/code/ui/manager/src/components/sidebar/Search.stories.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { action } from '@storybook/addon-actions'; -import { stories } from './mockdata.large'; +import { index } from './mockdata.large'; import { Search } from './Search'; import { SearchResults } from './SearchResults'; import { noResults } from './SearchResults.stories'; @@ -9,11 +9,11 @@ import { DEFAULT_REF_ID } from './Sidebar'; import type { Selection } from './types'; const refId = DEFAULT_REF_ID; -const data = { [refId]: { id: refId, url: '/', stories } }; +const data = { [refId]: { id: refId, url: '/', index, previewInitialized: true } }; const dataset = { hash: data, entries: Object.entries(data) }; const getLastViewed = () => - Object.values(stories) - .filter((item, index) => item.type === 'component' && item.parent && index % 20 === 0) + Object.values(index) + .filter((item, i) => item.type === 'component' && item.parent && i % 20 === 0) .map((component) => ({ storyId: component.id, refId })); export default { diff --git a/code/ui/manager/src/components/sidebar/Search.tsx b/code/ui/manager/src/components/sidebar/Search.tsx index c8fe29fa3d2e..b485bd53039e 100644 --- a/code/ui/manager/src/components/sidebar/Search.tsx +++ b/code/ui/manager/src/components/sidebar/Search.tsx @@ -176,9 +176,9 @@ export const Search = React.memo<{ ); const list: SearchItem[] = useMemo(() => { - return dataset.entries.reduce((acc: SearchItem[], [refId, { stories }]) => { - if (stories) { - acc.push(...Object.values(stories).map((item) => searchItem(item, dataset.hash[refId]))); + return dataset.entries.reduce((acc: SearchItem[], [refId, { index }]) => { + if (index) { + acc.push(...Object.values(index).map((item) => searchItem(item, dataset.hash[refId]))); } return acc; }, []); @@ -314,9 +314,9 @@ export const Search = React.memo<{ if (lastViewed && lastViewed.length) { results = lastViewed.reduce((acc, { storyId, refId }) => { const data = dataset.hash[refId]; - if (data && data.stories && data.stories[storyId]) { - const story = data.stories[storyId]; - const item = story.type === 'story' ? data.stories[story.parent] : story; + if (data && data.index && data.index[storyId]) { + const story = data.index[storyId]; + const item = story.type === 'story' ? data.index[story.parent] : story; // prevent duplicates if (!acc.some((res) => res.item.refId === refId && res.item.id === item.id)) { acc.push({ item: searchItem(item, dataset.hash[refId]), matches: [], score: 0 }); diff --git a/code/ui/manager/src/components/sidebar/SearchResults.stories.tsx b/code/ui/manager/src/components/sidebar/SearchResults.stories.tsx index e0194299220f..d2c72e0c721f 100644 --- a/code/ui/manager/src/components/sidebar/SearchResults.stories.tsx +++ b/code/ui/manager/src/components/sidebar/SearchResults.stories.tsx @@ -18,10 +18,10 @@ export default { const combinedDataset = (refs: Record): CombinedDataset => { const hash: Refs = Object.entries(refs).reduce( - (acc, [refId, stories]) => + (acc, [refId, index]) => Object.assign(acc, { [refId]: { - stories, + index, title: null, id: refId, url: 'iframe.html', @@ -37,10 +37,10 @@ const combinedDataset = (refs: Record): CombinedDataset => // @ts-expect-error (invalid input) const dataset = combinedDataset({ internal: mockDataset.withRoot, composed: mockDataset.noRoot }); -const internal = Object.values(dataset.hash.internal.stories).map((item) => +const internal = Object.values(dataset.hash.internal.index).map((item) => searchItem(item, dataset.hash.internal) ); -const composed = Object.values(dataset.hash.composed.stories).map((item) => +const composed = Object.values(dataset.hash.composed.index).map((item) => searchItem(item, dataset.hash.composed) ); const stories: SearchItem[] = internal.concat(composed); diff --git a/code/ui/manager/src/components/sidebar/Sidebar.stories.tsx b/code/ui/manager/src/components/sidebar/Sidebar.stories.tsx index 2a1e76de0d70..936789d0cde0 100644 --- a/code/ui/manager/src/components/sidebar/Sidebar.stories.tsx +++ b/code/ui/manager/src/components/sidebar/Sidebar.stories.tsx @@ -1,5 +1,6 @@ import React from 'react'; +import type { IndexHash } from 'lib/manager-api/src'; import { Sidebar, DEFAULT_REF_ID } from './Sidebar'; import { standardData as standardHeaderData } from './Heading.stories'; import * as ExplorerStories from './Explorer.stories'; @@ -18,30 +19,39 @@ export default { }; const { menu } = standardHeaderData; -const stories = mockDataset.withRoot; +const index = mockDataset.withRoot as IndexHash; const refId = DEFAULT_REF_ID; const storyId = 'root-1-child-a2--grandchild-a1-1'; -export const simpleData = { menu, stories, storyId }; -export const loadingData = { menu, stories: {} }; +export const simpleData = { menu, index, storyId }; +export const loadingData = { menu }; const refs: Record = { optimized: { id: 'optimized', title: 'This is a ref', url: 'https://example.com', - ready: false, type: 'lazy', - // @ts-expect-error (needs to be converted to CSF3) - stories, + index, + previewInitialized: true, + }, +}; + +const indexError = new Error('Failed to load index'); + +const refsError = { + optimized: { + ...refs.optimized, + index: undefined as IndexHash, + indexError, }, }; export const Simple = () => ( ( ); export const Loading = () => ( + +); + +export const Empty = () => ( + +); + +export const IndexError = () => ( ); -export const Empty = () => ( - -); - export const WithRefs = () => ( ( ); export const LoadingWithRefs = () => ( + +); + +export const LoadingWithRefError = () => ( ); diff --git a/code/ui/manager/src/components/sidebar/Sidebar.tsx b/code/ui/manager/src/components/sidebar/Sidebar.tsx index b3ba9d26e51d..e6d8ba1ca285 100644 --- a/code/ui/manager/src/components/sidebar/Sidebar.tsx +++ b/code/ui/manager/src/components/sidebar/Sidebar.tsx @@ -2,8 +2,9 @@ import React, { useMemo } from 'react'; import { styled } from '@storybook/theming'; import { ScrollArea, Spaced } from '@storybook/components'; -import type { StoriesHash, State } from '@storybook/manager-api'; +import type { State } from '@storybook/manager-api'; +import type { API_LoadedRefData } from 'lib/types/src'; import { Heading } from './Heading'; // eslint-disable-next-line import/no-cycle @@ -58,33 +59,23 @@ const Swap = React.memo(function Swap({ ); }); -const useCombination = ( - stories: StoriesHash, - ready: boolean, - error: Error | undefined, - refs: Refs -): CombinedDataset => { +const useCombination = (defaultRefData: API_LoadedRefData, refs: Refs): CombinedDataset => { const hash = useMemo( () => ({ [DEFAULT_REF_ID]: { - stories, + ...defaultRefData, title: null, id: DEFAULT_REF_ID, url: 'iframe.html', - ready, - error, }, ...refs, }), - [refs, stories] + [refs, defaultRefData] ); return useMemo(() => ({ hash, entries: Object.entries(hash) }), [hash]); }; -export interface SidebarProps { - stories: StoriesHash; - storiesConfigured: boolean; - storiesFailed?: Error; +export interface SidebarProps extends API_LoadedRefData { refs: State['refs']; menu: any[]; storyId?: string; @@ -96,9 +87,9 @@ export interface SidebarProps { export const Sidebar = React.memo(function Sidebar({ storyId = null, refId = DEFAULT_REF_ID, - stories, - storiesConfigured, - storiesFailed, + index, + indexError, + previewInitialized, menu, menuHighlighted = false, enableShortcuts = true, @@ -106,8 +97,8 @@ export const Sidebar = React.memo(function Sidebar({ }: SidebarProps) { const selected: Selection = useMemo(() => storyId && { storyId, refId }, [storyId, refId]); - const dataset = useCombination(stories, storiesConfigured, storiesFailed, refs); - const isLoading = !dataset.hash[DEFAULT_REF_ID].ready; + const dataset = useCombination({ index, indexError, previewInitialized }, refs); + const isLoading = !index && !indexError; const lastViewedProps = useLastViewed(selected); return ( diff --git a/code/ui/manager/src/components/sidebar/Tree.stories.tsx b/code/ui/manager/src/components/sidebar/Tree.stories.tsx index 29923a404a4a..807b861cccf2 100644 --- a/code/ui/manager/src/components/sidebar/Tree.stories.tsx +++ b/code/ui/manager/src/components/sidebar/Tree.stories.tsx @@ -1,11 +1,11 @@ /* eslint-disable storybook/use-storybook-testing-library */ // @TODO: use addon-interactions and remove the rule disable above import React from 'react'; -import type { ComponentEntry, StoriesHash } from '@storybook/manager-api'; +import type { ComponentEntry, IndexHash } from '@storybook/manager-api'; import { screen } from '@testing-library/dom'; import { Tree } from './Tree'; -import { stories } from './mockdata.large'; +import { index } from './mockdata.large'; import { DEFAULT_REF_ID } from './Sidebar'; export default { @@ -17,7 +17,7 @@ export default { }; const refId = DEFAULT_REF_ID; -const storyId = Object.values(stories).find((story) => story.type === 'story').id; +const storyId = Object.values(index).find((story) => story.type === 'story').id; const log = (id: string) => console.log(id); @@ -29,7 +29,7 @@ export const Full = () => { isBrowsing isMain refId={refId} - data={stories} + data={index} highlightedRef={{ current: { itemId: selectedId, refId } }} setHighlightedItemId={log} selectedStoryId={selectedId} @@ -38,10 +38,10 @@ export const Full = () => { ); }; -const tooltipStories = Object.keys(stories).reduce((acc, key) => { +const tooltipStories = Object.keys(index).reduce((acc, key) => { if (key === 'tooltip-tooltipselect--default') { acc['tooltip-tooltipselect--tooltipselect'] = { - ...stories[key], + ...index[key], id: 'tooltip-tooltipselect--tooltipselect', name: 'TooltipSelect', }; @@ -49,16 +49,16 @@ const tooltipStories = Object.keys(stories).reduce((acc, key) => { } if (key === 'tooltip-tooltipselect') { acc[key] = { - ...(stories[key] as ComponentEntry), + ...(index[key] as ComponentEntry), children: ['tooltip-tooltipselect--tooltipselect'], }; return acc; } - if (key.startsWith('tooltip')) acc[key] = stories[key]; + if (key.startsWith('tooltip')) acc[key] = index[key]; return acc; -}, {} as StoriesHash); +}, {} as IndexHash); -const singleStoryComponent: StoriesHash = { +const singleStoryComponent: IndexHash = { // @ts-expect-error (invalid input) single: { type: 'component', @@ -102,7 +102,7 @@ export const SingleStoryComponents = () => { ); }; -const docsOnlySinglesStoryComponent: StoriesHash = { +const docsOnlySinglesStoryComponent: IndexHash = { // @ts-expect-error (invalid input) single: { type: 'component', @@ -147,7 +147,7 @@ export const SkipToCanvasLinkFocused = { isBrowsing: true, isMain: true, refId, - data: stories, + data: index, highlightedRef: { current: { itemId: 'tooltip-tooltipbuildlist--default', refId } }, setHighlightedItemId: log, selectedStoryId: 'tooltip-tooltipbuildlist--default', diff --git a/code/ui/manager/src/components/sidebar/__tests__/Sidebar.test.tsx b/code/ui/manager/src/components/sidebar/__tests__/Sidebar.test.tsx index 3744b5890b66..2e409618e8ed 100644 --- a/code/ui/manager/src/components/sidebar/__tests__/Sidebar.test.tsx +++ b/code/ui/manager/src/components/sidebar/__tests__/Sidebar.test.tsx @@ -2,9 +2,10 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import { ThemeProvider, ensure, themes } from '@storybook/theming'; -import type { HashEntry, StoriesHash, Refs } from '@storybook/manager-api'; +import type { HashEntry, Refs } from '@storybook/manager-api'; import type { Theme } from '@storybook/theming'; import type { RenderResult } from '@testing-library/react'; +import type { API_IndexHash } from '@storybook/types'; import { Sidebar } from '../Sidebar'; import type { SidebarProps } from '../Sidebar'; @@ -15,12 +16,12 @@ const factory = (props: Partial): RenderResult => { return render( - + ); }; -const generateStories = ({ title, refId }: { title: string; refId?: string }): StoriesHash => { +const generateStories = ({ title, refId }: { title: string; refId?: string }): API_IndexHash => { const [root, componentName]: [string, string] = title.split('/') as any; const rootId: string = root.toLowerCase().replace(/\s+/g, '-'); const hypenatedComponentName: string = componentName.toLowerCase().replace(/\s+/g, '-'); @@ -61,7 +62,7 @@ const generateStories = ({ title, refId }: { title: string; refId?: string }): S }, ]; - return storyBase.reduce((accumulator: StoriesHash, current: HashEntry): StoriesHash => { + return storyBase.reduce((accumulator: API_IndexHash, current: HashEntry): API_IndexHash => { accumulator[current.id] = current; return accumulator; }, {}); @@ -71,14 +72,14 @@ describe('Sidebar', () => { test.skip("should not render an extra nested 'Page'", async () => { const refId = 'next'; const title = 'Getting Started/Install'; - const refStories: StoriesHash = generateStories({ refId, title }); - const internalStories: StoriesHash = generateStories({ title: 'Welcome/Example' }); + const refIndex: API_IndexHash = generateStories({ refId, title }); + const internalIndex: API_IndexHash = generateStories({ title: 'Welcome/Example' }); const refs: Refs = { [refId]: { - stories: refStories, + index: refIndex, id: refId, - ready: true, + previewInitialized: true, title: refId, url: 'https://ref.url', }, @@ -87,7 +88,7 @@ describe('Sidebar', () => { factory({ refs, refId, - stories: internalStories, + index: internalIndex, }); fireEvent.click(screen.getByText('Install')); diff --git a/code/ui/manager/src/components/sidebar/mockdata.large.ts b/code/ui/manager/src/components/sidebar/mockdata.large.ts index 34d83fb5ea5b..b4c2a0b99399 100644 --- a/code/ui/manager/src/components/sidebar/mockdata.large.ts +++ b/code/ui/manager/src/components/sidebar/mockdata.large.ts @@ -14,7 +14,7 @@ import type { Dataset } from './types'; // @ts-expect-error (TODO) -export const stories = { +export const index = { images: { name: 'Images', id: 'images', diff --git a/code/ui/manager/src/components/sidebar/utils.ts b/code/ui/manager/src/components/sidebar/utils.ts index 5406989df10d..e751ef5e9bd8 100644 --- a/code/ui/manager/src/components/sidebar/utils.ts +++ b/code/ui/manager/src/components/sidebar/utils.ts @@ -1,7 +1,7 @@ import memoize from 'memoizerific'; import { global } from '@storybook/global'; import type { SyntheticEvent } from 'react'; -import type { HashEntry, StoriesHash } from '@storybook/manager-api'; +import type { HashEntry, IndexHash } from '@storybook/manager-api'; // eslint-disable-next-line import/no-cycle import { DEFAULT_REF_ID } from './Sidebar'; @@ -30,11 +30,11 @@ export const getParents = memoize(1000)((id: string, dataset: Dataset): Item[] = const parent = getParent(id, dataset); return parent ? [parent, ...getParents(parent.id, dataset)] : []; }); -export const getAncestorIds = memoize(1000)((data: StoriesHash, id: string): string[] => +export const getAncestorIds = memoize(1000)((data: IndexHash, id: string): string[] => getParents(id, data).map((item) => item.id) ); export const getDescendantIds = memoize(1000)( - (data: StoriesHash, id: string, skipLeafs: boolean): string[] => { + (data: IndexHash, id: string, skipLeafs: boolean): string[] => { const entry = data[id]; const children = entry.type === 'story' || entry.type === 'docs' ? [] : entry.children; return children.reduce((acc, childId) => { @@ -47,7 +47,7 @@ export const getDescendantIds = memoize(1000)( ); export function getPath(item: Item, ref: RefType): string[] { - const parent = item.type !== 'root' && item.parent ? ref.stories[item.parent] : null; + const parent = item.type !== 'root' && item.parent ? ref.index[item.parent] : null; if (parent) return [...getPath(parent, ref), parent.name]; return ref.id === DEFAULT_REF_ID ? [] : [ref.title || ref.id]; } diff --git a/code/ui/manager/src/containers/sidebar.tsx b/code/ui/manager/src/containers/sidebar.tsx index 4d5b29352be1..78a05c817ff3 100755 --- a/code/ui/manager/src/containers/sidebar.tsx +++ b/code/ui/manager/src/containers/sidebar.tsx @@ -16,9 +16,9 @@ const Sidebar = React.memo(function Sideber() { storyId, refId, layout: { showToolbar, isFullscreen, showPanel, showNav }, - storiesHash, - storiesConfigured, - storiesFailed, + index, + indexError, + previewInitialized, refs, } = state; @@ -27,9 +27,9 @@ const Sidebar = React.memo(function Sideber() { return { title: name, url, - stories: storiesHash, - storiesFailed, - storiesConfigured, + index, + indexError, + previewInitialized, refs, storyId, refId, diff --git a/code/ui/manager/src/index.tsx b/code/ui/manager/src/index.tsx index 707be6f82d2e..0660ababc796 100644 --- a/code/ui/manager/src/index.tsx +++ b/code/ui/manager/src/index.tsx @@ -55,8 +55,8 @@ const Main: FC<{ provider: Provider }> = ({ provider }) => { const panelCount = Object.keys(api.getPanels()).length; const story = api.getData(state.storyId, state.refId); const isLoading = story - ? !!state.refs[state.refId] && !state.refs[state.refId].ready - : !state.storiesFailed && !state.storiesConfigured; + ? !!state.refs[state.refId] && !state.refs[state.refId].previewInitialized + : !state.previewInitialized; return (