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

UI: Sidebar context menu addon API #29557

Merged
merged 59 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
1abc353
allow custom link in context menu in sidebar, add addon type for inje…
ndelangen Nov 6, 2024
c69c1f3
hoist testProvider state to root, add WIP sidebar contextMenu
ndelangen Nov 8, 2024
716917d
improve UI
ndelangen Nov 12, 2024
a2d07fb
Merge branch 'next' into norbert/addon-api-context-menu
ndelangen Nov 12, 2024
82c5cde
add UI in context
ndelangen Nov 12, 2024
7bfc2ad
cleanup
ndelangen Nov 12, 2024
15e0f2c
cleanup
ndelangen Nov 12, 2024
9faaa59
cleanup
ndelangen Nov 12, 2024
653ff53
fixes
ndelangen Nov 12, 2024
b833d03
Merge branch 'next' into norbert/addon-api-context-menu
ndelangen Nov 12, 2024
d5995fd
Merge branch 'next' into norbert/addon-api-context-menu
ndelangen Nov 13, 2024
9d4eae2
remove contextMenu from test addon until it's ready for primetime
ndelangen Nov 13, 2024
b20b07c
Merge branch 'norbert/addon-api-context-menu' of https://github.com/s…
ndelangen Nov 13, 2024
7ad41a7
improvements
ndelangen Nov 13, 2024
4ea557e
fix build race condition
ndelangen Nov 13, 2024
74972a7
refactor
ndelangen Nov 13, 2024
cab4b71
fixing the core prep script race condition
ndelangen Nov 13, 2024
17b2890
fix for dev-mode
ndelangen Nov 13, 2024
76687a7
move components into components dir
ndelangen Nov 13, 2024
0a0c36b
renames
ndelangen Nov 13, 2024
39be69f
Merge branch 'next' into norbert/addon-api-context-menu
ndelangen Nov 13, 2024
4d8c845
fixes
ndelangen Nov 13, 2024
4d4bc94
fix tooltip not hiding after click in the sidebar gear menu
ndelangen Nov 14, 2024
800de95
Merge branch 'next' into norbert/addon-api-context-menu
ndelangen Nov 14, 2024
d99f0e6
fixes
ndelangen Nov 14, 2024
350cee7
fix initial state
ndelangen Nov 14, 2024
d23899e
Replace test provider title and description with render function
ghengeveld Nov 14, 2024
4582440
Fix story mocks and api call
ghengeveld Nov 14, 2024
6c411d4
fix condition hooks rendering bug
ndelangen Nov 14, 2024
011b854
fix bug with persisting state with function
ndelangen Nov 14, 2024
ad55d61
Merge branch 'norbert/addon-api-context-menu' of https://github.com/s…
ndelangen Nov 14, 2024
1c85387
use correct title component in panel
ndelangen Nov 14, 2024
f8cbd83
fix
ndelangen Nov 14, 2024
a35701d
fixes
ndelangen Nov 14, 2024
166aeb6
fix api method renames
ndelangen Nov 14, 2024
8b63bcb
Merge branch 'next' into norbert/addon-api-context-menu
ndelangen Nov 14, 2024
dfc147a
send specific stories when running from context menu
JReinhold Nov 14, 2024
a9e7905
Merge branch 'norbert/addon-api-context-menu' of github.com:storybook…
JReinhold Nov 14, 2024
e5e4499
set testNamePattern from story names
JReinhold Nov 14, 2024
8147355
update test run message, allow canceling
JReinhold Nov 14, 2024
7e43ecd
use root cancelTestProvider function
JReinhold Nov 14, 2024
c912e0e
do not depend on mouseLeave event, as it's often missed when user qui…
ndelangen Nov 15, 2024
9e841f8
simplify, add comment for clarity
ndelangen Nov 15, 2024
3a2415b
ensure the status icons are visible again, but hidden during hover
ndelangen Nov 15, 2024
5811cdb
fix duplicate keys in stories
JReinhold Nov 18, 2024
7454f3c
properly disable docs context loader in PS
JReinhold Nov 18, 2024
2b254b7
add testNamePattern to vitest mock
JReinhold Nov 18, 2024
ec28fa2
fix sidebarbottom story
ndelangen Nov 18, 2024
d2b09b1
Merge branch 'norbert/addon-api-context-menu' of https://github.com/s…
ndelangen Nov 18, 2024
dbdd73a
move stories into the same index ancestor
ndelangen Nov 18, 2024
2873064
enable addon-test context menu
JReinhold Nov 18, 2024
5101314
Merge branch 'norbert/addon-api-context-menu' of github.com:storybook…
JReinhold Nov 18, 2024
3d70d51
rename testproviders to testProviders & move RelativeTime component
ndelangen Nov 18, 2024
14f3ab4
Merge branch 'norbert/addon-api-context-menu' of https://github.com/s…
ndelangen Nov 18, 2024
adf6180
add interaction component test
ndelangen Nov 18, 2024
ac49cf1
cleanup
JReinhold Nov 19, 2024
fe2cec7
rename contextMenu > sidebarContextMenu in addon API
JReinhold Nov 19, 2024
b6aa053
hide context menu from test-less stories
JReinhold Nov 19, 2024
f771bdf
fix React key warning
JReinhold Nov 19, 2024
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
79 changes: 69 additions & 10 deletions code/addons/test/src/manager.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
import React, { useCallback, useEffect, useState } from 'react';
import React, { type FC, type SyntheticEvent, useCallback, useEffect, useState } from 'react';

