Merge pull request #20425 from storybookjs/tom/sb-1123-sb20250-vite-does-not-show-a-spinner-2

Core: Show "booting" progress until story is specified or errors
This commit is contained in:
Tom Coleman 2023-01-18 20:01:55 +11:00 committed by GitHub
commit 007d11a5b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 654 additions and 460 deletions

View File

@ -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,

View File

@ -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<API>;
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));
});

View File

@ -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<void>;
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<SubAPI, SubState, void> = (
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<SubAPI, SubState, void> = (
// 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<SubAPI, SubState, void> = (
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) => {

View File

@ -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<API_StoryEntry, 'parameters' | 'initialArgs' | 'argTypes' | 'args'>;
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<S>(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<void>;
updateStory: (storyId: StoryId, update: StoryUpdate, ref?: API_ComposedRef) => Promise<void>;
setPreviewInitialized: (ref?: ComposedRef) => Promise<void>;
}
const removedOptions = ['enableShortcuts', 'theme', 'showRoots'];
@ -132,11 +146,11 @@ export const init: ModuleFn<SubAPI, SubState, true> = ({
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<SubAPI, SubState, true> = ({
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<SubAPI, SubState, true> = ({
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<SubAPI, SubState, true> = ({
}
},
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<SubAPI, SubState, true> = ({
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<SubAPI, SubState, true> = ({
}
},
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<SubAPI, SubState, true> = ({
},
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<SubAPI, SubState, true> = ({
}
}
},
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<SubAPI, SubState, true> = ({
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<SubAPI, SubState, true> = ({
});
// 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<SubAPI, SubState, true> = ({
ref?: API_ComposedRef
): Promise<void> => {
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<void> => {
if (!ref) {
store.setState({ previewInitialized: true });
} else {
fullAPI.updateRef(ref.id, { previewInitialized: true });
}
},
};
@ -387,9 +401,9 @@ export const init: ModuleFn<SubAPI, SubState, true> = ({
}) {
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<SubAPI, SubState, true> = ({
}
);
// 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<SubAPI, SubState, true> = ({
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<SubAPI, SubState, true> = ({
}
);
// 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<SubAPI, SubState, true> = ({
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,
};

View File

@ -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']);

File diff suppressed because it is too large Load Diff

View File

@ -277,7 +277,7 @@ export class PreviewWithSelection<TFramework extends Renderer> extends Preview<T
if (entry.type === 'story') {
this.view.showPreparingStory({ immediate: viewModeChanged });
} else {
this.view.showPreparingDocs();
this.view.showPreparingDocs({ immediate: viewModeChanged });
}
// If the last render is still preparing, let's drop it right now. Either

View File

@ -10,9 +10,9 @@ export interface View<TStorybookRoot> {
showNoPreview(): void;
showPreparingStory(options: { immediate: boolean }): void;
showPreparingStory(options?: { immediate: boolean }): void;
showPreparingDocs(): void;
showPreparingDocs(options?: { immediate: boolean }): void;
showMain(): void;

View File

@ -165,9 +165,13 @@ export class WebView implements View<HTMLElement> {
}
}
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() {

View File

@ -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

View File

@ -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'
>
>;

View File

@ -80,6 +80,7 @@ const config: StorybookConfig = {
sourcemap: process.env.CI !== 'true',
},
}),
logLevel: 'debug',
};
export default config;

View File

@ -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 = () => (
<ManagerProvider
key="manager"
provider={new FakeProvider()}
path="/story/ui-app--loading-state"
storyId="ui-app--loading-state"
location={{ search: '' }}
navigate={() => {}}
docsOptions={{ docsMode: false }}
>
<App
key="app"
viewMode="story"
layout={{
initialActive: 'addons',
isFullscreen: false,
showToolbar: true,
panelPosition: 'right',
showNav: true,
showPanel: true,
showTabs: true,
}}
panelCount={0}
/>
</ManagerProvider>
);
function setPreviewInitialized({ api }: { api: API }) {
api.setPreviewInitialized();
return {};
}
export const Default = () => {
const provider = new FakeProvider();
return (
<ManagerProvider
key="manager"
provider={provider}
path="/story/ui-app--loading-state"
storyId="ui-app--loading-state"
location={{ search: '' }}
navigate={() => {}}
docsOptions={{ docsMode: false }}
>
<Consumer filter={setPreviewInitialized}>{() => <></>}</Consumer>
<App
key="app"
viewMode="story"
layout={{
initialActive: 'addons',
isFullscreen: false,
showToolbar: true,
panelPosition: 'right',
showNav: true,
showPanel: true,
showTabs: true,
}}
panelCount={0}
/>
</ManagerProvider>
);
};
export const LoadingState = () => (
<ManagerProvider

View File

@ -58,10 +58,10 @@ export const panels: Addon_Collection = {
};
const realSidebarProps: SidebarProps = {
stories: mockDataset.withRoot as SidebarProps['stories'],
index: mockDataset.withRoot as SidebarProps['index'],
menu: [],
refs: {},
storiesConfigured: true,
previewInitialized: true,
};
const PlaceholderBlock = styled.div(({ color }) => ({

View File

@ -80,7 +80,7 @@ export const FramesRenderer: FC<FramesRendererProps> = ({
useEffect(() => {
const newFrames = Object.values(refs)
.filter((r) => {
if (r.error) {
if (r.indexError) {
return false;
}
if (r.type === 'auto-inject') {

View File

@ -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 (
<ZoomConsumer>

View File

@ -25,9 +25,9 @@ const simple: Record<string, RefType> = {
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<string, RefType> = {
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,
},
};

View File

@ -168,7 +168,7 @@ export const RefIndicator = React.memo(
forwardRef<HTMLElement, RefType & { state: ReturnType<typeof getStateType> }>(
({ 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]

View File

@ -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<string, RefType> = {
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<string, RefType> = {
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<string, RefType> = {
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,
},
};

View File

@ -99,7 +99,7 @@ export const Ref: FC<RefType & RefProps> = 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<RefType & RefProps> = 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<HTMLElement>(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<RefType & RefProps> = React.memo(function Ref(props) {
{isExpanded && (
<Wrapper data-title={title} isMain={isMain}>
{state === 'auth' && <AuthBlock id={refId} loginUrl={loginUrl} />}
{state === 'error' && <ErrorBlock error={error} />}
{state === 'error' && <ErrorBlock error={indexError} />}
{state === 'loading' && <LoaderBlock isMain={isMain} />}
{state === 'empty' && <EmptyBlock isMain={isMain} />}
{state === 'ready' && (
@ -161,7 +161,7 @@ export const Ref: FC<RefType & RefProps> = React.memo(function Ref(props) {
isBrowsing={isBrowsing}
isMain={isMain}
refId={refId}
data={stories}
data={index}
docsMode={docsOptions.docsMode}
selectedStoryId={selectedStoryId}
onSelectStoryId={onSelectStoryId}

View File

@ -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 {

View File

@ -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 });

View File

@ -18,10 +18,10 @@ export default {
const combinedDataset = (refs: Record<string, StoriesHash>): 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<string, StoriesHash>): 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);

View File

@ -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<string, RefType> = {
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 = () => (
<Sidebar
storiesConfigured
previewInitialized
menu={menu}
stories={stories as any}
index={index as any}
storyId={storyId}
refId={refId}
refs={{}}
@ -49,25 +59,29 @@ export const Simple = () => (
);
export const Loading = () => (
<Sidebar previewInitialized={false} menu={menu} storyId={storyId} refId={refId} refs={{}} />
);
export const Empty = () => (
<Sidebar previewInitialized menu={menu} index={{}} storyId={storyId} refId={refId} refs={{}} />
);
export const IndexError = () => (
<Sidebar
storiesConfigured={false}
previewInitialized
indexError={indexError}
menu={menu}
stories={{}}
storyId={storyId}
refId={refId}
refs={{}}
/>
);
export const Empty = () => (
<Sidebar storiesConfigured menu={menu} stories={{}} storyId={storyId} refId={refId} refs={{}} />
);
export const WithRefs = () => (
<Sidebar
storiesConfigured
previewInitialized
menu={menu}
stories={stories as any}
index={index as any}
storyId={storyId}
refId={refId}
refs={refs}
@ -75,12 +89,15 @@ export const WithRefs = () => (
);
export const LoadingWithRefs = () => (
<Sidebar previewInitialized={false} menu={menu} storyId={storyId} refId={refId} refs={refs} />
);
export const LoadingWithRefError = () => (
<Sidebar
storiesConfigured={false}
previewInitialized={false}
menu={menu}
stories={stories as any}
storyId={storyId}
refId={refId}
refs={refs}
refs={refsError}
/>
);

View File

@ -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 (

View File

@ -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',

View File

@ -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<SidebarProps>): RenderResult => {
return render(
<ThemeProvider theme={theme}>
<Sidebar storiesConfigured menu={[]} stories={{}} refs={{}} {...props} />
<Sidebar menu={[]} index={{}} previewInitialized refs={{}} {...props} />
</ThemeProvider>
);
};
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'));

View File

@ -14,7 +14,7 @@
import type { Dataset } from './types';
// @ts-expect-error (TODO)
export const stories = {
export const index = {
images: {
name: 'Images',
id: 'images',

View File

@ -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];
}

View File

@ -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,

View File

@ -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 (
<CacheProvider value={emotionCache}>