diff --git a/lib/api/src/modules/notifications.ts b/lib/api/src/modules/notifications.ts index fabcc0312d7..e3325151670 100644 --- a/lib/api/src/modules/notifications.ts +++ b/lib/api/src/modules/notifications.ts @@ -2,6 +2,8 @@ import { Module } from '../index'; export interface Notification { id: string; + link: string; + content: string; onClear?: () => void; } diff --git a/lib/components/src/tabs/tabs.tsx b/lib/components/src/tabs/tabs.tsx index 089896af258..b4d55bfd5c9 100644 --- a/lib/components/src/tabs/tabs.tsx +++ b/lib/components/src/tabs/tabs.tsx @@ -139,7 +139,7 @@ export interface TabsProps { selected?: string; actions?: { onSelect: (id: string) => void; - }; + } & Record; backgroundColor?: string; absolute?: boolean; bordered?: boolean; diff --git a/lib/ui/src/components/notifications/item.js b/lib/ui/src/components/notifications/item.tsx similarity index 62% rename from lib/ui/src/components/notifications/item.js rename to lib/ui/src/components/notifications/item.tsx index e891f389992..462f76263ae 100644 --- a/lib/ui/src/components/notifications/item.js +++ b/lib/ui/src/components/notifications/item.tsx @@ -1,9 +1,9 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { FunctionComponent } from 'react'; +import { State } from '@storybook/api'; import { styled, lighten, darken } from '@storybook/theming'; import { Link } from '@storybook/router'; -const baseStyle = ({ theme }) => ({ +const Notification = styled.div(({ theme }) => ({ display: 'block', padding: '16px 20px', borderRadius: 10, @@ -15,26 +15,21 @@ const baseStyle = ({ theme }) => ({ backgroundColor: theme.base === 'light' ? darken(theme.background.app) : lighten(theme.background.app), textDecoration: 'none', -}); - -const NotificationLink = styled(Link)(baseStyle); -const Notification = styled.div(baseStyle); +})); +const NotificationLink = Notification.withComponent(Link); export const NotificationItemSpacer = styled.div({ height: 48, }); -export default function NotificationItem({ notification: { content, link } }) { +const NotificationItem: FunctionComponent<{ + notification: State['notifications'][0]; +}> = ({ notification: { content, link } }) => { return link ? ( {content} ) : ( {content} ); -} - -NotificationItem.propTypes = { - notification: PropTypes.shape({ - content: PropTypes.string.isRequired, - link: PropTypes.string, - }).isRequired, }; + +export default NotificationItem; diff --git a/lib/ui/src/components/notifications/notifications.js b/lib/ui/src/components/notifications/notifications.js deleted file mode 100644 index 973923beb66..00000000000 --- a/lib/ui/src/components/notifications/notifications.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { styled } from '@storybook/theming'; - -import NotificationItem from './item'; - -const List = styled.div( - { - zIndex: 10, - - '> * + *': { - marginTop: 10, - }, - '&:empty': { - display: 'none', - }, - }, - ({ placement }) => - placement || { - bottom: 0, - left: 0, - right: 0, - position: 'fixed', - } -); - -export default function NotificationList({ notifications, placement }) { - return ( - - {notifications.map(notification => ( - - ))} - - ); -} - -NotificationList.propTypes = { - placement: PropTypes.shape({ - position: PropTypes.string, - left: PropTypes.number, - right: PropTypes.number, - top: PropTypes.number, - bottom: PropTypes.number, - }), - notifications: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string, - content: PropTypes.string.isRequired, - link: PropTypes.string, - }).isRequired - ).isRequired, -}; -NotificationList.defaultProps = { - placement: undefined, -}; diff --git a/lib/ui/src/components/notifications/notifications.tsx b/lib/ui/src/components/notifications/notifications.tsx new file mode 100644 index 00000000000..3f189b0b0cc --- /dev/null +++ b/lib/ui/src/components/notifications/notifications.tsx @@ -0,0 +1,40 @@ +import React, { FunctionComponent } from 'react'; +import { State } from '@storybook/api'; +import { styled, CSSObject } from '@storybook/theming'; + +import NotificationItem from './item'; + +const List = styled.div<{ placement?: CSSObject }>( + { + zIndex: 10, + + '> * + *': { + marginTop: 10, + }, + '&:empty': { + display: 'none', + }, + }, + ({ placement }) => + placement || { + bottom: 0, + left: 0, + right: 0, + position: 'fixed', + } +); + +const NotificationList: FunctionComponent<{ + placement: CSSObject; + notifications: State['notifications']; +}> = ({ notifications, placement = undefined }) => { + return ( + + {notifications.map(notification => ( + + ))} + + ); +}; + +export default NotificationList; diff --git a/lib/ui/src/components/panel/panel.js b/lib/ui/src/components/panel/panel.js deleted file mode 100644 index 819cc5717e4..00000000000 --- a/lib/ui/src/components/panel/panel.js +++ /dev/null @@ -1,99 +0,0 @@ -import React, { Component, Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { styled } from '@storybook/theming'; -import { Tabs, Icons, IconButton } from '@storybook/components'; - -const DesktopOnlyIconButton = styled(IconButton)({ - // Hides full screen icon at mobile breakpoint defined in app.js - '@media (max-width: 599px)': { - display: 'none', - }, -}); - -class SafeTab extends Component { - constructor(props) { - super(props); - this.state = { hasError: false }; - } - - componentDidCatch(error, info) { - this.setState({ hasError: true }); - // eslint-disable-next-line no-console - console.error(error, info); - } - - render() { - const { hasError } = this.state; - const { children, title, id } = this.props; - if (hasError) { - return

Something went wrong.

; - } - return ( -
- {children} -
- ); - } -} -SafeTab.propTypes = { - children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]), - title: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, -}; -SafeTab.defaultProps = { - children: null, -}; - -const AddonPanel = React.memo( - ({ panels, actions, selectedPanel, panelPosition, absolute = true }) => ( - - - - - - - - - } - id="storybook-panel-root" - > - {Object.entries(panels).map(([k, v]) => ( - - {v.render} - - ))} - - ) -); -AddonPanel.displayName = 'AddonPanel'; -AddonPanel.propTypes = { - selectedPanel: PropTypes.string, - actions: PropTypes.shape({ - togglePosition: PropTypes.func, - toggleVisibility: PropTypes.func, - }).isRequired, - panels: PropTypes.shape({}).isRequired, - panelPosition: PropTypes.oneOf(['bottom', 'right']), - absolute: PropTypes.bool, -}; -AddonPanel.defaultProps = { - selectedPanel: null, - panelPosition: 'right', - absolute: true, -}; - -export default AddonPanel; diff --git a/lib/ui/src/components/panel/panel.tsx b/lib/ui/src/components/panel/panel.tsx new file mode 100644 index 00000000000..8a43ca8749d --- /dev/null +++ b/lib/ui/src/components/panel/panel.tsx @@ -0,0 +1,83 @@ +import React, { Component, Fragment } from 'react'; +import { styled } from '@storybook/theming'; +import { Tabs, Icons, IconButton } from '@storybook/components'; + +const DesktopOnlyIconButton = styled(IconButton)({ + // Hides full screen icon at mobile breakpoint defined in app.js + '@media (max-width: 599px)': { + display: 'none', + }, +}); + +interface SafeTabProps { + title: string; + id: string; +} + +class SafeTab extends Component { + constructor(props: SafeTabProps) { + super(props); + this.state = { hasError: false }; + } + + componentDidCatch(error: Error, info: any) { + this.setState({ hasError: true }); + // eslint-disable-next-line no-console + console.error(error, info); + } + + render() { + const { hasError } = this.state; + const { children, title, id } = this.props; + if (hasError) { + return

Something went wrong.

; + } + return ( +
+ {children} +
+ ); + } +} + +const AddonPanel = React.memo<{ + selectedPanel: string; + actions: { onSelect: (id: string) => void } & Record; + panels: Record; + panelPosition: 'bottom' | 'right'; + absolute: boolean; +}>(({ panels, actions, selectedPanel = null, panelPosition = 'right', absolute = true }) => ( + + + + + + + + + } + id="storybook-panel-root" + > + {Object.entries(panels).map(([k, v]) => ( + + {v.render} + + ))} + +)); +AddonPanel.displayName = 'AddonPanel'; + +export default AddonPanel; diff --git a/lib/ui/src/containers/nav.js b/lib/ui/src/containers/nav.tsx similarity index 86% rename from lib/ui/src/containers/nav.js rename to lib/ui/src/containers/nav.tsx index 0989c6ac84d..a743efa9a65 100755 --- a/lib/ui/src/containers/nav.js +++ b/lib/ui/src/containers/nav.tsx @@ -1,10 +1,11 @@ import { DOCS_MODE } from 'global'; -import React from 'react'; +import React, { FunctionComponent } from 'react'; import memoize from 'memoizerific'; import { Badge } from '@storybook/components'; -import { Consumer } from '@storybook/api'; +import { Consumer, Combo } from '@storybook/api'; +import { StoriesHash } from '@storybook/api/dist/modules/stories'; import { shortcutToHumanString } from '../libs/shortcut'; import ListItemIcon from '../components/sidebar/ListItemIcon'; @@ -16,7 +17,7 @@ const focusableUIElements = { storyPanelRoot: 'storybook-panel-root', }; -const shortcutToHumanStringIfEnabled = (shortcuts, enableShortcuts) => +const shortcutToHumanStringIfEnabled = (shortcuts: string[], enableShortcuts: boolean) => enableShortcuts ? shortcutToHumanString(shortcuts) : null; const createMenu = memoize(1)( @@ -108,9 +109,9 @@ const createMenu = memoize(1)( ] ); -export const collapseAllStories = stories => { +export const collapseAllStories = (stories: StoriesHash) => { // keep track of component IDs that have been rewritten to the ID of their first leaf child - const componentIdToLeafId = {}; + const componentIdToLeafId: Record = {}; // 1) remove all leaves const leavesRemoved = Object.values(stories).filter( @@ -126,8 +127,8 @@ export const collapseAllStories = stories => { return item; } - const nonLeafChildren = []; - const leafChildren = []; + const nonLeafChildren: string[] = []; + const leafChildren: string[] = []; children.forEach(child => (stories[child].isLeaf ? leafChildren : nonLeafChildren).push(child)); if (leafChildren.length === 0) { @@ -135,7 +136,13 @@ export const collapseAllStories = stories => { } const leafId = leafChildren[0]; - const component = { ...rest, id: leafId, isLeaf: true, isComponent: true }; + const component = { + ...rest, + id: leafId, + isLeaf: true, + isComponent: true, + children: [] as string[], + }; componentIdToLeafId[id] = leafId; // this is a component, so it should not have any non-leaf children @@ -160,16 +167,16 @@ export const collapseAllStories = stories => { return { children: rewritten, ...rest }; }); - const result = {}; + const result = {} as StoriesHash; childrenRewritten.forEach(item => { result[item.id] = item; }); return result; }; -export const collapseDocsOnlyStories = storiesHash => { +export const collapseDocsOnlyStories = (storiesHash: StoriesHash) => { // keep track of component IDs that have been rewritten to the ID of their first leaf child - const componentIdToLeafId = {}; + const componentIdToLeafId: Record = {}; const docsOnlyStoriesRemoved = Object.values(storiesHash).filter(item => { if (item.isLeaf && item.parameters && item.parameters.docsOnly) { componentIdToLeafId[item.parent] = item.id; @@ -187,7 +194,7 @@ export const collapseDocsOnlyStories = storiesHash => { ...item, id: leafId, isLeaf: true, - children: undefined, + children: [] as string[], }; return collapsed; } @@ -203,14 +210,14 @@ export const collapseDocsOnlyStories = storiesHash => { return item; }); - const result = {}; + const result = {} as StoriesHash; docsOnlyComponentsCollapsed.forEach(item => { result[item.id] = item; }); return result; }; -export const mapper = ({ state, api }) => { +export const mapper = ({ state, api }: Combo) => { const { ui: { name, url, enableShortcuts }, viewMode, @@ -236,6 +243,10 @@ export const mapper = ({ state, api }) => { }; }; -export default props => ( - {fromState => } +const Nav: FunctionComponent = props => ( + + {(fromState: ReturnType) => } + ); + +export default Nav; diff --git a/lib/ui/src/containers/notifications.js b/lib/ui/src/containers/notifications.js deleted file mode 100644 index afaef819751..00000000000 --- a/lib/ui/src/containers/notifications.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; - -import { Consumer } from '@storybook/api'; - -import Notifications from '../components/notifications/notifications'; - -export const mapper = ({ state }) => { - const { notifications } = state; - - return { - notifications, - }; -}; - -const NotificationConnect = props => ( - {fromState => } -); - -export default NotificationConnect; diff --git a/lib/ui/src/containers/notifications.tsx b/lib/ui/src/containers/notifications.tsx new file mode 100644 index 00000000000..227c04deb56 --- /dev/null +++ b/lib/ui/src/containers/notifications.tsx @@ -0,0 +1,21 @@ +import React, { FunctionComponent } from 'react'; + +import { Consumer, Combo } from '@storybook/api'; + +import Notifications from '../components/notifications/notifications'; + +export const mapper = ({ state }: Combo) => { + const { notifications } = state; + + return { + notifications, + }; +}; + +const NotificationConnect: FunctionComponent = props => ( + + {(fromState: ReturnType) => } + +); + +export default NotificationConnect; diff --git a/lib/ui/src/containers/panel.js b/lib/ui/src/containers/panel.js deleted file mode 100644 index 88821840822..00000000000 --- a/lib/ui/src/containers/panel.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import memoize from 'memoizerific'; -import { Consumer } from '@storybook/api'; - -import AddonPanel from '../components/panel/panel'; - -const createPanelActions = memoize(1)(api => ({ - onSelect: panel => api.setSelectedPanel(panel), - toggleVisibility: () => api.togglePanel(), - togglePosition: () => api.togglePanelPosition(), -})); - -const mapper = ({ state, api }) => ({ - panels: api.getStoryPanels(), - selectedPanel: api.getSelectedPanel(), - panelPosition: state.layout.panelPosition, - actions: createPanelActions(api), -}); - -export default props => ( - {customProps => } -); diff --git a/lib/ui/src/containers/panel.tsx b/lib/ui/src/containers/panel.tsx new file mode 100644 index 00000000000..2ca96d5b332 --- /dev/null +++ b/lib/ui/src/containers/panel.tsx @@ -0,0 +1,26 @@ +import React, { FunctionComponent } from 'react'; +import memoize from 'memoizerific'; +import { Consumer, Combo } from '@storybook/api'; + +import AddonPanel from '../components/panel/panel'; + +const createPanelActions = memoize(1)(api => ({ + onSelect: (panel: string) => api.setSelectedPanel(panel), + toggleVisibility: () => api.togglePanel(), + togglePosition: () => api.togglePanelPosition(), +})); + +const mapper = ({ state, api }: Combo) => ({ + panels: api.getStoryPanels(), + selectedPanel: api.getSelectedPanel(), + panelPosition: state.layout.panelPosition, + actions: createPanelActions(api), +}); + +const Panel: FunctionComponent = props => ( + + {(customProps: ReturnType) => } + +); + +export default Panel; diff --git a/lib/ui/src/containers/preview.js b/lib/ui/src/containers/preview.tsx similarity index 63% rename from lib/ui/src/containers/preview.js rename to lib/ui/src/containers/preview.tsx index e2c472045e1..e659bf9affa 100644 --- a/lib/ui/src/containers/preview.js +++ b/lib/ui/src/containers/preview.tsx @@ -1,21 +1,31 @@ import { PREVIEW_URL } from 'global'; import React from 'react'; -import { Consumer } from '@storybook/api'; +import { Consumer, Combo } from '@storybook/api'; +import { StoriesHash } from '@storybook/api/dist/modules/stories'; import { Preview } from '../components/preview/preview'; const nonAlphanumSpace = /[^a-z0-9 ]/gi; const doubleSpace = /\s\s/gi; -const replacer = match => ` ${match} `; -const addExtraWhiteSpace = input => +const replacer = (match: string) => ` ${match} `; + +const addExtraWhiteSpace = (input: string) => input.replace(nonAlphanumSpace, replacer).replace(doubleSpace, ' '); -const getDescription = (storiesHash, storyId) => { + +const getDescription = (storiesHash: StoriesHash, storyId: string) => { const storyInfo = storiesHash[storyId]; - return storyInfo ? addExtraWhiteSpace(`${storyInfo.kind} - ${storyInfo.name}`) : ''; + + if (storyInfo) { + // @ts-ignore + const { kind, name } = storyInfo; + return kind && name ? addExtraWhiteSpace(`${kind} - ${name}`) : ''; + } + + return ''; }; -const mapper = ({ api, state }) => { +const mapper = ({ api, state }: Combo) => { const { layout, location, customQueryParams, storiesHash, storyId } = state; const { parameters } = storiesHash[storyId] || {}; return { @@ -25,13 +35,13 @@ const mapper = ({ api, state }) => { description: getDescription(storiesHash, storyId), ...api.getUrlState(), queryParams: customQueryParams, - docsOnly: parameters && parameters.docsOnly, + docsOnly: (parameters && parameters.docsOnly) as boolean, location, parameters, }; }; -function getBaseUrl() { +function getBaseUrl(): string { try { return PREVIEW_URL || 'iframe.html'; } catch (e) { @@ -41,7 +51,7 @@ function getBaseUrl() { const PreviewConnected = React.memo(props => ( - {fromState => ( + {(fromState: ReturnType) => (