import { AddonPanel, Badge, Link as LinkComponent, Spaced } from 'storybook/internal/components';
import {
AddonPanel,
Badge,
Button,
Link as LinkComponent,
type ListItem,
Spaced,
} from 'storybook/internal/components';
import { TESTING_MODULE_RUN_ALL_REQUEST } from 'storybook/internal/core-events';
import type { Combo } from 'storybook/internal/manager-api';
import { Consumer, addons, types, useAddonState } from 'storybook/internal/manager-api';
import {
Consumer,
addons,
types,
useAddonState,
useStorybookApi,
} from 'storybook/internal/manager-api';
import { useTheme } from 'storybook/internal/theming';
import {
type API_HashEntry,
type API_StatusObject,
type API_StatusValue,
type Addon_TestProviderState,
type Addon_TestProviderType,
Addon_TypesEnum,
} from 'storybook/internal/types';

import { PlayIcon } from '@storybook/icons';

import { Panel } from './Panel';
import { GlobalErrorModal } from './components/GlobalErrorModal';
import { ADDON_ID, PANEL_ID, TEST_PROVIDER_ID } from './constants';
Expand Down Expand Up @@ -79,6 +97,40 @@ const RelativeTime = ({ timestamp, testCount }: { timestamp: Date; testCount: nu
);
};

const ContextMenuItem: FC<{
context: API_HashEntry;
state: Addon_TestProviderState<{
testResults: TestResult[];
}>;
ListItem: typeof ListItem;
}> = ({ context, state, ListItem }) => {
const api = useStorybookApi();

const onClick = useCallback(
(event: SyntheticEvent) => {
event.stopPropagation();
// TODO - actually send along a sub-set based on `context` to test.
api.getChannel().emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: TEST_PROVIDER_ID });
},
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
[api]
);

const theme = useTheme();

return (
<ListItem
title={'Component tests'}
right={
<Button variant="ghost" padding="small" disabled={state.crashed || state.running}>
<PlayIcon fill={theme.barTextColor} />
</Button>
}
center={state.running ? 'Running...' : 'Run tests'}
onClick={onClick}
/>
);
};

