Skip to content

Commit

Permalink
add a new addon type: Page, refactor manager layout to allow for pages
Browse files Browse the repository at this point in the history
  • Loading branch information
ndelangen committed Jul 4, 2023
1 parent 8afab86 commit 683c480
Show file tree
Hide file tree
Showing 18 changed files with 489 additions and 185 deletions.
35 changes: 20 additions & 15 deletions code/lib/manager-api/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ class ManagerProvider extends Component<ManagerProviderProps, State> {
location,
path,
refId,
viewMode = props.docsOptions.docsMode ? 'docs' : 'story',
viewMode = props.docsOptions.docsMode ? 'docs' : props.viewMode,
singleStory,
storyId,
docsOptions,
Expand Down Expand Up @@ -419,36 +419,41 @@ const addonStateCache: {
// shared state
export function useSharedState<S>(stateId: string, defaultState?: S) {
const api = useStorybookApi();
const existingState = api.getAddonState<S>(stateId);
const existingState = api.getAddonState<S>(stateId) || addonStateCache[stateId];
const state = orDefault<S>(
existingState,
addonStateCache[stateId] ? addonStateCache[stateId] : defaultState
);
const setState = (s: S | API_StateMerger<S>, options?: Options) => {
// set only after the stories are loaded
if (addonStateCache[stateId]) {

if (api.getAddonState(stateId) !== state) {
api.setAddonState<S>(stateId, state).then((s) => {
addonStateCache[stateId] = s;
}
api.setAddonState<S>(stateId, s, options);
});
}

const setState = (s: S | API_StateMerger<S>, options?: Options) => {
const result = api.setAddonState<S>(stateId, s, options);
addonStateCache[stateId] = result;
return result;
};
const allListeners = useMemo(() => {
const stateChangeHandlers = {
[`${SHARED_STATE_CHANGED}-client-${stateId}`]: (s: S) => setState(s),
[`${SHARED_STATE_SET}-client-${stateId}`]: (s: S) => setState(s),
[`${SHARED_STATE_CHANGED}-client-${stateId}`]: setState,
[`${SHARED_STATE_SET}-client-${stateId}`]: setState,
};
const stateInitializationHandlers = {
[SET_STORIES]: () => {
[SET_STORIES]: async () => {
const currentState = api.getAddonState(stateId);
if (currentState) {
addonStateCache[stateId] = currentState;
api.emit(`${SHARED_STATE_SET}-manager-${stateId}`, currentState);
} else if (addonStateCache[stateId]) {
// this happens when HMR
setState(addonStateCache[stateId]);
await setState(addonStateCache[stateId]);
api.emit(`${SHARED_STATE_SET}-manager-${stateId}`, addonStateCache[stateId]);
} else if (defaultState !== undefined) {
// if not HMR, yet the defaults are from the manager
setState(defaultState);
await setState(defaultState);
// initialize addonStateCache after first load, so its available for subsequent HMR
addonStateCache[stateId] = defaultState;
api.emit(`${SHARED_STATE_SET}-manager-${stateId}`, defaultState);
Expand All @@ -472,9 +477,9 @@ export function useSharedState<S>(stateId: string, defaultState?: S) {
const emit = useChannel(allListeners);
return [
state,
(newStateOrMerger: S | API_StateMerger<S>, options?: Options) => {
setState(newStateOrMerger, options);
emit(`${SHARED_STATE_CHANGED}-manager-${stateId}`, newStateOrMerger);
async (newStateOrMerger: S | API_StateMerger<S>, options?: Options) => {
const result = await setState(newStateOrMerger, options);
emit(`${SHARED_STATE_CHANGED}-manager-${stateId}`, result);
},
] as [S, (newStateOrMerger: S | API_StateMerger<S>, options?: Options) => void];
}
Expand Down
55 changes: 46 additions & 9 deletions code/lib/manager-api/src/lib/addons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type {
Addon_Elements,
Addon_Loaders,
Addon_Type,
Addon_BaseType,
Addon_PageType,
Addon_Types,
} from '@storybook/types';
import { Addon_TypesEnum } from '@storybook/types';
Expand Down Expand Up @@ -93,28 +95,56 @@ export class AddonStore {
this.serverChannel = channel;
};

getElements = (type: Addon_Types): Addon_Collection => {
getElements<T extends Addon_Types | Addon_TypesEnum.experimental_PAGE>(
type: T
): T extends Addon_TypesEnum.experimental_PAGE
? Addon_Collection<Addon_PageType>
: Addon_Collection<Addon_BaseType> {
if (!this.elements[type]) {
this.elements[type] = {};
}
// @ts-expect-error (Kaspar told me to do this)
return this.elements[type];
};
}

/**
* Adds a panel to the addon store.
* @param {string} id - The id of the panel.
* @param {Addon_Type} options - The options for the panel.
* @returns {void}
*
* @deprecated Use the 'add' method instead.
* @example
* addons.add('My Panel', {
* title: 'My Title',
* type: types.PANEL,
* render: () => <div>My Content</div>,
* });
*/
addPanel = (
id: string,
options: Omit<Addon_Type, 'type' | 'id'> & DeprecatedAddonWithId
options: Omit<Addon_BaseType, 'type' | 'id'> & DeprecatedAddonWithId
): void => {
this.add(id, {
type: Addon_TypesEnum.PANEL,
...options,
});
};

add = (id: string, addon: Omit<Addon_Type, 'id'> & DeprecatedAddonWithId) => {
/**
* Adds an addon to the addon store.
* @param {string} id - The id of the addon.
* @param {Addon_Type} addon - The addon to add.
* @returns {void}
*/
add(
id: string,
addon: Addon_BaseType | (Omit<Addon_PageType, 'id'> & DeprecatedAddonWithId)
): void {
const { type } = addon;
const collection = this.getElements(type);
collection[id] = { id, ...addon };
};
}

setConfig = (value: Addon_Config) => {
Object.assign(this.config, value);
Expand All @@ -129,11 +159,18 @@ export class AddonStore {

getConfig = () => this.config;

register = (name: string, registerCallback: (api: API) => void): void => {
if (this.loaders[name]) {
logger.warn(`${name} was loaded twice, this could have bad side-effects`);
/**
* Registers an addon loader function.
*
* @param {string} id - The id of the addon loader.
* @param {(api: API) => void} callback - The function that will be called to register the addon.
* @returns {void}
*/
register = (id: string, callback: (api: API) => void): void => {
if (this.loaders[id]) {
logger.warn(`${id} was loaded twice, this could have bad side-effects`);
}
this.loaders[name] = registerCallback;
this.loaders[id] = callback;
};

loadAddons = (api: any) => {
Expand Down
19 changes: 14 additions & 5 deletions code/lib/manager-api/src/modules/addons.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type {
Addon_Type,
Addon_BaseType,
Addon_Collection,
Addon_PageType,
Addon_Types,
API_Collection,
API_Panels,
API_StateMerger,
} from '@storybook/types';
Expand All @@ -18,11 +19,19 @@ export interface SubAPI {
/**
* Returns a collection of elements of a specific type.
* @protected This is used internally in storybook's manager.
* @template FAKE - The type of the elements in the collection.
* @template T - The type of the elements in the collection.
* @param {Addon_Types} type - The type of the elements to retrieve.
* @param {Addon_Types | Addon_TypesEnum.experimental_PAGE} type - The type of the elements to retrieve.
* @returns {API_Collection<T>} - A collection of elements of the specified type.
*/
getElements: <T = Addon_Type>(type: Addon_Types) => API_Collection<T>;
getElements: <
FAKE = any,
T extends Addon_Types | Addon_TypesEnum.experimental_PAGE = Addon_Types
>(
type: T
) => T extends Addon_TypesEnum.experimental_PAGE
? Addon_Collection<Addon_PageType>
: Addon_Collection<Addon_BaseType>;
/**
* Returns a collection of all panels.
* This is the same as calling getElements('panel')
Expand Down Expand Up @@ -101,7 +110,7 @@ export const init: ModuleFn<SubAPI, SubState> = ({ provider, store, fullAPI }) =

const { parameters } = story;

const filteredPanels: API_Collection = {};
const filteredPanels: Addon_Collection<Addon_BaseType> = {};
Object.entries(allPanels).forEach(([id, panel]) => {
const { paramKey } = panel;
if (paramKey && parameters && parameters[paramKey] && parameters[paramKey].disable) {
Expand Down
16 changes: 15 additions & 1 deletion code/lib/manager-api/src/modules/settings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import type { API_Settings } from '@storybook/types';
import type { API_Settings, StoryId } from '@storybook/types';
import type { ModuleFn } from '../index';

export interface SubAPI {
storeSelection: () => void;
retrieveSelection: () => StoryId;
/**
* Changes the active settings tab.
* @param path - The path of the settings page to navigate to. The path NOT should include the `/settings` prefix.
Expand Down Expand Up @@ -62,6 +64,18 @@ export const init: ModuleFn<SubAPI, SubState> = ({ store, navigate, fullAPI }) =

navigate(path);
},
retrieveSelection() {
const { settings } = store.getState();

return settings.lastTrackedStoryId;
},
storeSelection: async () => {
const { storyId, settings } = store.getState();

await store.setState({
settings: { ...settings, lastTrackedStoryId: storyId },
});
},
};

return { state: { settings: { lastTrackedStoryId: null } }, api };
Expand Down
10 changes: 8 additions & 2 deletions code/lib/manager-api/src/modules/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,11 +600,17 @@ export const init: ModuleFn<SubAPI, SubState, true> = ({
const { sourceType } = getEventMetadata(this, fullAPI);

if (sourceType === 'local') {
if (fullAPI.isSettingsScreenActive()) return;
const state = store.getState();
if (
state.location?.pathname !== '/' &&
state.viewMode !== 'story' &&
state.viewMode !== 'docs'
) {
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();
if (state.storyId !== storyId || state.viewMode !== viewMode) {
navigate(`/${viewMode}/${storyId}`);
}
Expand Down
56 changes: 52 additions & 4 deletions code/lib/manager-api/src/tests/stories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,8 +699,14 @@ describe('stories API', () => {
return false;
},
});
const store = createMockStore({});
const { init, api } = initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
const store = createMockStore({ viewMode: 'story' });
const { init, api } = initStoriesAndSetState({
store,
navigate,
provider,
fullAPI,
viewMode: 'story',
} as any);

Object.assign(fullAPI, api);
init();
Expand All @@ -717,7 +723,16 @@ describe('stories API', () => {
},
});
const store = createMockStore({ viewMode: 'story', storyId: 'a--1' });
initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
const { api, init } = initStoriesAndSetState({
store,
navigate,
provider,
fullAPI,
viewMode: 'story',
storyId: 'a--1',
} as any);
Object.assign(fullAPI, api);
init();

fullAPI.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' });

Expand All @@ -732,7 +747,40 @@ describe('stories API', () => {
},
});
const store = createMockStore({ viewMode: 'settings', storyId: 'about' });
initStoriesAndSetState({ store, navigate, provider, fullAPI } as any);
const { api, init } = initStoriesAndSetState({
store,
navigate,
provider,
fullAPI,
viewMode: 'settings',
storyId: 'about',
} as any);
Object.assign(fullAPI, api);
init();

fullAPI.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' });

expect(navigate).not.toHaveBeenCalled();
});

it('DOES not navigate if a custom page was selected', async () => {
const navigate = jest.fn();
const fullAPI = Object.assign(new EventEmitter(), {
isSettingsScreenActive() {
return true;
},
});
const store = createMockStore({ viewMode: 'custom', storyId: undefined });
const { api, init } = initStoriesAndSetState({
store,
navigate,
provider,
fullAPI,
viewMode: 'custom',
storyId: undefined,
} as any);
Object.assign(fullAPI, api);
init();

fullAPI.emit(STORY_SPECIFIED, { storyId: 'a--1', viewMode: 'story' });

Expand Down
Loading

0 comments on commit 683c480

Please sign in to comment.