Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Addon API: Add experimental page addon type #23307

Merged
merged 37 commits into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
683c480
add a new addon type: Page, refactor manager layout to allow for pages
ndelangen Jul 4, 2023
f28c739
apply router changes from https://github.com/storybookjs/storybook/pu…
ndelangen Jul 4, 2023
d31d7cb
make `/` the default path, even when there's no query in the URL
ndelangen Jul 4, 2023
2724a96
cleanup
ndelangen Jul 4, 2023
dbc587f
fix an ancient bug where navigating before preview load, the preview …
ndelangen Jul 4, 2023
40daf32
Merge branch 'norbert/ui-tabs-types-improvements' into norbert/page-a…
ndelangen Jul 4, 2023
6c26daf
cleanup
ndelangen Jul 4, 2023
4486548
fix
ndelangen Jul 4, 2023
ca586a4
fix
ndelangen Jul 4, 2023
3837bdc
Merge branch 'norbert/ui-tabs-types-improvements' into norbert/page-a…
ndelangen Jul 5, 2023
87e8e14
Merge branch 'norbert/ui-tabs-types-improvements' into norbert/page-a…
ndelangen Jul 6, 2023
2a8fe23
Merge branch 'norbert/page-addons-refactor' of github.com:storybookjs…
ndelangen Jul 6, 2023
155eb12
Merge branch 'norbert/ui-tabs-types-improvements' into norbert/page-a…
ndelangen Jul 6, 2023
2a22fc6
Merge branch 'norbert/ui-tabs-types-improvements' into norbert/page-a…
ndelangen Jul 6, 2023
c9dcee1
Merge branch 'norbert/ui-tabs-types-improvements' into norbert/page-a…
ndelangen Jul 6, 2023
ad0a0fa
Merge branch 'release/7.2' into norbert/page-addons-refactor
ndelangen Jul 7, 2023
13ec5f6
Merge branch 'release/7.2' into norbert/page-addons-refactor
ndelangen Jul 7, 2023
bce1c1e
rename
ndelangen Jul 7, 2023
d67aeba
cleanup
ndelangen Jul 7, 2023
ccf22aa
I like tests
ndelangen Jul 7, 2023
1af8f33
Merge branch 'release/7.2' into norbert/page-addons-refactor
ndelangen Jul 10, 2023
ad444c7
Update code/lib/preview-api/src/modules/preview-web/PreviewWithSelect…
ndelangen Jul 11, 2023
8b33973
remove the fake generic type argument
ndelangen Jul 11, 2023
dde330c
improve readability of api's stories module init
ndelangen Jul 11, 2023
36f4a7a
fix cyclical state setting
ndelangen Jul 11, 2023
9f79059
addon render function are now called like React elements, thus keys a…
ndelangen Jul 11, 2023
d9e2984
fix type issues
ndelangen Jul 11, 2023
80e9be5
Merge branch 'release/7.2' into norbert/page-addons-refactor
ndelangen Jul 11, 2023
e981fb2
Merge branch 'release/7.2' into norbert/page-addons-refactor
ndelangen Jul 12, 2023
9027184
Merge branch 'norbert/page-addons-refactor' of github.com:storybookjs…
ndelangen Jul 12, 2023
428e6ce
add test for early setting of selection in preview-web
ndelangen Jul 12, 2023
701d560
add migration documentation
ndelangen Jul 12, 2023
f0b368d
move test
ndelangen Jul 12, 2023
f2b43c9
Merge branch 'release/7.2' into norbert/page-addons-refactor
ndelangen Jul 12, 2023
2aa315e
Straighten out behaviour when messages are received during init
tmeasday Jul 13, 2023
df8bf31
fix
ndelangen Jul 13, 2023
d5b712b
Merge branch 'release/7.2' into norbert/page-addons-refactor
ndelangen Jul 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<h1>Migration</h1>

