mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-16 00:05:56 +08:00
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:
commit
007d11a5b1
@ -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,
|
||||
|
@ -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));
|
||||
});
|
||||
|
@ -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) => {
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
>
|
||||
>;
|
||||
|
||||
|
@ -80,6 +80,7 @@ const config: StorybookConfig = {
|
||||
sourcemap: process.env.CI !== 'true',
|
||||
},
|
||||
}),
|
||||
logLevel: 'debug',
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
@ -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
|
||||
|
@ -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 }) => ({
|
||||
|
@ -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') {
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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 {
|
||||
|
@ -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 });
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
@ -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 (
|
||||
|
@ -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',
|
||||
|
@ -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'));
|
||||
|
@ -14,7 +14,7 @@
|
||||
import type { Dataset } from './types';
|
||||
|
||||
// @ts-expect-error (TODO)
|
||||
export const stories = {
|
||||
export const index = {
|
||||
images: {
|
||||
name: 'Images',
|
||||
id: 'images',
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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}>
|
||||
|
Loading…
x
Reference in New Issue
Block a user