addons.register(ADDON_ID, (api) => {
const storybookBuilder = (globalThis as any).STORYBOOK_BUILDER || '';
if (storybookBuilder.includes('vite')) {
Expand All @@ -93,6 +145,13 @@ addons.register(ADDON_ID, (api) => {
watchable: true,

name: 'Component tests',
contextMenu: ({ context, state }, { ListItem }) => {
if (context.type === 'docs') {
JReinhold marked this conversation as resolved.
Show resolved Hide resolved
return null;
}

return <ContextMenuItem context={context} state={state} ListItem={ListItem} />;
},
title: ({ crashed, failed }) =>
crashed || failed ? 'Component tests failed' : 'Component tests',
description: ({ failed, running, watching, progress, crashed, error }) => {
Expand Down Expand Up @@ -181,20 +240,20 @@ addons.register(ADDON_ID, (api) => {
}>);
}

const filter = ({ state }: Combo) => {
return {
storyId: state.storyId,
};
};

addons.add(PANEL_ID, {
type: types.PANEL,
title: Title,
match: ({ viewMode }) => viewMode === 'story',
render: ({ active }) => {
const newLocal = useCallback(({ state }: Combo) => {
return {
storyId: state.storyId,
};
}, []);

return (
<AddonPanel active={active}>
<Consumer filter={newLocal}>{({ storyId }) => <Panel storyId={storyId} />}</Consumer>
<Consumer filter={filter}>{({ storyId }) => <Panel storyId={storyId} />}</Consumer>
</AddonPanel>
);
},
Expand Down
32 changes: 25 additions & 7 deletions code/core/src/components/components/tooltip/TooltipLinkList.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ComponentProps, SyntheticEvent } from 'react';
import type { ComponentProps, ReactNode, SyntheticEvent } from 'react';
import React, { useCallback } from 'react';

import { styled } from '@storybook/core/theming';
Expand All @@ -15,7 +15,8 @@ const List = styled.div(
},
({ theme }) => ({
borderRadius: theme.appBorderRadius + 2,
})
}),
({ theme }) => (theme.base === 'dark' ? { background: theme.background.content } : {})
);

const Group = styled.div(({ theme }) => ({
Expand All @@ -25,15 +26,27 @@ const Group = styled.div(({ theme }) => ({
},
}));

export interface Link extends Omit<ListItemProps, 'onClick'> {
export interface NormalLink extends Omit<ListItemProps, 'onClick'> {
id: string;
onClick?: (
event: SyntheticEvent,
item: Pick<ListItemProps, 'id' | 'active' | 'disabled' | 'title' | 'href'>
) => void;
}

interface ItemProps extends Link {
export type Link = CustomLink | NormalLink;

/**
* This is a custom link that can be used in the `TooltipLinkList` component. It allows for custom
* content to be rendered in the list; it does not have to be a link.
*/
interface CustomLink {
id: string;
icon?: any;
content: ReactNode;
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
}

interface ItemProps extends NormalLink {
isIndented?: boolean;
}

Expand Down Expand Up @@ -63,9 +76,14 @@ export const TooltipLinkList = ({ links, LinkWrapper, ...props }: TooltipLinkLis
.map((group, index) => {
return (
<Group key={group.map((link) => link.id).join(`~${index}~`)}>
{group.map((link) => (
<Item key={link.id} isIndented={isIndented} LinkWrapper={LinkWrapper} {...link} />
))}
{group.map((link) => {
if ('content' in link) {
return link.content;
}
return (
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
<Item key={link.id} isIndented={isIndented} LinkWrapper={LinkWrapper} {...link} />
);
})}
</Group>
);
})}
Expand Down
4 changes: 2 additions & 2 deletions code/core/src/components/components/tooltip/WithTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ const WithTooltipPure = ({
}
);

const tooltipComponent = (
const tooltipComponent = isVisible ? (
<Tooltip
placement={state?.placement}
ref={setTooltipRef}
Expand All @@ -133,7 +133,7 @@ const WithTooltipPure = ({
{/* @ts-expect-error (non strict) */}
{typeof tooltip === 'function' ? tooltip({ onHide: () => onVisibleChange(false) }) : tooltip}
</Tooltip>
);
) : null;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an optimization that makes a lot of sense to me...
Why call a function which we will not use the returned JSX of?


return (
<>
Expand Down
8 changes: 4 additions & 4 deletions code/core/src/core-events/data/testing-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ export type TestProviderState = Addon_TestProviderState;
export type TestProviders = Record<TestProviderId, TestProviderConfig & TestProviderState>;

export type TestingModuleRunRequestStories = {
id: string;
name: string;
id: string; // button--primary
name: string; // Primary
};

export type TestingModuleRunRequestPayload = {
providerId: TestProviderId;
payload: {
stories: TestingModuleRunRequestStories[];
importPath: string;
componentPath: string;
importPath: string; // ./.../button.stories.tsx
componentPath: string; // ./.../button.tsx
}[];
};

Expand Down
1 change: 1 addition & 0 deletions code/core/src/manager-api/modules/addons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface SubAPI {
| Addon_Types
| Addon_TypesEnum.experimental_PAGE
| Addon_TypesEnum.experimental_SIDEBAR_BOTTOM
| Addon_TypesEnum.experimental_TEST_PROVIDER
| Addon_TypesEnum.experimental_SIDEBAR_TOP = Addon_Types,
>(
type: T
Expand Down
100 changes: 100 additions & 0 deletions code/core/src/manager-api/modules/experimental_testmodule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Addon_TypesEnum } from '@storybook/core/types';

import {
TESTING_MODULE_CANCEL_TEST_RUN_REQUEST,
TESTING_MODULE_RUN_ALL_REQUEST,
type TestProviderId,
type TestProviderState,
type TestProviders,
} from '@storybook/core/core-events';

import type { ModuleFn } from '../lib/types';

export type SubState = {
testProviders: TestProviders;
};

const STORAGE_KEY = '@storybook/manager/test-providers';

const initialTestProviderState: TestProviderState = {
details: {} as { [key: string]: any },
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
cancellable: false,
cancelling: false,
running: false,
watching: false,
failed: false,
crashed: false,
};

export type SubAPI = {
getTestproviderState(id: string): TestProviderState | undefined;
updateTestproviderState(id: TestProviderId, update: Partial<TestProviderState>): void;
clearTestproviderState(id: TestProviderId): void;
runTestprovider(id: TestProviderId): void;
cancelTestprovider(id: TestProviderId): void;
};

export const init: ModuleFn = ({ store, fullAPI }) => {
let sessionState: TestProviders = {};
try {
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
sessionState = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}');
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
} catch (_) {
//
}

const state: SubState = {
testProviders: sessionState,
};

const api: SubAPI = {
getTestproviderState(id) {
const { testProviders } = store.getState();

return testProviders?.[id];
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Potential runtime error if testProviders[id] is undefined when spreading

updateTestproviderState(id, update) {
return store.setState(
({ testProviders }) => {
return { testProviders: { ...testProviders, [id]: { ...testProviders[id], ...update } } };
},
{ persistence: 'session' }
);
},
clearTestproviderState(id) {
const update = {
cancelling: false,
running: true,
failed: false,
crashed: false,
progress: undefined,
};
return store.setState(
({ testProviders }) => {
return { testProviders: { ...testProviders, [id]: { ...testProviders[id], ...update } } };
},
{ persistence: 'session' }
);
},
runTestprovider(id) {
fullAPI.emit(TESTING_MODULE_RUN_ALL_REQUEST, { providerId: id });

return () => api.cancelTestprovider(id);
},
cancelTestprovider(id) {
api.updateTestproviderState(id, { cancelling: true });
fullAPI.emit(TESTING_MODULE_CANCEL_TEST_RUN_REQUEST, { providerId: id });
},
};

const initModule = async () => {
const initialState = Object.fromEntries(
Object.entries(fullAPI.getElements(Addon_TypesEnum.experimental_TEST_PROVIDER)).map(
([id, config]) => [id, { ...config, ...initialTestProviderState, ...sessionState[id] }]
)
);

store.setState({ testProviders: initialState }, { persistence: 'session' });
};

return { init: initModule, state, api };
};
4 changes: 4 additions & 0 deletions code/core/src/manager-api/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { noArrayMerge } from './lib/merge';
import type { ModuleFn } from './lib/types';
import * as addons from './modules/addons';
import * as channel from './modules/channel';
import * as testproviders from './modules/experimental_testmodule';
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
import * as globals from './modules/globals';
import * as layout from './modules/layout';
import * as notifications from './modules/notifications';
Expand Down Expand Up @@ -79,6 +80,7 @@ export type State = layout.SubState &
stories.SubState &
refs.SubState &
notifications.SubState &
testproviders.SubState &
version.SubState &
url.SubState &
shortcuts.SubState &
Expand All @@ -98,6 +100,7 @@ export type API = addons.SubAPI &
globals.SubAPI &
layout.SubAPI &
notifications.SubAPI &
testproviders.SubAPI &
shortcuts.SubAPI &
settings.SubAPI &
version.SubAPI &
Expand Down Expand Up @@ -178,6 +181,7 @@ class ManagerProvider extends Component<ManagerProviderProps, State> {
addons,
layout,
notifications,
testproviders,
settings,
shortcuts,
stories,
Expand Down
13 changes: 12 additions & 1 deletion code/core/src/manager/components/sidebar/Refs.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React from 'react';

import { fn } from '@storybook/test';

import { ManagerContext } from '@storybook/core/manager-api';

import { standardData as standardHeaderData } from './Heading.stories';
Expand All @@ -8,6 +10,15 @@ import { Ref } from './Refs';
import { mockDataset } from './mockdata';
import type { RefType } from './types';

const managerContext = {
state: { docsOptions: {}, testProviders: {} },
api: {
on: fn().mockName('api::on'),
off: fn().mockName('api::off'),
getElements: fn(() => ({})),
},
} as any;
ndelangen marked this conversation as resolved.
Show resolved Hide resolved

export default {
component: Ref,
title: 'Sidebar/Refs',
Expand All @@ -16,7 +27,7 @@ export default {
globals: { sb_theme: 'side-by-side' },
decorators: [
(storyFn: any) => (
<ManagerContext.Provider value={{ state: { docsOptions: {} } } as any}>
<ManagerContext.Provider value={managerContext}>
<IconSymbols />
{storyFn()}
</ManagerContext.Provider>
Expand Down
Loading
Loading