- [From version 7.0.0 to 7.2.0](#from-version-700-to-720)
- [Addon API are more type-strict](#addon-api-are-more-type-strict)
- [From version 6.5.x to 7.0.0](#from-version-65x-to-700)
- [7.0 breaking changes](#70-breaking-changes)
- [Dropped support for Node 15 and below](#dropped-support-for-node-15-and-below)
Expand Down Expand Up @@ -300,6 +302,31 @@
- [Packages renaming](#packages-renaming)
- [Deprecated embedded addons](#deprecated-embedded-addons)

## From version 7.0.0 to 7.2.0

#### Addon API are more type-strict

When registering an addon using `@storybook/manager-api`, the addon API is now more type-strict. This means if you use typescript to compile your addon before publishing it might start giving you errors.

The `type` property is now required field, and the `id` property should not be set anymore.

Here's a correct example:
```tsx
import { addons, types } from '@storybook/manager-api';

addons.register('my-addon', () => {
addons.add('my-addon/panel', {
type: types.PANEL,
title: 'My Addon',
render: ({ active }) => active ? <div>Hello World</div> : null,
});
});
```

The API: `addons.addPanel()` is now deprecated, and will be removed in 8.0.0. Please use `addons.add()` instead.

The `render` method can now be a `React.FunctionComponent` (without the `children` prop). Storybook will now render it, rather than calling it as a function.

## From version 6.5.x to 7.0.0

A number of these changes can be made automatically by the Storybook CLI. To take advantage of these "automigrations", run `npx storybook@latest upgrade --prerelease` or `pnpx dlx storybook@latest upgrade --prerelease`.
Expand Down
4 changes: 2 additions & 2 deletions code/addons/a11y/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ addons.register(ADDON_ID, (api) => {
addons.add(PANEL_ID, {
title: Title,
type: types.PANEL,
render: ({ active = true, key }) => (
<A11yContextProvider key={key} active={active}>
render: ({ active = true }) => (
<A11yContextProvider active={active}>
<A11YPanel />
</A11yContextProvider>
),
Expand Down
2 changes: 1 addition & 1 deletion code/addons/actions/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ addons.register(ADDON_ID, (api) => {
addons.add(PANEL_ID, {
title: Title,
type: types.PANEL,
render: ({ active, key }) => <ActionLogger key={key} api={api} active={!!active} />,
render: ({ active }) => <ActionLogger api={api} active={!!active} />,
paramKey: PARAM_KEY,
});
});
4 changes: 2 additions & 2 deletions code/addons/controls/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ addons.register(ADDON_ID, (api) => {
title: Title,
type: types.PANEL,
paramKey: PARAM_KEY,
render: ({ key, active }) => {
render: ({ active }) => {
if (!active || !api.getCurrentStoryData()) {
return null;
}
return (
<AddonPanel key={key} active={active}>
<AddonPanel active={active}>
<ControlsPanel />
</AddonPanel>
);
Expand Down
4 changes: 2 additions & 2 deletions code/addons/interactions/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,15 @@ addons.register(ADDON_ID, (api) => {
type: types.PANEL,
title: Title,
match: ({ viewMode }) => viewMode === 'story',
render: ({ key, active }) => {
render: ({ active }) => {
const newLocal = useCallback(({ state }: Combo) => {
return {
storyId: state.storyId,
};
}, []);

return (
<AddonPanel key={key} active={active}>
<AddonPanel active={active}>
<Consumer filter={newLocal}>{({ storyId }) => <Panel storyId={storyId} />}</Consumer>
</AddonPanel>
);
Expand Down
2 changes: 1 addition & 1 deletion code/addons/jest/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ addons.register(ADDON_ID, (api) => {
addons.add(PANEL_ID, {
title: 'Tests',
type: types.PANEL,
render: ({ active, key }) => <Panel key={key} api={api} active={active} />,
render: ({ active }) => <Panel api={api} active={active} />,
paramKey: PARAM_KEY,
});
});
2 changes: 1 addition & 1 deletion code/addons/storysource/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ addons.register(ADDON_ID, (api) => {
addons.add(PANEL_ID, {
type: types.PANEL,
title: 'Code',
render: ({ active, key }) => (active ? <StoryPanel key={key} api={api} /> : null),
render: ({ active }) => (active ? <StoryPanel api={api} /> : null),
paramKey: 'storysource',
});
});
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 @@ -183,7 +183,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 @@ -418,36 +418,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) && 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 () => {
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -471,9 +476,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
15 changes: 10 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 @@ -19,10 +20,14 @@ export interface SubAPI {
* Returns a collection of elements of a specific type.
* @protected This is used internally in storybook's manager.
* @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: <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 +106,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
26 changes: 20 additions & 6 deletions code/lib/manager-api/src/modules/stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
CURRENT_STORY_WAS_SET,
STORY_MISSING,
DOCS_PREPARED,
SET_CURRENT_STORY,
} from '@storybook/core-events';
import { logger } from '@storybook/client-logger';

Expand Down Expand Up @@ -593,13 +594,26 @@ export const init: ModuleFn<SubAPI, SubState, true> = ({
const { sourceType } = getEventMetadata(this, fullAPI);

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();
if (state.storyId !== storyId || state.viewMode !== viewMode) {
navigate(`/${viewMode}/${storyId}`);
const isCanvasRoute =
state.path === '/' || state.viewMode === 'story' || state.viewMode === 'docs';
const stateHasSelection = state.viewMode && state.storyId;
const stateSelectionDifferent = state.viewMode !== viewMode || state.storyId !== storyId;
/**
* When storybook starts, we want to navigate to the first story.
* But there are a few exceptions:
* - If the current storyId and viewMode are already set/correct.
* - If the user has navigated away already.
* - If the user started storybook with a specific page-URL like "/settings/about"
*/
if (isCanvasRoute) {
if (stateHasSelection && stateSelectionDifferent) {
// The manager state is correct, the preview state is lagging behind
fullAPI.emit(SET_CURRENT_STORY, { storyId: state.storyId, viewMode: state.viewMode });
} else if (stateSelectionDifferent) {
// The preview state is correct, the manager state is lagging behind
navigate(`/${viewMode}/${storyId}`);
}
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand Down
Loading