From d835929bc8e84a37739d218b857042694bbf058d Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Mon, 6 Aug 2018 23:49:30 +0200 Subject: [PATCH 001/413] WIP --- lib/ui/package.json | 4 +- lib/ui/src/app.js | 122 ++++++++++++++++++ lib/ui/src/index.js | 46 ++----- .../src/modules/ui/containers/addon_panel.js | 30 +++-- lib/ui/src/modules/ui/containers/layout.js | 20 ++- .../src/modules/ui/containers/routed_link.js | 53 ++++++-- .../src/modules/ui/containers/search_box.js | 47 ++++--- .../modules/ui/containers/shortcuts_help.js | 28 ++-- .../modules/ui/containers/stories_panel.js | 42 ++++-- lib/ui/src/provider.js | 9 ++ lib/ui/src/state.js | 4 + lib/ui/src/store.js | 33 +++++ yarn.lock | 61 ++++++++- 13 files changed, 388 insertions(+), 111 deletions(-) create mode 100644 lib/ui/src/app.js create mode 100644 lib/ui/src/provider.js create mode 100644 lib/ui/src/state.js create mode 100644 lib/ui/src/store.js diff --git a/lib/ui/package.json b/lib/ui/package.json index 3547cfc421d5..fc41c68ffb04 100644 --- a/lib/ui/package.json +++ b/lib/ui/package.json @@ -33,12 +33,14 @@ "lodash.pick": "^4.4.0", "lodash.sortby": "^4.7.0", "lodash.throttle": "^4.1.1", + "mobx": "^5.0.3", "prop-types": "^15.6.2", "qs": "^6.5.2", "react-emotion": "^9.2.6", "react-fuzzy": "^0.5.2", "react-lifecycles-compat": "^3.0.4", - "react-modal": "^3.5.1" + "react-modal": "^3.5.1", + "react-router-dom": "^4.3.1" }, "devDependencies": { "@storybook/addon-actions": "4.0.0-alpha.16", diff --git a/lib/ui/src/app.js b/lib/ui/src/app.js new file mode 100644 index 000000000000..fc280fcb0540 --- /dev/null +++ b/lib/ui/src/app.js @@ -0,0 +1,122 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { BrowserRouter, Route } from 'react-router-dom'; + +import { Provider } from './state'; + +import Layout from './modules/ui/containers/layout'; +import StoriesPanel from './modules/ui/containers/stories_panel'; +import AddonPanel from './modules/ui/containers/addon_panel'; +import ShortcutsHelp from './modules/ui/containers/shortcuts_help'; +import SearchBox from './modules/ui/containers/search_box'; + +function ensureKind(storyKinds, selectedKind) { + if (!storyKinds) return selectedKind; + + const found = storyKinds.find(item => item.kind === selectedKind); + if (found) return found.kind; + // if the selected kind is non-existant, select the first kind + const kinds = storyKinds.map(item => item.kind); + return kinds[0]; +} + +function ensureStory(storyKinds, selectedKind, selectedStory) { + if (!storyKinds) return selectedStory; + + const kindInfo = storyKinds.find(item => item.kind === selectedKind); + if (!kindInfo) return null; + + const found = kindInfo.stories.find(item => item === selectedStory); + if (found) return found; + + // if the selected story is non-existant, select the first story + return kindInfo.stories[0]; +} + +/* eslint-disable */ +class App extends React.Component { + static propTypes = { + provider: PropTypes.shape({ + renderPreview: PropTypes.func.isRequired, + }).isRequired, + }; + + state = { + showShortcutsHelp: false, + storyFilter: null, + selectedAddonPanel: null, + isMobileDevice: false, + shortcutOptions: { + goFullScreen: false, + showStoriesPanel: true, + showAddonPanel: true, + showSearchBox: false, + addonPanelInRight: false, + enableShortcuts: true, + }, + uiOptions: { + name: 'STORYBOOK', + url: 'https://github.com/storybooks/storybook', + sortStoriesByKind: false, + hierarchySeparator: '/', + hierarchyRootSeparator: null, + sidebarAnimations: true, + theme: null, + }, + updateState: this.updateState, + }; + + componentDidMount() { + const { provider } = this.props; + + provider.handleAPI({ + onStory: (...args) => {}, + setStories: this.setStories, + selectStory: (...args) => {}, + handleShortcut(...args) {}, + }); + } + + setStories = stories => { + const selectedKind = ensureKind(stories, this.state.selectedKind); + const currentSelectedStory = + this.state.selectedKind === selectedKind ? this.state.selectedStory : null; + const selectedStory = ensureStory(stories, selectedKind, currentSelectedStory); + + this.setState({ + stories, + selectedStory, + selectedKind, + }); + }; + + updateState = (...args) => this.setState(...args); + + render() { + const { provider } = this.props; + const { selectedKind, selectedStory } = this.state; + + const Preview = () => provider.renderPreview(selectedKind, selectedStory); + + return ( + + + ( + } + storiesPanel={() => } + addonPanel={() => } + shortcutsHelp={() => } + searchBox={() => } + /> + )} + /> + + + ); + } +} + +export default App; diff --git a/lib/ui/src/index.js b/lib/ui/src/index.js index 10c6816bef39..38fd064bc0c5 100644 --- a/lib/ui/src/index.js +++ b/lib/ui/src/index.js @@ -1,44 +1,16 @@ -import { createApp } from '@storybook/mantra-core'; -import Podda from '@storybook/podda'; +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './app'; -import buildContext from './context'; -import shortcutsModule from './modules/shortcuts'; -import apiModule from './modules/api'; -import uiModule from './modules/ui'; -import { setContext, setActions } from './compose'; +import Provider from './provider'; -export class Provider { - renderPreview() { - throw new Error('Provider.renderPreview() is not implemented!'); - } - - handleAPI() { - throw new Error('Provider.handleAPI() is not implemented!'); - } -} - -export default function(domNode, provider) { +function renderStorybokUI(domNode, provider) { if (!(provider instanceof Provider)) { throw new Error('provider is not extended from the base Provider'); } - const defaultState = { - ...shortcutsModule.defaultState, - ...apiModule.defaultState, - ...uiModule.defaultState, - }; - const clientStore = new Podda(defaultState); - clientStore.registerAPI('toggle', (store, key) => store.set(key, !store.get(key))); - - const context = buildContext(clientStore, domNode, provider); - const app = createApp(context); - - app.loadModule(shortcutsModule); - app.loadModule(apiModule); - app.loadModule(uiModule); - - setContext(context); - setActions(app._bindContext(app.actions)); // eslint-disable-line - - app.init(); + ReactDOM.render(, domNode); } + +export { Provider }; +export default renderStorybokUI; diff --git a/lib/ui/src/modules/ui/containers/addon_panel.js b/lib/ui/src/modules/ui/containers/addon_panel.js index 32c20cc48224..da692776403f 100644 --- a/lib/ui/src/modules/ui/containers/addon_panel.js +++ b/lib/ui/src/modules/ui/containers/addon_panel.js @@ -1,20 +1,30 @@ +import React from 'react'; import { AddonPanel } from '@storybook/components'; -import genPoddaLoader from '../libs/gen_podda_loader'; -import compose from '../../../compose'; +import { Consumer } from '../../../state'; -export function mapper(state, props, { context, actions }) { - const panels = context().provider.getPanels(); - const actionMap = actions(); +export function mapper(state, { panels }) { const selectedPanel = state.selectedAddonPanel; + console.log(panels); + return { panels, selectedPanel, - onPanelSelect: actionMap.ui.selectAddonPanel, + onPanelSelect: (...args) => { + console.log('onPanelSelect', args); + }, }; } -export default compose( - genPoddaLoader(mapper), - { withRef: false } -)(AddonPanel); +export default props => ( + + {state => { + const finalProps = { + ...props, + ...mapper(state, props), + }; + + return ; + }} + +); diff --git a/lib/ui/src/modules/ui/containers/layout.js b/lib/ui/src/modules/ui/containers/layout.js index 4e73a45f5d57..01c04c73e4dd 100755 --- a/lib/ui/src/modules/ui/containers/layout.js +++ b/lib/ui/src/modules/ui/containers/layout.js @@ -1,7 +1,7 @@ +import React from 'react'; import pick from 'lodash.pick'; import { Layout } from '@storybook/components'; -import genPoddaLoader from '../libs/gen_podda_loader'; -import compose from '../../../compose'; +import { Consumer } from '../../../state'; export const mapper = state => { const { shortcutOptions, isMobileDevice, uiOptions } = state; @@ -20,7 +20,15 @@ export const mapper = state => { }; }; -export default compose( - genPoddaLoader(mapper), - { withRef: false } -)(Layout); +export default props => ( + + {state => { + const finalProps = { + ...props, + ...mapper(state), + }; + + return ; + }} + +); diff --git a/lib/ui/src/modules/ui/containers/routed_link.js b/lib/ui/src/modules/ui/containers/routed_link.js index a13795924f62..a292b3d11570 100644 --- a/lib/ui/src/modules/ui/containers/routed_link.js +++ b/lib/ui/src/modules/ui/containers/routed_link.js @@ -1,24 +1,55 @@ import { RoutedLink, MenuLink } from '@storybook/components'; -import genPoddaLoader from '../libs/gen_podda_loader'; -import { getUrlState } from '../configs/handle_routing'; -import compose from '../../../compose'; +import React from 'react'; -export function mapper(state, props) { - const { url } = getUrlState({ ...state, ...props.overrideParams }); +import { Consumer } from '../../../state'; + +// import genPoddaLoader from '../libs/gen_podda_loader'; +// import { getUrlState } from '../configs/handle_routing'; +// import compose from '../../../compose'; + +export function mapper() { + // const { url } = getUrlState({ ...state, ...props.overrideParams }); return { - href: url, + // href: url, + href: '', }; } -const composer = compose( - genPoddaLoader(mapper), - { withRef: false } +// const composer = compose( +// genPoddaLoader(mapper), +// { withRef: false } +// ); + +const ComposedMenuLink = props => ( + + {state => { + const finalProps = { + ...props, + ...mapper(state), + }; + + return ; + }} + +); + +const ComposedRoutedLink = props => ( + + {state => { + const finalProps = { + ...props, + ...mapper(state), + }; + + return ; + }} + ); -const ComposedMenuLink = composer(MenuLink); +// const ComposedMenuLink = composer(MenuLink); export { ComposedMenuLink as MenuLink }; -const ComposedRoutedLink = composer(RoutedLink); +// const ComposedRoutedLink = composer(RoutedLink); export { ComposedRoutedLink as RoutedLink }; diff --git a/lib/ui/src/modules/ui/containers/search_box.js b/lib/ui/src/modules/ui/containers/search_box.js index 2d03772323c4..5da1ccf144af 100644 --- a/lib/ui/src/modules/ui/containers/search_box.js +++ b/lib/ui/src/modules/ui/containers/search_box.js @@ -1,22 +1,31 @@ +import React from 'react'; import SearchBoxComponent from '../components/search_box'; -import genPoddaLoader from '../libs/gen_podda_loader'; -import compose from '../../../compose'; -export const mapper = (state, props, { actions }) => { - const actionMap = actions(); - return { - showSearchBox: state.shortcutOptions.showSearchBox, - stories: state.stories, - onSelectStory: actionMap.api.selectStory, - onClose() { - actionMap.shortcuts.setOptions({ - showSearchBox: false, - }); - }, - }; -}; +import { Consumer } from '../../../state'; -export default compose( - genPoddaLoader(mapper), - { withRef: false } -)(SearchBoxComponent); +export const mapper = state => ({ + showSearchBox: state.shortcutOptions.showSearchBox, + stories: state.stories, + onSelectStory: (...args) => { + console.log('onSelectStory', args); + }, + onClose() { + console.log('searchbox.onClose'); + // actionMap.shortcuts.setOptions({ + // showSearchBox: false, + // }); + }, +}); + +export default props => ( + + {state => { + const finalProps = { + ...props, + ...mapper(state), + }; + + return ; + }} + +); diff --git a/lib/ui/src/modules/ui/containers/shortcuts_help.js b/lib/ui/src/modules/ui/containers/shortcuts_help.js index 09e6843790dc..20958878a54f 100755 --- a/lib/ui/src/modules/ui/containers/shortcuts_help.js +++ b/lib/ui/src/modules/ui/containers/shortcuts_help.js @@ -1,20 +1,30 @@ +import React from 'react'; import { window } from 'global'; + import ShortcutsHelp from '../components/shortcuts_help'; -import genPoddaLoader from '../libs/gen_podda_loader'; -import compose from '../../../compose'; +import { Consumer } from '../../../state'; -export const mapper = (state, props, { actions }) => { - const actionMap = actions(); +export const mapper = state => { const data = { isOpen: state.showShortcutsHelp, - onClose: actionMap.ui.toggleShortcutsHelp, + onClose: (...args) => { + console.log('onClose', args); + }, platform: window.navigator.platform.toLowerCase(), }; return data; }; -export default compose( - genPoddaLoader(mapper), - { withRef: false } -)(ShortcutsHelp); +export default props => ( + + {state => { + const finalProps = { + ...props, + ...mapper(state), + }; + + return ; + }} + +); diff --git a/lib/ui/src/modules/ui/containers/stories_panel.js b/lib/ui/src/modules/ui/containers/stories_panel.js index 8609f2b5690d..7c5650e723e4 100755 --- a/lib/ui/src/modules/ui/containers/stories_panel.js +++ b/lib/ui/src/modules/ui/containers/stories_panel.js @@ -1,7 +1,11 @@ -import StoriesPanel from '../components/stories_panel'; +import React from 'react'; +import { Consumer } from '../../../state'; + import * as filters from '../libs/filters'; -import genPoddaLoader from '../libs/gen_podda_loader'; -import compose from '../../../compose'; +import StoriesPanel from '../components/stories_panel'; + +// import genPoddaLoader from '../libs/gen_podda_loader'; +// import compose from '../../../compose'; import { prepareStoriesForHierarchy, @@ -10,9 +14,7 @@ import { createHierarchies, } from '../libs/hierarchy'; -export const mapper = (state, props, { actions }) => { - const actionMap = actions(); - +export const mapper = state => { const { stories, selectedKind, @@ -56,12 +58,18 @@ export const mapper = (state, props, { actions }) => { selectedKind, selectedStory, selectedHierarchy, - onSelectStory: actionMap.api.selectStory, + onSelectStory: (...args) => { + console.log('onSelectStory', args); + }, shortcutOptions, storyFilter, - onStoryFilter: actionMap.ui.setStoryFilter, - openShortcutsHelp: actionMap.ui.toggleShortcutsHelp, + onStoryFilter: (...args) => { + console.log('onStoryFilter', args); + }, + openShortcutsHelp: (...args) => { + console.log('openShortcutsHelp', args); + }, sidebarAnimations, isMobileDevice, name, @@ -69,7 +77,15 @@ export const mapper = (state, props, { actions }) => { }; }; -export default compose( - genPoddaLoader(mapper), - { withRef: false } -)(StoriesPanel); +export default props => ( + + {state => { + const finalProps = { + ...props, + ...mapper(state), + }; + + return ; + }} + +); diff --git a/lib/ui/src/provider.js b/lib/ui/src/provider.js new file mode 100644 index 000000000000..7773bb581e20 --- /dev/null +++ b/lib/ui/src/provider.js @@ -0,0 +1,9 @@ +export default class Provider { + renderPreview() { + throw new Error('Provider.renderPreview() is not implemented!'); + } + + handleAPI() { + throw new Error('Provider.handleAPI() is not implemented!'); + } +} diff --git a/lib/ui/src/state.js b/lib/ui/src/state.js new file mode 100644 index 000000000000..a0faf5ea4b84 --- /dev/null +++ b/lib/ui/src/state.js @@ -0,0 +1,4 @@ +import React from 'react'; + +const { Provider, Consumer } = React.createContext(); +export { Provider, Consumer }; diff --git a/lib/ui/src/store.js b/lib/ui/src/store.js new file mode 100644 index 000000000000..b02755af46e1 --- /dev/null +++ b/lib/ui/src/store.js @@ -0,0 +1,33 @@ +const createStore = (initialState = {}) => { + const internalState = { ...initialState }; + + return { + getAll() { + return internalState; + }, + }; +}; + +export default createStore({ + showShortcutsHelp: false, + storyFilter: null, + selectedAddonPanel: null, + isMobileDevice: false, + shortcutOptions: { + goFullScreen: false, + showStoriesPanel: true, + showAddonPanel: true, + showSearchBox: false, + addonPanelInRight: false, + enableShortcuts: true, + }, + uiOptions: { + name: 'STORYBOOK', + url: 'https://github.com/storybooks/storybook', + sortStoriesByKind: false, + hierarchySeparator: '/', + hierarchyRootSeparator: null, + sidebarAnimations: true, + theme: null, + }, +}); diff --git a/yarn.lock b/yarn.lock index 6716235de8bb..43d717c34e7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8217,6 +8217,16 @@ highlight.js@~9.12.0: version "9.12.0" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" +history@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b" + dependencies: + invariant "^2.2.1" + loose-envify "^1.2.0" + resolve-pathname "^2.2.0" + value-equal "^0.4.0" + warning "^3.0.0" + hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -8237,7 +8247,7 @@ hoist-non-react-statics@1.x.x, hoist-non-react-statics@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb" -hoist-non-react-statics@^2.3.1: +hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0: version "2.5.5" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" @@ -8868,7 +8878,7 @@ invariant@^2.2.0: dependencies: loose-envify "^1.0.0" -invariant@^2.2.2: +invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" dependencies: @@ -11336,7 +11346,7 @@ longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" dependencies: @@ -12050,6 +12060,10 @@ mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdi dependencies: minimist "0.0.8" +mobx@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.0.3.tgz#53b97f2a0f9b0dd7774c96249f81bf2d513d8e1c" + mock-fs@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.5.0.tgz#75245b966f7e3defe197b03454af9c5b355594b7" @@ -13307,7 +13321,7 @@ path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" -path-to-regexp@^1.0.1: +path-to-regexp@^1.0.1, path-to-regexp@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" dependencies: @@ -14062,7 +14076,7 @@ prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@^15.6.0, prop-types@^15.6.2: +prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: version "15.6.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" dependencies: @@ -14744,6 +14758,29 @@ react-reconciler@^0.7.0: object-assign "^4.1.1" prop-types "^15.6.0" +react-router-dom@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.3.1.tgz#4c2619fc24c4fa87c9fd18f4fb4a43fe63fbd5c6" + dependencies: + history "^4.7.2" + invariant "^2.2.4" + loose-envify "^1.3.1" + prop-types "^15.6.1" + react-router "^4.3.1" + warning "^4.0.1" + +react-router@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-4.3.1.tgz#aada4aef14c809cb2e686b05cee4742234506c4e" + dependencies: + history "^4.7.2" + hoist-non-react-statics "^2.5.0" + invariant "^2.2.4" + loose-envify "^1.3.1" + path-to-regexp "^1.7.0" + prop-types "^15.6.1" + warning "^4.0.1" + react-scripts@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-1.1.4.tgz#d5c230e707918d6dd2d06f303b10f5222d017c88" @@ -15743,6 +15780,10 @@ resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" +resolve-pathname@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879" + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" @@ -18387,6 +18428,10 @@ validate-npm-package-name@^3.0.0, validate-npm-package-name@~3.0.0: dependencies: builtins "^1.0.3" +value-equal@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7" + vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -18548,6 +18593,12 @@ warning@^3.0.0: dependencies: loose-envify "^1.0.0" +warning@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.1.tgz#66ce376b7fbfe8a887c22bdf0e7349d73d397745" + dependencies: + loose-envify "^1.0.0" + warp10@^1.0.0: version "1.3.6" resolved "https://registry.yarnpkg.com/warp10/-/warp10-1.3.6.tgz#edffff4f06382d2e469ba88ccfcb95bb81d3bda6" From 70865f7ed948a2bfa4b518dd0720519901008ab1 Mon Sep 17 00:00:00 2001 From: Alexandre BODIN Date: Tue, 7 Aug 2018 09:22:36 +0200 Subject: [PATCH 002/413] wip --- lib/components/src/layout/index.js | 6 ++---- lib/ui/src/app.js | 13 +++++++------ lib/ui/src/index.js | 4 +++- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/components/src/layout/index.js b/lib/components/src/layout/index.js index e385fd10e931..4eda9b91c01b 100755 --- a/lib/components/src/layout/index.js +++ b/lib/components/src/layout/index.js @@ -50,7 +50,7 @@ const Layout = props => { theme, addonPanel: AddonPanel, storiesPanel: StoriesPanel, - preview: Preview, + preview, shortcutsHelp: ShortcutsHelp, searchBox: SearchBox, } = props; @@ -64,9 +64,7 @@ const Layout = props => { - - - + {preview} ) : ( diff --git a/lib/ui/src/app.js b/lib/ui/src/app.js index fc280fcb0540..4fa3a8073bd8 100644 --- a/lib/ui/src/app.js +++ b/lib/ui/src/app.js @@ -71,7 +71,11 @@ class App extends React.Component { const { provider } = this.props; provider.handleAPI({ - onStory: (...args) => {}, + onStory: (cb) => { + return function stopListening() { + + } + }, setStories: this.setStories, selectStory: (...args) => {}, handleShortcut(...args) {}, @@ -94,10 +98,7 @@ class App extends React.Component { updateState = (...args) => this.setState(...args); render() { - const { provider } = this.props; - const { selectedKind, selectedStory } = this.state; - - const Preview = () => provider.renderPreview(selectedKind, selectedStory); + const { provider, preview } = this.props; return ( @@ -105,7 +106,7 @@ class App extends React.Component { ( } + preview={preview} storiesPanel={() => } addonPanel={() => } shortcutsHelp={() => } diff --git a/lib/ui/src/index.js b/lib/ui/src/index.js index 38fd064bc0c5..145557ca53b7 100644 --- a/lib/ui/src/index.js +++ b/lib/ui/src/index.js @@ -9,7 +9,9 @@ function renderStorybokUI(domNode, provider) { throw new Error('provider is not extended from the base Provider'); } - ReactDOM.render(, domNode); + const preview = () => provider.renderPreview(null, null); + + ReactDOM.render(, domNode); } export { Provider }; From 4fa3206339c9e69d3fc84f54693f51c976d5273c Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Tue, 7 Aug 2018 20:57:53 +0200 Subject: [PATCH 003/413] add mobx --- lib/ui/package.json | 1 + lib/ui/src/app.js | 134 +++--------------- lib/ui/src/index.js | 41 +++++- lib/ui/src/modules/ui/containers/layout.js | 24 +--- .../src/modules/ui/containers/routed_link.js | 39 +---- .../modules/ui/containers/shortcuts_help.js | 31 ++-- .../modules/ui/containers/stories_panel.js | 53 +++---- lib/ui/src/state.js | 4 - lib/ui/src/store.js | 122 ++++++++++++---- yarn.lock | 21 ++- 10 files changed, 207 insertions(+), 263 deletions(-) delete mode 100644 lib/ui/src/state.js diff --git a/lib/ui/package.json b/lib/ui/package.json index fc41c68ffb04..69c10a4be072 100644 --- a/lib/ui/package.json +++ b/lib/ui/package.json @@ -34,6 +34,7 @@ "lodash.sortby": "^4.7.0", "lodash.throttle": "^4.1.1", "mobx": "^5.0.3", + "mobx-react": "^5.2.3", "prop-types": "^15.6.2", "qs": "^6.5.2", "react-emotion": "^9.2.6", diff --git a/lib/ui/src/app.js b/lib/ui/src/app.js index 4fa3a8073bd8..e1b610bcea5d 100644 --- a/lib/ui/src/app.js +++ b/lib/ui/src/app.js @@ -1,123 +1,27 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { BrowserRouter, Route } from 'react-router-dom'; - -import { Provider } from './state'; - import Layout from './modules/ui/containers/layout'; import StoriesPanel from './modules/ui/containers/stories_panel'; -import AddonPanel from './modules/ui/containers/addon_panel'; +// import AddonPanel from './modules/ui/containers/addon_panel'; import ShortcutsHelp from './modules/ui/containers/shortcuts_help'; -import SearchBox from './modules/ui/containers/search_box'; - -function ensureKind(storyKinds, selectedKind) { - if (!storyKinds) return selectedKind; - - const found = storyKinds.find(item => item.kind === selectedKind); - if (found) return found.kind; - // if the selected kind is non-existant, select the first kind - const kinds = storyKinds.map(item => item.kind); - return kinds[0]; -} - -function ensureStory(storyKinds, selectedKind, selectedStory) { - if (!storyKinds) return selectedStory; - - const kindInfo = storyKinds.find(item => item.kind === selectedKind); - if (!kindInfo) return null; - - const found = kindInfo.stories.find(item => item === selectedStory); - if (found) return found; - - // if the selected story is non-existant, select the first story - return kindInfo.stories[0]; -} - -/* eslint-disable */ -class App extends React.Component { - static propTypes = { - provider: PropTypes.shape({ - renderPreview: PropTypes.func.isRequired, - }).isRequired, - }; - - state = { - showShortcutsHelp: false, - storyFilter: null, - selectedAddonPanel: null, - isMobileDevice: false, - shortcutOptions: { - goFullScreen: false, - showStoriesPanel: true, - showAddonPanel: true, - showSearchBox: false, - addonPanelInRight: false, - enableShortcuts: true, - }, - uiOptions: { - name: 'STORYBOOK', - url: 'https://github.com/storybooks/storybook', - sortStoriesByKind: false, - hierarchySeparator: '/', - hierarchyRootSeparator: null, - sidebarAnimations: true, - theme: null, - }, - updateState: this.updateState, - }; - - componentDidMount() { - const { provider } = this.props; - - provider.handleAPI({ - onStory: (cb) => { - return function stopListening() { - - } - }, - setStories: this.setStories, - selectStory: (...args) => {}, - handleShortcut(...args) {}, - }); - } - - setStories = stories => { - const selectedKind = ensureKind(stories, this.state.selectedKind); - const currentSelectedStory = - this.state.selectedKind === selectedKind ? this.state.selectedStory : null; - const selectedStory = ensureStory(stories, selectedKind, currentSelectedStory); - - this.setState({ - stories, - selectedStory, - selectedKind, - }); - }; - - updateState = (...args) => this.setState(...args); - - render() { - const { provider, preview } = this.props; - - return ( - - - ( - } - addonPanel={() => } - shortcutsHelp={() => } - searchBox={() => } - /> - )} - /> - - - ); - } -} +// import SearchBox from './modules/ui/containers/search_box'; + +const App = ({ provider, store }) => { + const Preview = () => provider.renderPreview(null, null); + + return ( + } + storiesPanel={() => } + addonPanel={() =>
addonPanel
} + shortcutsHelp={() => } + searchBox={() =>
searchBox
} + // addonPanel={() => } + // shortcutsHelp={() => } + // searchBox={() => } + /> + ); +}; export default App; diff --git a/lib/ui/src/index.js b/lib/ui/src/index.js index 145557ca53b7..a1515a7bb53b 100644 --- a/lib/ui/src/index.js +++ b/lib/ui/src/index.js @@ -1,17 +1,52 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import App from './app'; +import { Provider as MobxProvider } from 'mobx-react'; +import ReactModal from 'react-modal'; +import App from './app'; import Provider from './provider'; +import createStore from './store'; function renderStorybokUI(domNode, provider) { if (!(provider instanceof Provider)) { throw new Error('provider is not extended from the base Provider'); } - const preview = () => provider.renderPreview(null, null); + const store = createStore(); + + provider.handleAPI({ + onStory(cb) { + console.log('onStory'); + return function stopListening() {}; + }, + setStories(stories) { + console.log('setStories'); + store.setStories(stories); + }, + selectStory() { + console.log('selectStory'); + }, + handleShortcut() { + console.log('handleShortcut'); + } + }); + + // Tell react-modal which element to mark as aria-hidden + ReactModal.setAppElement(domNode); + + const Container = process.env.STORYBOOK_EXAMPLE_APP + ? React.StrictMode + : 'div'; + + const root = ( + + + + + + ); - ReactDOM.render(, domNode); + ReactDOM.render(root, domNode); } export { Provider }; diff --git a/lib/ui/src/modules/ui/containers/layout.js b/lib/ui/src/modules/ui/containers/layout.js index 01c04c73e4dd..852e0d17114f 100755 --- a/lib/ui/src/modules/ui/containers/layout.js +++ b/lib/ui/src/modules/ui/containers/layout.js @@ -1,9 +1,10 @@ -import React from 'react'; import pick from 'lodash.pick'; import { Layout } from '@storybook/components'; -import { Consumer } from '../../../state'; -export const mapper = state => { +import { inject } from 'mobx-react'; + +export default inject(stores => { + const state = stores.store; const { shortcutOptions, isMobileDevice, uiOptions } = state; const currentOptions = pick( shortcutOptions, @@ -16,19 +17,6 @@ export const mapper = state => { return { ...currentOptions, isMobileDevice, - ...uiOptions, + ...uiOptions }; -}; - -export default props => ( - - {state => { - const finalProps = { - ...props, - ...mapper(state), - }; - - return ; - }} - -); +})(Layout); diff --git a/lib/ui/src/modules/ui/containers/routed_link.js b/lib/ui/src/modules/ui/containers/routed_link.js index a292b3d11570..add69863a3f1 100644 --- a/lib/ui/src/modules/ui/containers/routed_link.js +++ b/lib/ui/src/modules/ui/containers/routed_link.js @@ -2,7 +2,7 @@ import { RoutedLink, MenuLink } from '@storybook/components'; import React from 'react'; -import { Consumer } from '../../../state'; +import { inject } from 'mobx-react'; // import genPoddaLoader from '../libs/gen_podda_loader'; // import { getUrlState } from '../configs/handle_routing'; @@ -13,43 +13,12 @@ export function mapper() { return { // href: url, - href: '', + href: '' }; } -// const composer = compose( -// genPoddaLoader(mapper), -// { withRef: false } -// ); +const ComposedMenuLink = inject(({ store }) => mapper(store))(MenuLink); +const ComposedRoutedLink = inject(({ store }) => mapper(store))(RoutedLink); -const ComposedMenuLink = props => ( - - {state => { - const finalProps = { - ...props, - ...mapper(state), - }; - - return ; - }} - -); - -const ComposedRoutedLink = props => ( - - {state => { - const finalProps = { - ...props, - ...mapper(state), - }; - - return ; - }} - -); - -// const ComposedMenuLink = composer(MenuLink); export { ComposedMenuLink as MenuLink }; - -// const ComposedRoutedLink = composer(RoutedLink); export { ComposedRoutedLink as RoutedLink }; diff --git a/lib/ui/src/modules/ui/containers/shortcuts_help.js b/lib/ui/src/modules/ui/containers/shortcuts_help.js index 20958878a54f..a07345596039 100755 --- a/lib/ui/src/modules/ui/containers/shortcuts_help.js +++ b/lib/ui/src/modules/ui/containers/shortcuts_help.js @@ -1,30 +1,15 @@ -import React from 'react'; +import { inject } from 'mobx-react'; import { window } from 'global'; import ShortcutsHelp from '../components/shortcuts_help'; -import { Consumer } from '../../../state'; -export const mapper = state => { - const data = { - isOpen: state.showShortcutsHelp, - onClose: (...args) => { - console.log('onClose', args); - }, - platform: window.navigator.platform.toLowerCase(), +export const mapper = store => { + console.log(store); + return { + isOpen: store.showShortcutsHelp, + onClose: () => store.toggleShortcutsHelp(), + platform: window.navigator.platform.toLowerCase() }; - - return data; }; -export default props => ( - - {state => { - const finalProps = { - ...props, - ...mapper(state), - }; - - return ; - }} - -); +export default inject(({ store }) => mapper(store))(ShortcutsHelp); diff --git a/lib/ui/src/modules/ui/containers/stories_panel.js b/lib/ui/src/modules/ui/containers/stories_panel.js index 7c5650e723e4..a9ef7eac9963 100755 --- a/lib/ui/src/modules/ui/containers/stories_panel.js +++ b/lib/ui/src/modules/ui/containers/stories_panel.js @@ -1,20 +1,15 @@ -import React from 'react'; -import { Consumer } from '../../../state'; +import { inject } from 'mobx-react'; -import * as filters from '../libs/filters'; import StoriesPanel from '../components/stories_panel'; - -// import genPoddaLoader from '../libs/gen_podda_loader'; -// import compose from '../../../compose'; - +import * as filters from '../libs/filters'; import { prepareStoriesForHierarchy, resolveStoryHierarchy, resolveStoryHierarchyRoots, - createHierarchies, + createHierarchies } from '../libs/hierarchy'; -export const mapper = state => { +export const mapper = store => { const { stories, selectedKind, @@ -22,8 +17,8 @@ export const mapper = state => { uiOptions, storyFilter, shortcutOptions, - isMobileDevice, - } = state; + isMobileDevice + } = store; const { sortStoriesByKind, @@ -31,7 +26,7 @@ export const mapper = state => { hierarchyRootSeparator, sidebarAnimations, name, - url, + url } = uiOptions; const preparedStories = prepareStoriesForHierarchy( @@ -50,8 +45,14 @@ export const mapper = state => { const storiesHierarchies = createHierarchies(filteredStories); - const { storyName } = resolveStoryHierarchyRoots(selectedKind, hierarchyRootSeparator); - const selectedHierarchy = resolveStoryHierarchy(storyName, hierarchySeparator); + const { storyName } = resolveStoryHierarchyRoots( + selectedKind, + hierarchyRootSeparator + ); + const selectedHierarchy = resolveStoryHierarchy( + storyName, + hierarchySeparator + ); return { storiesHierarchies, @@ -62,30 +63,14 @@ export const mapper = state => { console.log('onSelectStory', args); }, shortcutOptions, - storyFilter, - onStoryFilter: (...args) => { - console.log('onStoryFilter', args); - }, - openShortcutsHelp: (...args) => { - console.log('openShortcutsHelp', args); - }, + onStoryFilter: filter => store.setStoryFilter(filter), + openShortcutsHelp: () => store.toggleShortcutsHelp(), sidebarAnimations, isMobileDevice, name, - url, + url }; }; -export default props => ( - - {state => { - const finalProps = { - ...props, - ...mapper(state), - }; - - return ; - }} - -); +export default inject(({ store }) => mapper(store))(StoriesPanel); diff --git a/lib/ui/src/state.js b/lib/ui/src/state.js deleted file mode 100644 index a0faf5ea4b84..000000000000 --- a/lib/ui/src/state.js +++ /dev/null @@ -1,4 +0,0 @@ -import React from 'react'; - -const { Provider, Consumer } = React.createContext(); -export { Provider, Consumer }; diff --git a/lib/ui/src/store.js b/lib/ui/src/store.js index b02755af46e1..e2f047ee6638 100644 --- a/lib/ui/src/store.js +++ b/lib/ui/src/store.js @@ -1,33 +1,97 @@ -const createStore = (initialState = {}) => { - const internalState = { ...initialState }; +import { observable, observe, action } from 'mobx'; - return { - getAll() { - return internalState; +function ensureKind(storyKinds, selectedKind) { + if (!storyKinds) return selectedKind; + + const found = storyKinds.find(item => item.kind === selectedKind); + if (found) return found.kind; + // if the selected kind is non-existant, select the first kind + const kinds = storyKinds.map(item => item.kind); + return kinds[0]; +} + +function ensureStory(storyKinds, selectedKind, selectedStory) { + if (!storyKinds) return selectedStory; + + const kindInfo = storyKinds.find(item => item.kind === selectedKind); + if (!kindInfo) return null; + + const found = kindInfo.stories.find(item => item === selectedStory); + if (found) return found; + + // if the selected story is non-existant, select the first story + return kindInfo.stories[0]; +} + +const createStore = () => { + const store = observable( + { + stories: [], + selectedKind: undefined, + selectedStory: undefined, + showShortcutsHelp: false, + storyFilter: null, + selectedAddonPanel: null, + isMobileDevice: false, + shortcutOptions: { + goFullScreen: false, + showStoriesPanel: true, + showAddonPanel: true, + showSearchBox: false, + addonPanelInRight: false, + enableShortcuts: true + }, + uiOptions: { + name: 'STORYBOOK', + url: 'https://github.com/storybooks/storybook', + sortStoriesByKind: false, + hierarchySeparator: '/', + hierarchyRootSeparator: null, + sidebarAnimations: true, + theme: null + }, + + /** UI actions */ + setStoryFilter(filter) { + this.storyFilter = filter; + }, + + toggleShortcutsHelp() { + this.showShortcutsHelp = !this.showShortcutsHelp; + }, + + selectAddonPanel(panelName) { + this.selectedAddonPanel = panelName; + }, + + setStories(stories) { + const selectedKind = ensureKind(stories, this.selectedKind); + const currentSelectedStory = + this.selectedKind === selectedKind ? this.selectedStory : null; + const selectedStory = ensureStory( + stories, + selectedKind, + currentSelectedStory + ); + + this.stories = stories; + this.selectedStory = selectedStory; + this.selectedKind = selectedKind; + } }, - }; + { + setStoryFilter: action, + toggleShortcutsHelp: action, + selectAddonPanel: action, + setStories: action + } + ); + + observe(store, change => { + console.log('Store updated', change); + }); + + return store; }; -export default createStore({ - showShortcutsHelp: false, - storyFilter: null, - selectedAddonPanel: null, - isMobileDevice: false, - shortcutOptions: { - goFullScreen: false, - showStoriesPanel: true, - showAddonPanel: true, - showSearchBox: false, - addonPanelInRight: false, - enableShortcuts: true, - }, - uiOptions: { - name: 'STORYBOOK', - url: 'https://github.com/storybooks/storybook', - sortStoriesByKind: false, - hierarchySeparator: '/', - hierarchyRootSeparator: null, - sidebarAnimations: true, - theme: null, - }, -}); +export default createStore; diff --git a/yarn.lock b/yarn.lock index 43d717c34e7e..795faa5daf0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7968,7 +7968,13 @@ graphql-request@^1.4.0: dependencies: cross-fetch "1.1.1" -graphql@^0.12.3, graphql@^0.13.2: +graphql@^0.12.3: + version "0.12.3" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.12.3.tgz#11668458bbe28261c0dcb6e265f515ba79f6ce07" + dependencies: + iterall "1.1.3" + +graphql@^0.13.2: version "0.13.2" resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.13.2.tgz#4c740ae3c222823e7004096f832e7b93b2108270" dependencies: @@ -9485,6 +9491,10 @@ istanbul@^0.4.5: which "^1.1.1" wordwrap "^1.0.0" +iterall@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.3.tgz#1cbbff96204056dde6656e2ed2e2226d0e6d72c9" + iterall@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" @@ -12060,6 +12070,13 @@ mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdi dependencies: minimist "0.0.8" +mobx-react@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-5.2.3.tgz#cdf6141c2fe63377c5813cbd254e8ce0d4676631" + dependencies: + hoist-non-react-statics "^2.5.0" + react-lifecycles-compat "^3.0.2" + mobx@^5.0.3: version "5.0.3" resolved "https://registry.yarnpkg.com/mobx/-/mobx-5.0.3.tgz#53b97f2a0f9b0dd7774c96249f81bf2d513d8e1c" @@ -14656,7 +14673,7 @@ react-lifecycles-compat@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.2.tgz#7279047275bd727a912e25f734c0559527e84eff" -react-lifecycles-compat@^3.0.4: +react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" From 61791213853410be589ce460ca0ecb3ee6bb4ef3 Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Tue, 7 Aug 2018 21:36:07 +0200 Subject: [PATCH 004/413] Adds actions - [ ] Split into multiple stores - [ ] Implement remaining actions - [ ] handle routing in a better way --- lib/ui/src/app.js | 14 ++-- lib/ui/src/index.js | 22 ++---- lib/ui/src/init-provider-api.js | 68 +++++++++++++++++++ .../src/modules/ui/containers/addon_panel.js | 26 ++----- .../src/modules/ui/containers/search_box.js | 40 ++++------- .../modules/ui/containers/stories_panel.js | 4 +- lib/ui/src/store.js | 21 ++++-- 7 files changed, 113 insertions(+), 82 deletions(-) create mode 100644 lib/ui/src/init-provider-api.js diff --git a/lib/ui/src/app.js b/lib/ui/src/app.js index e1b610bcea5d..8ab242165a6f 100644 --- a/lib/ui/src/app.js +++ b/lib/ui/src/app.js @@ -1,25 +1,21 @@ import React from 'react'; -import PropTypes from 'prop-types'; import Layout from './modules/ui/containers/layout'; import StoriesPanel from './modules/ui/containers/stories_panel'; -// import AddonPanel from './modules/ui/containers/addon_panel'; +import AddonPanel from './modules/ui/containers/addon_panel'; import ShortcutsHelp from './modules/ui/containers/shortcuts_help'; -// import SearchBox from './modules/ui/containers/search_box'; +import SearchBox from './modules/ui/containers/search_box'; const App = ({ provider, store }) => { - const Preview = () => provider.renderPreview(null, null); + const Preview = () => provider.renderPreview(store.selectedKind, store.selectedStory); return ( } storiesPanel={() => } - addonPanel={() =>
addonPanel
} shortcutsHelp={() => } - searchBox={() =>
searchBox
} - // addonPanel={() => } - // shortcutsHelp={() => } - // searchBox={() => } + searchBox={() => } + addonPanel={() => } /> ); }; diff --git a/lib/ui/src/index.js b/lib/ui/src/index.js index a1515a7bb53b..02f5c26a5fb8 100644 --- a/lib/ui/src/index.js +++ b/lib/ui/src/index.js @@ -5,6 +5,7 @@ import ReactModal from 'react-modal'; import App from './app'; import Provider from './provider'; +import initProviderApi from './init-provider-api'; import createStore from './store'; function renderStorybokUI(domNode, provider) { @@ -13,23 +14,10 @@ function renderStorybokUI(domNode, provider) { } const store = createStore(); + initProviderApi({ provider, store }); - provider.handleAPI({ - onStory(cb) { - console.log('onStory'); - return function stopListening() {}; - }, - setStories(stories) { - console.log('setStories'); - store.setStories(stories); - }, - selectStory() { - console.log('selectStory'); - }, - handleShortcut() { - console.log('handleShortcut'); - } - }); + const Preview = () => + provider.renderPreview(store.selectedKind, store.selectedStory); // Tell react-modal which element to mark as aria-hidden ReactModal.setAppElement(domNode); @@ -41,7 +29,7 @@ function renderStorybokUI(domNode, provider) { const root = ( - + ); diff --git a/lib/ui/src/init-provider-api.js b/lib/ui/src/init-provider-api.js new file mode 100644 index 000000000000..47c9d76f6e74 --- /dev/null +++ b/lib/ui/src/init-provider-api.js @@ -0,0 +1,68 @@ +import { autorun } from 'mobx'; + +import { EventEmitter } from 'events'; + +export default ({ provider, store }) => { + const callbacks = new EventEmitter(); + let currentKind; + let currentStory; + + const api = { + onStory(cb) { + console.log('onStory'); + callbacks.on('story', cb); + if (currentKind && currentStory) { + // Using a setTimeout to call the callback to make sure it's + // not called on current event-loop. When users add callbacks + // they usually expect it to be called in a future event loop. + setTimeout(() => cb(currentKind, currentStory), 0); + } + return function stopListening() { + callbacks.removeListener('story', cb); + }; + }, + setStories(stories) { + store.setStories(stories); + }, + selectInCurrentKind(...args) { + console.log('selectInCurrentKind', args); + }, + selectStory(...args) { + console.log('selectStory', args); + }, + handleShortcut(...args) { + console.log('handleShortcut', args); + }, + setQueryParams(...args) { + console.log('setQueryParams', args); + }, + setOptions(...args) { + console.log('setOptions', args); + }, + getQueryParam(...args) { + console.log('getQueryParam', args); + }, + getUrlState(...args) { + console.log('getUrlState', args); + } + }; + + provider.handleAPI(api); + + autorun(() => { + console.log('AUTORUN'); + if (!store.selectedKind) return; + + if ( + store.selectedKind === currentKind && + store.selectedStory === currentStory + ) { + // No change in the selected story so avoid emitting 'story' + return; + } + + currentKind = store.selectedKind; + currentStory = store.selectedStory; + callbacks.emit('story', store.selectedKind, store.selectedStory); + }); +}; diff --git a/lib/ui/src/modules/ui/containers/addon_panel.js b/lib/ui/src/modules/ui/containers/addon_panel.js index da692776403f..9309475a334c 100644 --- a/lib/ui/src/modules/ui/containers/addon_panel.js +++ b/lib/ui/src/modules/ui/containers/addon_panel.js @@ -1,30 +1,14 @@ -import React from 'react'; +import { inject } from 'mobx-react'; import { AddonPanel } from '@storybook/components'; -import { Consumer } from '../../../state'; -export function mapper(state, { panels }) { - const selectedPanel = state.selectedAddonPanel; - - console.log(panels); +export function mapper(store, { panels }) { + const selectedPanel = store.selectedAddonPanel; return { panels, selectedPanel, - onPanelSelect: (...args) => { - console.log('onPanelSelect', args); - }, + onPanelSelect: panel => store.selectAddonPanel(panel) }; } -export default props => ( - - {state => { - const finalProps = { - ...props, - ...mapper(state, props), - }; - - return ; - }} - -); +export default inject(({ store }, props) => mapper(store, props))(AddonPanel); diff --git a/lib/ui/src/modules/ui/containers/search_box.js b/lib/ui/src/modules/ui/containers/search_box.js index 5da1ccf144af..c04fb2dfcc67 100644 --- a/lib/ui/src/modules/ui/containers/search_box.js +++ b/lib/ui/src/modules/ui/containers/search_box.js @@ -1,31 +1,17 @@ -import React from 'react'; -import SearchBoxComponent from '../components/search_box'; +import { inject } from 'mobx-react'; +import { observe } from 'mobx'; -import { Consumer } from '../../../state'; +import SearchBox from '../components/search_box'; -export const mapper = state => ({ - showSearchBox: state.shortcutOptions.showSearchBox, - stories: state.stories, - onSelectStory: (...args) => { - console.log('onSelectStory', args); - }, - onClose() { - console.log('searchbox.onClose'); - // actionMap.shortcuts.setOptions({ - // showSearchBox: false, - // }); - }, -}); +export const mapper = store => { + console.log('la', store.shortcutOptions.showSearchBox); -export default props => ( - - {state => { - const finalProps = { - ...props, - ...mapper(state), - }; + return { + showSearchBox: store.shortcutOptions.showSearchBox, + stories: store.stories, + onSelectStory: (kind, story) => store.selectStory(kind, story), + onClose: () => store.toggleSearchBox() + }; +}; - return ; - }} - -); +export default inject(({ store }) => mapper(store))(SearchBox); diff --git a/lib/ui/src/modules/ui/containers/stories_panel.js b/lib/ui/src/modules/ui/containers/stories_panel.js index a9ef7eac9963..d62120d16385 100755 --- a/lib/ui/src/modules/ui/containers/stories_panel.js +++ b/lib/ui/src/modules/ui/containers/stories_panel.js @@ -59,9 +59,7 @@ export const mapper = store => { selectedKind, selectedStory, selectedHierarchy, - onSelectStory: (...args) => { - console.log('onSelectStory', args); - }, + onSelectStory: (kind, story) => store.selectStory(kind, story), shortcutOptions, storyFilter, onStoryFilter: filter => store.setStoryFilter(filter), diff --git a/lib/ui/src/store.js b/lib/ui/src/store.js index e2f047ee6638..ceb45b5af055 100644 --- a/lib/ui/src/store.js +++ b/lib/ui/src/store.js @@ -51,6 +51,11 @@ const createStore = () => { theme: null }, + toggleSearchBox() { + this.shortcutOptions.showSearchBox = !this.shortcutOptions + .showSearchBox; + }, + /** UI actions */ setStoryFilter(filter) { this.storyFilter = filter; @@ -75,22 +80,28 @@ const createStore = () => { ); this.stories = stories; + this.selectedStory = selectedStory; + this.selectedKind = selectedKind; + }, + + selectStory(kind, story) { + const selectedKind = ensureKind(this.stories, kind); + const selectedStory = ensureStory(this.stories, selectedKind, story); + this.selectedStory = selectedStory; this.selectedKind = selectedKind; } }, { + toggleSearchBox: action, setStoryFilter: action, toggleShortcutsHelp: action, selectAddonPanel: action, - setStories: action + setStories: action, + selectStory: action } ); - observe(store, change => { - console.log('Store updated', change); - }); - return store; }; From 918209f64d67568d305cf7277a52af76a3527ab3 Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Tue, 7 Aug 2018 22:43:59 +0200 Subject: [PATCH 005/413] Handle shortcuts --- lib/ui/src/app.js | 40 ++-- lib/ui/src/init-provider-api.js | 5 +- .../src/modules/ui/containers/search_box.js | 2 - .../modules/ui/containers/shortcuts_help.js | 13 +- lib/ui/src/store.js | 203 +++++++++++------- 5 files changed, 162 insertions(+), 101 deletions(-) diff --git a/lib/ui/src/app.js b/lib/ui/src/app.js index 8ab242165a6f..23679b85aff6 100644 --- a/lib/ui/src/app.js +++ b/lib/ui/src/app.js @@ -1,23 +1,39 @@ import React from 'react'; +import keyEvents from './libs/key_events'; import Layout from './modules/ui/containers/layout'; import StoriesPanel from './modules/ui/containers/stories_panel'; import AddonPanel from './modules/ui/containers/addon_panel'; import ShortcutsHelp from './modules/ui/containers/shortcuts_help'; import SearchBox from './modules/ui/containers/search_box'; -const App = ({ provider, store }) => { - const Preview = () => provider.renderPreview(store.selectedKind, store.selectedStory); +class App extends React.Component { + componentDidMount() { + // Handle events + document.addEventListener('keydown', e => { + const parsedEvent = keyEvents(e); + if (parsedEvent) { + this.props.store.handleEvent(parsedEvent); + } + }); + } - return ( - } - storiesPanel={() => } - shortcutsHelp={() => } - searchBox={() => } - addonPanel={() => } - /> - ); -}; + render() { + const { provider, store } = this.props; + + const Preview = () => + provider.renderPreview(store.selectedKind, store.selectedStory); + + return ( + } + storiesPanel={() => } + shortcutsHelp={() => } + searchBox={() => } + addonPanel={() => } + /> + ); + } +} export default App; diff --git a/lib/ui/src/init-provider-api.js b/lib/ui/src/init-provider-api.js index 47c9d76f6e74..2edf3454703e 100644 --- a/lib/ui/src/init-provider-api.js +++ b/lib/ui/src/init-provider-api.js @@ -30,8 +30,8 @@ export default ({ provider, store }) => { selectStory(...args) { console.log('selectStory', args); }, - handleShortcut(...args) { - console.log('handleShortcut', args); + handleShortcut(event) { + store.handleEvent(event); }, setQueryParams(...args) { console.log('setQueryParams', args); @@ -50,7 +50,6 @@ export default ({ provider, store }) => { provider.handleAPI(api); autorun(() => { - console.log('AUTORUN'); if (!store.selectedKind) return; if ( diff --git a/lib/ui/src/modules/ui/containers/search_box.js b/lib/ui/src/modules/ui/containers/search_box.js index c04fb2dfcc67..a90570e52314 100644 --- a/lib/ui/src/modules/ui/containers/search_box.js +++ b/lib/ui/src/modules/ui/containers/search_box.js @@ -4,8 +4,6 @@ import { observe } from 'mobx'; import SearchBox from '../components/search_box'; export const mapper = store => { - console.log('la', store.shortcutOptions.showSearchBox); - return { showSearchBox: store.shortcutOptions.showSearchBox, stories: store.stories, diff --git a/lib/ui/src/modules/ui/containers/shortcuts_help.js b/lib/ui/src/modules/ui/containers/shortcuts_help.js index a07345596039..b5a9049483d8 100755 --- a/lib/ui/src/modules/ui/containers/shortcuts_help.js +++ b/lib/ui/src/modules/ui/containers/shortcuts_help.js @@ -3,13 +3,10 @@ import { window } from 'global'; import ShortcutsHelp from '../components/shortcuts_help'; -export const mapper = store => { - console.log(store); - return { - isOpen: store.showShortcutsHelp, - onClose: () => store.toggleShortcutsHelp(), - platform: window.navigator.platform.toLowerCase() - }; -}; +export const mapper = store => ({ + isOpen: store.showShortcutsHelp, + onClose: () => store.toggleShortcutsHelp(), + platform: window.navigator.platform.toLowerCase() +}); export default inject(({ store }) => mapper(store))(ShortcutsHelp); diff --git a/lib/ui/src/store.js b/lib/ui/src/store.js index ceb45b5af055..837514651d07 100644 --- a/lib/ui/src/store.js +++ b/lib/ui/src/store.js @@ -1,4 +1,5 @@ -import { observable, observe, action } from 'mobx'; +import { observable } from 'mobx'; +import { features } from './libs/key_events'; function ensureKind(storyKinds, selectedKind) { if (!storyKinds) return selectedKind; @@ -22,85 +23,135 @@ function ensureStory(storyKinds, selectedKind, selectedStory) { // if the selected story is non-existant, select the first story return kindInfo.stories[0]; } - const createStore = () => { - const store = observable( - { - stories: [], - selectedKind: undefined, - selectedStory: undefined, - showShortcutsHelp: false, - storyFilter: null, - selectedAddonPanel: null, - isMobileDevice: false, - shortcutOptions: { - goFullScreen: false, - showStoriesPanel: true, - showAddonPanel: true, - showSearchBox: false, - addonPanelInRight: false, - enableShortcuts: true - }, - uiOptions: { - name: 'STORYBOOK', - url: 'https://github.com/storybooks/storybook', - sortStoriesByKind: false, - hierarchySeparator: '/', - hierarchyRootSeparator: null, - sidebarAnimations: true, - theme: null - }, - - toggleSearchBox() { - this.shortcutOptions.showSearchBox = !this.shortcutOptions - .showSearchBox; - }, - - /** UI actions */ - setStoryFilter(filter) { - this.storyFilter = filter; - }, - - toggleShortcutsHelp() { - this.showShortcutsHelp = !this.showShortcutsHelp; - }, - - selectAddonPanel(panelName) { - this.selectedAddonPanel = panelName; - }, - - setStories(stories) { - const selectedKind = ensureKind(stories, this.selectedKind); - const currentSelectedStory = - this.selectedKind === selectedKind ? this.selectedStory : null; - const selectedStory = ensureStory( - stories, - selectedKind, - currentSelectedStory - ); - - this.stories = stories; - this.selectedStory = selectedStory; - this.selectedKind = selectedKind; - }, - - selectStory(kind, story) { - const selectedKind = ensureKind(this.stories, kind); - const selectedStory = ensureStory(this.stories, selectedKind, story); - - this.selectedStory = selectedStory; - this.selectedKind = selectedKind; + const store = observable({ + stories: [], + selectedKind: undefined, + selectedStory: undefined, + showShortcutsHelp: false, + storyFilter: null, + selectedAddonPanel: null, + isMobileDevice: false, + shortcutOptions: { + goFullScreen: false, + showStoriesPanel: true, + showAddonPanel: true, + showSearchBox: false, + addonPanelInRight: false, + enableShortcuts: true + }, + uiOptions: { + name: 'STORYBOOK', + url: 'https://github.com/storybooks/storybook', + sortStoriesByKind: false, + hierarchySeparator: '/', + hierarchyRootSeparator: null, + sidebarAnimations: true, + theme: null + }, + + jumpToStory(direction) { + const flatteredStories = []; + let currentIndex = -1; + + this.stories.forEach(({ kind, stories }) => { + stories.forEach(story => { + flatteredStories.push({ kind, story }); + if (kind === this.selectedKind && story === this.selectedStory) { + currentIndex = flatteredStories.length - 1; + } + }); + }); + + const jumpedStory = flatteredStories[currentIndex + direction]; + if (!jumpedStory) { + return; + } + + this.selectedKind = jumpedStory.kind; + this.selectedStory = jumpedStory.story; + }, + + handleEvent(event) { + if (!this.shortcutOptions.enableShortcuts) return; + + switch (event) { + case features.NEXT_STORY: { + this.jumpToStory(1); + break; + } + case features.PREV_STORY: { + this.jumpToStory(-1); + break; + } + case features.FULLSCREEN: { + this.shortcutOptions.goFullScreen = !this.shortcutOptions + .goFullScreen; + break; + } + case features.ADDON_PANEL: { + this.shortcutOptions.showAddonPanel = !this.shortcutOptions + .showAddonPanel; + break; + } + case features.STORIES_PANEL: { + this.shortcutOptions.showStoriesPanel = !this.shortcutOptions + .showStoriesPanel; + break; + } + case features.SHOW_SEARCH: { + this.toggleSearchBox(); + break; + } + case features.ADDON_PANEL_IN_RIGHT: { + this.shortcutOptions.addonPanelInRight = !this.shortcutOptions + .addonPanelInRight; + } + default: + return; } }, - { - toggleSearchBox: action, - setStoryFilter: action, - toggleShortcutsHelp: action, - selectAddonPanel: action, - setStories: action, - selectStory: action + + toggleSearchBox() { + this.shortcutOptions.showSearchBox = !this.shortcutOptions.showSearchBox; + }, + + /** UI actions */ + setStoryFilter(filter) { + this.storyFilter = filter; + }, + + toggleShortcutsHelp() { + this.showShortcutsHelp = !this.showShortcutsHelp; + }, + + selectAddonPanel(panelName) { + this.selectedAddonPanel = panelName; + }, + + setStories(stories) { + const selectedKind = ensureKind(stories, this.selectedKind); + const currentSelectedStory = + this.selectedKind === selectedKind ? this.selectedStory : null; + const selectedStory = ensureStory( + stories, + selectedKind, + currentSelectedStory + ); + + this.stories = stories; + this.selectedStory = selectedStory; + this.selectedKind = selectedKind; + }, + + selectStory(kind, story) { + const selectedKind = ensureKind(this.stories, kind); + const selectedStory = ensureStory(this.stories, selectedKind, story); + + this.selectedStory = selectedStory; + this.selectedKind = selectedKind; } - ); + }); return store; }; From eba6fd71d2fc40bba109315d0f531ce8b3dc611a Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Wed, 8 Aug 2018 00:53:57 +0200 Subject: [PATCH 006/413] Fix code need undefined fields in the state. this needs fixing --- .../cra-kitchen-sink/.storybook/config.js | 4 +- lib/ui/src/index.js | 6 +- lib/ui/src/init-provider-api.js | 32 +- .../shortcuts/actions/shortcuts.test.js | 116 +++--- .../src/modules/ui/configs/handle_routing.js | 193 ++++++++-- .../src/modules/ui/containers/routed_link.js | 17 +- lib/ui/src/store.js | 337 ++++++++++++------ 7 files changed, 475 insertions(+), 230 deletions(-) diff --git a/examples/cra-kitchen-sink/.storybook/config.js b/examples/cra-kitchen-sink/.storybook/config.js index 0a2e0ad678e9..32940cec5b9c 100644 --- a/examples/cra-kitchen-sink/.storybook/config.js +++ b/examples/cra-kitchen-sink/.storybook/config.js @@ -7,11 +7,11 @@ setOptions({ goFullScreen: false, showAddonsPanel: true, showSearchBox: false, - addonPanelInRight: true, + addonPanelInRight: false, sortStoriesByKind: false, hierarchySeparator: /\./, hierarchyRootSeparator: /\|/, - enableShortcuts: false, + enableShortcuts: true, }); function loadStories() { diff --git a/lib/ui/src/index.js b/lib/ui/src/index.js index 02f5c26a5fb8..bf1b9a3e4ab6 100644 --- a/lib/ui/src/index.js +++ b/lib/ui/src/index.js @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'; import { Provider as MobxProvider } from 'mobx-react'; import ReactModal from 'react-modal'; +import handleRouting from './modules/ui/configs/handle_routing'; import App from './app'; import Provider from './provider'; import initProviderApi from './init-provider-api'; @@ -14,10 +15,11 @@ function renderStorybokUI(domNode, provider) { } const store = createStore(); + initProviderApi({ provider, store }); - const Preview = () => - provider.renderPreview(store.selectedKind, store.selectedStory); + handleRouting(store); + // store.selectAddonPanel(); // Tell react-modal which element to mark as aria-hidden ReactModal.setAppElement(domNode); diff --git a/lib/ui/src/init-provider-api.js b/lib/ui/src/init-provider-api.js index 2edf3454703e..ce79cd83fbe5 100644 --- a/lib/ui/src/init-provider-api.js +++ b/lib/ui/src/init-provider-api.js @@ -1,7 +1,8 @@ import { autorun } from 'mobx'; - import { EventEmitter } from 'events'; +import { getUrlState } from './modules/ui/configs/handle_routing'; + export default ({ provider, store }) => { const callbacks = new EventEmitter(); let currentKind; @@ -9,7 +10,6 @@ export default ({ provider, store }) => { const api = { onStory(cb) { - console.log('onStory'); callbacks.on('story', cb); if (currentKind && currentStory) { // Using a setTimeout to call the callback to make sure it's @@ -24,26 +24,30 @@ export default ({ provider, store }) => { setStories(stories) { store.setStories(stories); }, - selectInCurrentKind(...args) { - console.log('selectInCurrentKind', args); + selectInCurrentKind(story) { + store.selectInCurrentKind(story); }, - selectStory(...args) { - console.log('selectStory', args); + selectStory(kind, story) { + store.selectedStory(kind, story); }, handleShortcut(event) { store.handleEvent(event); }, - setQueryParams(...args) { - console.log('setQueryParams', args); + setOptions(options) { + store.setOptions(options); + store.setShortcutsOptions(options); }, - setOptions(...args) { - console.log('setOptions', args); + setQueryParams(customQueryParams) { + store.setQueryParams(customQueryParams); }, - getQueryParam(...args) { - console.log('getQueryParam', args); + getQueryParam(key) { + if (store.customQueryParams) { + return store.customQueryParams[key]; + } + return undefined; }, - getUrlState(...args) { - console.log('getUrlState', args); + getUrlState(overrideParams) { + return getUrlState({ ...store, ...overrideParams }); } }; diff --git a/lib/ui/src/modules/shortcuts/actions/shortcuts.test.js b/lib/ui/src/modules/shortcuts/actions/shortcuts.test.js index 81ad4b4a2257..370f06da2312 100644 --- a/lib/ui/src/modules/shortcuts/actions/shortcuts.test.js +++ b/lib/ui/src/modules/shortcuts/actions/shortcuts.test.js @@ -1,68 +1,68 @@ -import actions from './shortcuts'; +// import actions from './shortcuts'; -class MockClientStore { - update(cb) { - this.updateCallback = cb; - } -} +// class MockClientStore { +// update(cb) { +// this.updateCallback = cb; +// } +// } -describe('manager.shortcuts.actions.shortcuts', () => { - describe('setOptions', () => { - test('should update options', () => { - const clientStore = new MockClientStore(); - actions.setOptions({ clientStore }, { abc: 10 }); +// describe('manager.shortcuts.actions.shortcuts', () => { +// describe('setOptions', () => { +// test('should update options', () => { +// const clientStore = new MockClientStore(); +// actions.setOptions({ clientStore }, { abc: 10 }); - const state = { - shortcutOptions: { bbc: 50, abc: 40 }, - }; +// const state = { +// shortcutOptions: { bbc: 50, abc: 40 }, +// }; - const stateUpdates = clientStore.updateCallback(state); - expect(stateUpdates).toEqual({ - shortcutOptions: { bbc: 50, abc: 10 }, - }); - }); +// const stateUpdates = clientStore.updateCallback(state); +// expect(stateUpdates).toEqual({ +// shortcutOptions: { bbc: 50, abc: 10 }, +// }); +// }); - test('should only update options for the key already defined', () => { - const clientStore = new MockClientStore(); - actions.setOptions({ clientStore }, { abc: 10, kki: 50 }); +// test('should only update options for the key already defined', () => { +// const clientStore = new MockClientStore(); +// actions.setOptions({ clientStore }, { abc: 10, kki: 50 }); - const state = { - shortcutOptions: { bbc: 50, abc: 40 }, - }; +// const state = { +// shortcutOptions: { bbc: 50, abc: 40 }, +// }; - const stateUpdates = clientStore.updateCallback(state); - expect(stateUpdates).toEqual({ - shortcutOptions: { bbc: 50, abc: 10 }, - }); - }); +// const stateUpdates = clientStore.updateCallback(state); +// expect(stateUpdates).toEqual({ +// shortcutOptions: { bbc: 50, abc: 10 }, +// }); +// }); - test('should warn about deprecated option names', () => { - const clientStore = new MockClientStore(); - const spy = jest.spyOn(console, 'warn').mockImplementation(() => {}); - actions.setOptions( - { clientStore }, - { - showLeftPanel: 1, - showDownPanel: 2, - downPanelInRight: 3, - } - ); +// test('should warn about deprecated option names', () => { +// const clientStore = new MockClientStore(); +// const spy = jest.spyOn(console, 'warn').mockImplementation(() => {}); +// actions.setOptions( +// { clientStore }, +// { +// showLeftPanel: 1, +// showDownPanel: 2, +// downPanelInRight: 3, +// } +// ); - const state = { - shortcutOptions: {}, - }; - const stateUpdates = clientStore.updateCallback(state); - expect(spy).toHaveBeenCalledTimes(3); - expect(stateUpdates).toEqual({ - shortcutOptions: { - showStoriesPanel: 1, - showAddonPanel: 2, - addonPanelInRight: 3, - }, - }); +// const state = { +// shortcutOptions: {}, +// }; +// const stateUpdates = clientStore.updateCallback(state); +// expect(spy).toHaveBeenCalledTimes(3); +// expect(stateUpdates).toEqual({ +// shortcutOptions: { +// showStoriesPanel: 1, +// showAddonPanel: 2, +// addonPanelInRight: 3, +// }, +// }); - spy.mockReset(); - spy.mockRestore(); - }); - }); -}); +// spy.mockReset(); +// spy.mockRestore(); +// }); +// }); +// }); diff --git a/lib/ui/src/modules/ui/configs/handle_routing.js b/lib/ui/src/modules/ui/configs/handle_routing.js index 1b4e10432730..815102362fcf 100755 --- a/lib/ui/src/modules/ui/configs/handle_routing.js +++ b/lib/ui/src/modules/ui/configs/handle_routing.js @@ -1,21 +1,148 @@ +// import { window, location, history } from 'global'; +// import qs from 'qs'; + +// export const config = { +// insidePopState: false, +// }; + +// export function getUrlState(data) { +// const { selectedKind, selectedStory, customQueryParams } = data; + +// const { +// goFullScreen: full, +// showAddonPanel: addons, +// showStoriesPanel: stories, +// addonPanelInRight: panelRight, +// } = data.shortcutOptions; + +// const { selectedAddonPanel: addonPanel } = data; + +// const urlObj = { +// selectedKind, +// selectedStory, +// full: Number(full), +// addons: Number(addons), +// stories: Number(stories), +// panelRight: Number(panelRight), +// addonPanel, +// ...customQueryParams, +// }; + +// const url = `?${qs.stringify(urlObj)}`; + +// return { +// ...urlObj, +// full, +// addons, +// stories, +// panelRight, +// url, +// }; +// } + +// export function changeUrl(clientStore, usePush) { +// // Do not change the URL if we are inside a popState event. +// if (config.insidePopState) return; + +// const data = clientStore.getAll(); +// if (!data.selectedKind) return; + +// const state = getUrlState(data); +// history[usePush ? 'pushState' : 'replaceState'](state, '', state.url); +// } + +// export function updateStore(queryParams, actions) { +// const { +// selectedKind, +// selectedStory, +// full = 0, +// down = 1, +// addons = down, +// left = 1, +// stories = left, +// panelRight = 0, +// downPanel, +// addonPanel = downPanel, +// ...customQueryParams +// } = queryParams; + +// if (selectedKind) { +// actions.api.selectStory(selectedKind, selectedStory); +// } + +// actions.shortcuts.setOptions({ +// goFullScreen: Boolean(Number(full)), +// showAddonPanel: Boolean(Number(addons)), +// showStoriesPanel: Boolean(Number(stories)), +// addonPanelInRight: Boolean(Number(panelRight)), +// }); + +// if (addonPanel) { +// actions.ui.selectAddonPanel(addonPanel); +// } +// actions.api.setQueryParams(customQueryParams); +// } + +// export function handleInitialUrl(actions, l) { +// const queryString = l.search.substring(1); +// if (!queryString || queryString === '') return; + +// const parsedQs = qs.parse(queryString); +// updateStore(parsedQs, actions); +// } + +// export default function({ clientStore }, actions) { +// // handle initial URL +// handleInitialUrl(actions, location); + +// const data = clientStore.getAll(); +// let prevKind = data.selectedKind; +// let prevStory = data.selectedStory; + +// // subscribe to clientStore and change the URL +// clientStore.subscribe(() => { +// const { selectedKind, selectedStory } = clientStore.getAll(); +// // use pushState only when a new story is selected +// const usePush = +// prevKind != null && +// prevStory != null && +// (selectedKind !== prevKind || selectedStory !== prevStory); +// changeUrl(clientStore, usePush); +// prevKind = selectedKind; +// prevStory = selectedStory; +// }); +// changeUrl(clientStore); + +// // handle back button +// window.onpopstate = () => { +// config.insidePopState = true; +// handleInitialUrl(actions, location); +// config.insidePopState = false; +// }; +// } + import { window, location, history } from 'global'; +import { autorun } from 'mobx'; import qs from 'qs'; export const config = { - insidePopState: false, + insidePopState: false }; -export function getUrlState(data) { - const { selectedKind, selectedStory, customQueryParams } = data; +export function getUrlState(store) { + const { + selectedKind, + selectedStory, + customQueryParams, + selectedAddonPanel: addonPanel + } = store; const { goFullScreen: full, showAddonPanel: addons, showStoriesPanel: stories, - addonPanelInRight: panelRight, - } = data.shortcutOptions; - - const { selectedAddonPanel: addonPanel } = data; + addonPanelInRight: panelRight + } = store.shortcutOptions; const urlObj = { selectedKind, @@ -25,7 +152,7 @@ export function getUrlState(data) { stories: Number(stories), panelRight: Number(panelRight), addonPanel, - ...customQueryParams, + ...customQueryParams }; const url = `?${qs.stringify(urlObj)}`; @@ -36,22 +163,21 @@ export function getUrlState(data) { addons, stories, panelRight, - url, + url }; } -export function changeUrl(clientStore, usePush) { +export function changeUrl(store, usePush) { // Do not change the URL if we are inside a popState event. if (config.insidePopState) return; - const data = clientStore.getAll(); - if (!data.selectedKind) return; + if (!store.selectedKind) return; - const state = getUrlState(data); + const state = getUrlState(store); history[usePush ? 'pushState' : 'replaceState'](state, '', state.url); } -export function updateStore(queryParams, actions) { +export function updateStore(store, queryParams) { const { selectedKind, selectedStory, @@ -67,56 +193,55 @@ export function updateStore(queryParams, actions) { } = queryParams; if (selectedKind) { - actions.api.selectStory(selectedKind, selectedStory); + store.selectStory(selectedKind, selectedStory); } - actions.shortcuts.setOptions({ + store.setShortcutsOptions({ goFullScreen: Boolean(Number(full)), showAddonPanel: Boolean(Number(addons)), showStoriesPanel: Boolean(Number(stories)), - addonPanelInRight: Boolean(Number(panelRight)), + addonPanelInRight: Boolean(Number(panelRight)) }); if (addonPanel) { - actions.ui.selectAddonPanel(addonPanel); + store.selectAddonPanel(addonPanel); } - actions.api.setQueryParams(customQueryParams); + store.setQueryParams(customQueryParams); } -export function handleInitialUrl(actions, l) { +export function handleInitialUrl(store, l) { const queryString = l.search.substring(1); if (!queryString || queryString === '') return; const parsedQs = qs.parse(queryString); - updateStore(parsedQs, actions); + updateStore(store, parsedQs); } -export default function({ clientStore }, actions) { +export default function(store) { // handle initial URL - handleInitialUrl(actions, location); + handleInitialUrl(store, location); - const data = clientStore.getAll(); - let prevKind = data.selectedKind; - let prevStory = data.selectedStory; + let prevKind = store.selectedKind; + let prevStory = store.selectedStory; // subscribe to clientStore and change the URL - clientStore.subscribe(() => { - const { selectedKind, selectedStory } = clientStore.getAll(); + autorun(() => { // use pushState only when a new story is selected const usePush = prevKind != null && prevStory != null && - (selectedKind !== prevKind || selectedStory !== prevStory); - changeUrl(clientStore, usePush); - prevKind = selectedKind; - prevStory = selectedStory; + (store.selectedKind !== prevKind || store.selectedStory !== prevStory); + changeUrl(store, usePush); + prevKind = store.selectedKind; + prevStory = store.selectedStory; }); - changeUrl(clientStore); + + changeUrl(store); // handle back button window.onpopstate = () => { config.insidePopState = true; - handleInitialUrl(actions, location); + handleInitialUrl(store, location); config.insidePopState = false; }; } diff --git a/lib/ui/src/modules/ui/containers/routed_link.js b/lib/ui/src/modules/ui/containers/routed_link.js index add69863a3f1..11191670b6eb 100644 --- a/lib/ui/src/modules/ui/containers/routed_link.js +++ b/lib/ui/src/modules/ui/containers/routed_link.js @@ -5,20 +5,23 @@ import React from 'react'; import { inject } from 'mobx-react'; // import genPoddaLoader from '../libs/gen_podda_loader'; -// import { getUrlState } from '../configs/handle_routing'; +import { getUrlState } from '../configs/handle_routing'; // import compose from '../../../compose'; -export function mapper() { - // const { url } = getUrlState({ ...state, ...props.overrideParams }); +export function mapper(store, props) { + const { url } = getUrlState({ ...store, ...props.overrideParams }); return { - // href: url, - href: '' + href: url }; } -const ComposedMenuLink = inject(({ store }) => mapper(store))(MenuLink); -const ComposedRoutedLink = inject(({ store }) => mapper(store))(RoutedLink); +const ComposedMenuLink = inject(({ store }, props) => mapper(store, props))( + MenuLink +); +const ComposedRoutedLink = inject(({ store }, props) => mapper(store, props))( + RoutedLink +); export { ComposedMenuLink as MenuLink }; export { ComposedRoutedLink as RoutedLink }; diff --git a/lib/ui/src/store.js b/lib/ui/src/store.js index 837514651d07..85d99e649890 100644 --- a/lib/ui/src/store.js +++ b/lib/ui/src/store.js @@ -1,5 +1,20 @@ -import { observable } from 'mobx'; +import { observable, action, set } from 'mobx'; +import pick from 'lodash.pick'; + import { features } from './libs/key_events'; +import checkIfMobileDevice from './modules/ui/libs/is_mobile_device'; + +const { userAgent } = global.window.navigator; +const isMobileDevice = checkIfMobileDevice(userAgent); + +const deprecationMessage = (oldName, newName) => + `The ${oldName} option has been renamed to ${newName} and will not be available in the next major Storybook release. Please update your config.`; + +const renamedOptions = { + showLeftPanel: 'showStoriesPanel', + showDownPanel: 'showAddonPanel', + downPanelInRight: 'addonPanelInRight' +}; function ensureKind(storyKinds, selectedKind) { if (!storyKinds) return selectedKind; @@ -23,135 +38,231 @@ function ensureStory(storyKinds, selectedKind, selectedStory) { // if the selected story is non-existant, select the first story return kindInfo.stories[0]; } + +export function ensurePanel(panels, selectedPanel, currentPanel) { + if (Object.keys(panels).indexOf(selectedPanel) >= 0) return selectedPanel; + // if the selected panel is non-existant, select the current panel + // and output to console all available panels + const logger = console; + logger.group('Available Panels ID:'); + Object.keys(panels).forEach(panelID => + logger.log(`${panelID} (${panels[panelID].title})`) + ); + logger.groupEnd('Available Panels ID:'); + return currentPanel; +} + const createStore = () => { - const store = observable({ - stories: [], - selectedKind: undefined, - selectedStory: undefined, - showShortcutsHelp: false, - storyFilter: null, - selectedAddonPanel: null, - isMobileDevice: false, - shortcutOptions: { - goFullScreen: false, - showStoriesPanel: true, - showAddonPanel: true, - showSearchBox: false, - addonPanelInRight: false, - enableShortcuts: true - }, - uiOptions: { - name: 'STORYBOOK', - url: 'https://github.com/storybooks/storybook', - sortStoriesByKind: false, - hierarchySeparator: '/', - hierarchyRootSeparator: null, - sidebarAnimations: true, - theme: null - }, + const store = observable( + { + showShortcutsHelp: false, + storyFilter: null, + selectedAddonPanel: null, + isMobileDevice, + shortcutOptions: { + goFullScreen: false, + showStoriesPanel: !isMobileDevice, + showAddonPanel: true, + showSearchBox: false, + addonPanelInRight: false, + enableShortcuts: true + }, + uiOptions: { + name: 'STORYBOOK', + url: 'https://github.com/storybooks/storybook', + sortStoriesByKind: false, + hierarchySeparator: '/', + hierarchyRootSeparator: null, + sidebarAnimations: true, + theme: null + }, - jumpToStory(direction) { - const flatteredStories = []; - let currentIndex = -1; + setOptions(options) { + const newOptions = pick(options, Object.keys(this.uiOptions)); + set(this.uiOptions, newOptions); - this.stories.forEach(({ kind, stories }) => { - stories.forEach(story => { - flatteredStories.push({ kind, story }); - if (kind === this.selectedKind && story === this.selectedStory) { - currentIndex = flatteredStories.length - 1; - } - }); - }); + if (options.hasOwnProperty('selectedAddonPanel')) { + this.selectedAddonPanel = ensurePanel( + provider.getPanels(), + options.selectedAddonPanel, + this.selectedAddonPanel + ); + } + }, - const jumpedStory = flatteredStories[currentIndex + direction]; - if (!jumpedStory) { - return; - } + setShortcutsOptions(options) { + const updatedOptions = { + ...this.shortcutOptions, + ...pick(options, Object.keys(this.shortcutOptions)) + }; - this.selectedKind = jumpedStory.kind; - this.selectedStory = jumpedStory.story; - }, + const withNewNames = Object.keys(renamedOptions).reduce( + (acc, oldName) => { + const newName = renamedOptions[oldName]; - handleEvent(event) { - if (!this.shortcutOptions.enableShortcuts) return; + if (oldName in options && !(newName in options)) { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.warn(deprecationMessage(oldName, newName)); + } - switch (event) { - case features.NEXT_STORY: { - this.jumpToStory(1); - break; - } - case features.PREV_STORY: { - this.jumpToStory(-1); - break; - } - case features.FULLSCREEN: { - this.shortcutOptions.goFullScreen = !this.shortcutOptions - .goFullScreen; - break; - } - case features.ADDON_PANEL: { - this.shortcutOptions.showAddonPanel = !this.shortcutOptions - .showAddonPanel; - break; - } - case features.STORIES_PANEL: { - this.shortcutOptions.showStoriesPanel = !this.shortcutOptions - .showStoriesPanel; - break; - } - case features.SHOW_SEARCH: { - this.toggleSearchBox(); - break; + return { + ...acc, + [newName]: options[oldName] + }; + } + + return acc; + }, + updatedOptions + ); + + set(this.shortcutOptions, withNewNames); + }, + + jumpToStory(direction) { + const flatteredStories = []; + let currentIndex = -1; + + this.stories.forEach(({ kind, stories }) => { + stories.forEach(story => { + flatteredStories.push({ kind, story }); + if (kind === this.selectedKind && story === this.selectedStory) { + currentIndex = flatteredStories.length - 1; + } + }); + }); + + const jumpedStory = flatteredStories[currentIndex + direction]; + if (!jumpedStory) { + return; } - case features.ADDON_PANEL_IN_RIGHT: { - this.shortcutOptions.addonPanelInRight = !this.shortcutOptions - .addonPanelInRight; + + this.selectedKind = jumpedStory.kind; + this.selectedStory = jumpedStory.story; + }, + + handleEvent(event) { + if (!this.shortcutOptions.enableShortcuts) return; + + switch (event) { + case features.NEXT_STORY: { + this.jumpToStory(1); + break; + } + case features.PREV_STORY: { + this.jumpToStory(-1); + break; + } + case features.FULLSCREEN: { + this.shortcutOptions.goFullScreen = !this.shortcutOptions + .goFullScreen; + break; + } + case features.ADDON_PANEL: { + this.shortcutOptions.showAddonPanel = !this.shortcutOptions + .showAddonPanel; + break; + } + case features.STORIES_PANEL: { + this.shortcutOptions.showStoriesPanel = !this.shortcutOptions + .showStoriesPanel; + break; + } + case features.SHOW_SEARCH: { + this.toggleSearchBox(); + break; + } + case features.ADDON_PANEL_IN_RIGHT: { + this.shortcutOptions.addonPanelInRight = !this.shortcutOptions + .addonPanelInRight; + } + default: + return; } - default: - return; - } - }, + }, - toggleSearchBox() { - this.shortcutOptions.showSearchBox = !this.shortcutOptions.showSearchBox; - }, + toggleSearchBox() { + this.shortcutOptions.showSearchBox = !this.shortcutOptions + .showSearchBox; + }, - /** UI actions */ - setStoryFilter(filter) { - this.storyFilter = filter; - }, + /** UI actions */ + setStoryFilter(filter) { + this.storyFilter = filter; + }, - toggleShortcutsHelp() { - this.showShortcutsHelp = !this.showShortcutsHelp; - }, + toggleShortcutsHelp() { + this.showShortcutsHelp = !this.showShortcutsHelp; + }, - selectAddonPanel(panelName) { - this.selectedAddonPanel = panelName; - }, + selectAddonPanel(panelName) { + this.selectedAddonPanel = panelName; + }, - setStories(stories) { - const selectedKind = ensureKind(stories, this.selectedKind); - const currentSelectedStory = - this.selectedKind === selectedKind ? this.selectedStory : null; - const selectedStory = ensureStory( - stories, - selectedKind, - currentSelectedStory - ); - - this.stories = stories; - this.selectedStory = selectedStory; - this.selectedKind = selectedKind; - }, + setStories(stories) { + const selectedKind = ensureKind(stories, this.selectedKind); + const currentSelectedStory = + this.selectedKind === selectedKind ? this.selectedStory : null; + const selectedStory = ensureStory( + stories, + selectedKind, + currentSelectedStory + ); + + this.stories = stories; + this.selectedStory = selectedStory; + this.selectedKind = selectedKind; + }, + + selectStory(kind, story) { + const selectedKind = ensureKind(this.stories, kind); + const selectedStory = ensureStory(this.stories, selectedKind, story); + + this.selectedStory = selectedStory; + this.selectedKind = selectedKind; + }, + + selectInCurrentKind(story) { + const selectedStory = ensureStory( + state.stories, + state.selectedKind, + story + ); - selectStory(kind, story) { - const selectedKind = ensureKind(this.stories, kind); - const selectedStory = ensureStory(this.stories, selectedKind, story); + this.selectedStory = selectedStory; + }, - this.selectedStory = selectedStory; - this.selectedKind = selectedKind; + setQueryParams(customQueryParams) { + const updatedQueryParams = { + ...this.customQueryParams, + ...customQueryParams + }; + + Object.keys(customQueryParams).forEach(key => { + if (updatedQueryParams[key] === null) { + delete updatedQueryParams[key]; + } + }); + + this.customQueryParams = updatedQueryParams; + } + }, + { + setOptions: action, + setShortcutsOptions: action, + jumpToStory: action, + handleEvent: action, + toggleSearchBox: action, + setStoryFilter: action, + toggleShortcutsHelp: action, + selectAddonPanel: action, + setStories: action, + selectStory: action, + selectInCurrentKind: action, + setQueryParams: action } - }); + ); return store; }; From 4aa1f5eb9446119bb316c42fb8d55d4a8613a819 Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Wed, 8 Aug 2018 20:44:12 +0200 Subject: [PATCH 007/413] WIP: routing --- lib/ui/src/app.js | 89 +++++++++++++++++++++++++++++++++++++++++-- lib/ui/src/index.js | 14 +++++-- lib/ui/src/routing.js | 31 +++++++++++++++ lib/ui/src/store.js | 2 +- 4 files changed, 129 insertions(+), 7 deletions(-) create mode 100644 lib/ui/src/routing.js diff --git a/lib/ui/src/app.js b/lib/ui/src/app.js index 23679b85aff6..90f0dce51845 100644 --- a/lib/ui/src/app.js +++ b/lib/ui/src/app.js @@ -1,4 +1,6 @@ import React from 'react'; +import { autorun } from 'mobx'; +import qs from 'qs'; import keyEvents from './libs/key_events'; import Layout from './modules/ui/containers/layout'; @@ -8,16 +10,97 @@ import ShortcutsHelp from './modules/ui/containers/shortcuts_help'; import SearchBox from './modules/ui/containers/search_box'; class App extends React.Component { - componentDidMount() { - // Handle events + parseInitialRoute() { + const { location, store } = this.props; + + const { + selectedKind, + selectedStory, + full = 0, + down = 1, + addons = down, + left = 1, + stories = left, + panelRight = 0, + downPanel, + addonPanel = downPanel, + ...customQueryParams + } = qs.parse(location.search.substring(1)); + + if (selectedKind) { + store.selectStory(selectedKind, selectedStory); + } + + store.setShortcutsOptions({ + goFullScreen: Boolean(Number(full)), + showAddonPanel: Boolean(Number(addons)), + showStoriesPanel: Boolean(Number(stories)), + addonPanelInRight: Boolean(Number(panelRight)) + }); + + if (addonPanel) { + store.selectAddonPanel(addonPanel); + } + store.setQueryParams(customQueryParams); + } + + initUrlUpdater() { + const { history, store } = this.props; + + this.cancelHistoryWatching = autorun(() => { + const { + selectedKind, + selectedStory, + selectedAddonPanel, + shortcutOptions: { + goFullScreen, + showAddonPanel, + showStoriesPanel, + addonPanelInRight + } + } = store; + + history.push({ + pathname: '/', + search: qs.stringify({ + selectedKind: selectedKind, + selectedStory: selectedStory, + full: Number(goFullScreen), + addons: Number(showAddonPanel), + stories: Number(showStoriesPanel), + panelRight: Number(addonPanelInRight), + addonPanel: selectedAddonPanel + }) + }); + }); + } + + initKeysHandler() { + const { store } = this.props; document.addEventListener('keydown', e => { const parsedEvent = keyEvents(e); if (parsedEvent) { - this.props.store.handleEvent(parsedEvent); + store.handleEvent(parsedEvent); } }); } + componentDidMount() { + setTimeout(() => { + this.initKeysHandler(); + this.parseInitialRoute(); + // this.initUrlUpdater(); + }, 0); + } + + componentWillUnmount() { + this.cancelHistoryWatching(); + } + + shouldComponentUpdate() { + return false; + } + render() { const { provider, store } = this.props; diff --git a/lib/ui/src/index.js b/lib/ui/src/index.js index bf1b9a3e4ab6..b1ce5ac0b066 100644 --- a/lib/ui/src/index.js +++ b/lib/ui/src/index.js @@ -8,17 +8,18 @@ import App from './app'; import Provider from './provider'; import initProviderApi from './init-provider-api'; import createStore from './store'; +import { Router, Route } from './routing'; function renderStorybokUI(domNode, provider) { if (!(provider instanceof Provider)) { throw new Error('provider is not extended from the base Provider'); } - const store = createStore(); + const store = createStore({ provider }); initProviderApi({ provider, store }); - handleRouting(store); + // handleRouting(store); // store.selectAddonPanel(); // Tell react-modal which element to mark as aria-hidden @@ -31,7 +32,14 @@ function renderStorybokUI(domNode, provider) { const root = ( - + + ( + + )} + /> + ); diff --git a/lib/ui/src/routing.js b/lib/ui/src/routing.js new file mode 100644 index 000000000000..fd117eff5ed4 --- /dev/null +++ b/lib/ui/src/routing.js @@ -0,0 +1,31 @@ +import React from 'react'; +import { matchPath } from 'react-router'; +import { BrowserRouter as Router, Route, Link } from 'react-router-dom'; +import qs from 'qs'; + +const QueryLink = props => ( + +); + +const QueryRoute = ({ path, render }) => ( + { + const match = matchPath(path, { + // keep backwards compatibility by asserting /components as default + path: + qs.parse(props.location.search, { ignoreQueryPrefix: true }).path || + '/components' + }); + + if (match) { + return render({ ...props, match }); + } + return null; + }} + /> +); + +export { Router, QueryRoute as Route, QueryLink as Link }; diff --git a/lib/ui/src/store.js b/lib/ui/src/store.js index 85d99e649890..8ffe687b1148 100644 --- a/lib/ui/src/store.js +++ b/lib/ui/src/store.js @@ -52,7 +52,7 @@ export function ensurePanel(panels, selectedPanel, currentPanel) { return currentPanel; } -const createStore = () => { +const createStore = ({ provider }) => { const store = observable( { showShortcutsHelp: false, From 5ba1760d42b6c321d014a36d9f56a325393ee56d Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Wed, 8 Aug 2018 23:51:30 +0200 Subject: [PATCH 008/413] remove stuff related to podda / mantra --- lib/ui/package.json | 2 + lib/ui/src/app.js | 115 +---- .../__snapshots__/header.stories.storyshot | 0 .../__snapshots__/menu_item.stories.storyshot | 0 .../search_box.stories.storyshot | 0 .../{modules/ui => }/components/menu_item.js | 0 .../ui => }/components/menu_item.stories.js | 0 .../ui => }/components/menu_item.test.js | 0 .../{modules/ui => }/components/search_box.js | 0 .../ui => }/components/search_box.stories.js | 0 .../ui => }/components/search_box.test.js | 0 .../ui => }/components/shortcuts_help.js | 0 .../components/shortcuts_help.stories.js | 0 .../__snapshots__/index.stories.storyshot | 0 .../text_filter.stories.storyshot | 0 .../ui => }/components/stories_panel/index.js | 0 .../components/stories_panel/index.stories.js | 62 +++ .../components/stories_panel/index.test.js | 0 .../__snapshots__/index.stories.storyshot | 0 .../stories_panel/stories_tree/index.js | 0 .../stories_tree/index.stories.js | 97 +++++ .../stories_panel/stories_tree/index.test.js | 411 ++++++++++++++++++ .../stories_tree/tree_decorators.js | 0 .../stories_tree/tree_decorators_utils.js | 0 .../tree_decorators_utils.test.js | 0 .../stories_panel/stories_tree/tree_header.js | 0 .../stories_tree/tree_header.test.js | 0 .../stories_tree/tree_node_type.js | 0 .../stories_panel/stories_tree/tree_style.js | 0 .../components/stories_panel/text_filter.js | 0 .../stories_panel/text_filter.stories.js | 0 .../stories_panel/text_filter.test.js | 0 .../ui => }/containers/addon_panel.js | 2 +- .../ui => }/containers/addon_panel.test.js | 0 .../src/{modules/ui => }/containers/header.js | 15 +- .../ui => }/containers/header.test.js | 0 .../src/{modules/ui => }/containers/layout.js | 2 +- lib/ui/src/containers/routed_link.js | 14 + .../ui => }/containers/routed_link.test.js | 0 lib/ui/src/containers/search_box.js | 12 + .../ui => }/containers/search_box.test.js | 0 .../ui => }/containers/shortcuts_help.js | 2 +- .../ui => }/containers/shortcuts_help.test.js | 0 .../ui => }/containers/stories_panel.js | 18 +- .../ui => }/containers/stories_panel.test.js | 0 lib/ui/src/index.js | 28 +- lib/ui/src/init-history-handler.js | 41 ++ lib/ui/src/init-key-handler.js | 13 + lib/ui/src/init-provider-api.js | 15 +- lib/ui/src/{modules/ui => }/libs/filters.js | 6 +- .../src/{modules/ui => }/libs/filters.test.js | 14 + lib/ui/src/{modules/ui => }/libs/hierarchy.js | 0 .../{modules/ui => }/libs/hierarchy.test.js | 0 .../{modules/ui => }/libs/is_mobile_device.js | 0 .../ui => }/libs/is_mobile_device.test.js | 0 lib/ui/src/modules/api/actions/api.js | 137 ------ lib/ui/src/modules/api/actions/api.test.js | 319 -------------- lib/ui/src/modules/api/actions/index.js | 5 - lib/ui/src/modules/api/configs/init_api.js | 65 --- .../src/modules/api/configs/init_api.test.js | 187 -------- lib/ui/src/modules/api/index.js | 20 - lib/ui/src/modules/shortcuts/actions/index.js | 5 - .../modules/shortcuts/actions/shortcuts.js | 89 ---- .../shortcuts/actions/shortcuts.test.js | 68 --- lib/ui/src/modules/shortcuts/index.js | 20 - lib/ui/src/modules/ui/actions/index.js | 5 - lib/ui/src/modules/ui/actions/ui.js | 13 - lib/ui/src/modules/ui/actions/ui.test.js | 38 -- .../components/stories_panel/index.stories.js | 62 --- .../stories_tree/index.stories.js | 97 ----- .../stories_panel/stories_tree/index.test.js | 411 ------------------ .../modules/ui/configs/handle_keyevents.js | 11 - .../ui/configs/handle_keyevents.test.js | 91 ---- .../src/modules/ui/configs/handle_routing.js | 247 ----------- .../modules/ui/configs/handle_routing.test.js | 130 ------ lib/ui/src/modules/ui/configs/init_panels.js | 7 - .../modules/ui/configs/init_panels.test.js | 25 -- .../src/modules/ui/containers/layout.test.js | 18 - .../src/modules/ui/containers/routed_link.js | 27 -- .../src/modules/ui/containers/search_box.js | 15 - lib/ui/src/modules/ui/index.js | 18 - .../src/modules/ui/libs/gen_podda_loader.js | 18 - .../modules/ui/libs/gen_podda_loader.test.js | 114 ----- lib/ui/src/modules/ui/routes.js | 36 -- lib/ui/src/routing.js | 33 +- lib/ui/src/store.js | 140 +++--- yarn.lock | 12 +- 87 files changed, 819 insertions(+), 2533 deletions(-) rename lib/ui/src/{modules/ui => }/components/__snapshots__/header.stories.storyshot (100%) rename lib/ui/src/{modules/ui => }/components/__snapshots__/menu_item.stories.storyshot (100%) rename lib/ui/src/{modules/ui => }/components/__snapshots__/search_box.stories.storyshot (100%) rename lib/ui/src/{modules/ui => }/components/menu_item.js (100%) rename lib/ui/src/{modules/ui => }/components/menu_item.stories.js (100%) rename lib/ui/src/{modules/ui => }/components/menu_item.test.js (100%) rename lib/ui/src/{modules/ui => }/components/search_box.js (100%) rename lib/ui/src/{modules/ui => }/components/search_box.stories.js (100%) rename lib/ui/src/{modules/ui => }/components/search_box.test.js (100%) rename lib/ui/src/{modules/ui => }/components/shortcuts_help.js (100%) rename lib/ui/src/{modules/ui => }/components/shortcuts_help.stories.js (100%) rename lib/ui/src/{modules/ui => }/components/stories_panel/__snapshots__/index.stories.storyshot (100%) rename lib/ui/src/{modules/ui => }/components/stories_panel/__snapshots__/text_filter.stories.storyshot (100%) rename lib/ui/src/{modules/ui => }/components/stories_panel/index.js (100%) create mode 100644 lib/ui/src/components/stories_panel/index.stories.js rename lib/ui/src/{modules/ui => }/components/stories_panel/index.test.js (100%) rename lib/ui/src/{modules/ui => }/components/stories_panel/stories_tree/__snapshots__/index.stories.storyshot (100%) rename lib/ui/src/{modules/ui => }/components/stories_panel/stories_tree/index.js (100%) create mode 100644 lib/ui/src/components/stories_panel/stories_tree/index.stories.js create mode 100644 lib/ui/src/components/stories_panel/stories_tree/index.test.js rename lib/ui/src/{modules/ui => }/components/stories_panel/stories_tree/tree_decorators.js (100%) rename lib/ui/src/{modules/ui => }/components/stories_panel/stories_tree/tree_decorators_utils.js (100%) rename lib/ui/src/{modules/ui => }/components/stories_panel/stories_tree/tree_decorators_utils.test.js (100%) rename lib/ui/src/{modules/ui => }/components/stories_panel/stories_tree/tree_header.js (100%) rename lib/ui/src/{modules/ui => }/components/stories_panel/stories_tree/tree_header.test.js (100%) rename lib/ui/src/{modules/ui => }/components/stories_panel/stories_tree/tree_node_type.js (100%) rename lib/ui/src/{modules/ui => }/components/stories_panel/stories_tree/tree_style.js (100%) rename lib/ui/src/{modules/ui => }/components/stories_panel/text_filter.js (100%) rename lib/ui/src/{modules/ui => }/components/stories_panel/text_filter.stories.js (100%) rename lib/ui/src/{modules/ui => }/components/stories_panel/text_filter.test.js (100%) rename lib/ui/src/{modules/ui => }/containers/addon_panel.js (84%) rename lib/ui/src/{modules/ui => }/containers/addon_panel.test.js (100%) rename lib/ui/src/{modules/ui => }/containers/header.js (74%) rename lib/ui/src/{modules/ui => }/containers/header.test.js (100%) rename lib/ui/src/{modules/ui => }/containers/layout.js (96%) create mode 100644 lib/ui/src/containers/routed_link.js rename lib/ui/src/{modules/ui => }/containers/routed_link.test.js (100%) create mode 100644 lib/ui/src/containers/search_box.js rename lib/ui/src/{modules/ui => }/containers/search_box.test.js (100%) rename lib/ui/src/{modules/ui => }/containers/shortcuts_help.js (85%) rename lib/ui/src/{modules/ui => }/containers/shortcuts_help.test.js (100%) rename lib/ui/src/{modules/ui => }/containers/stories_panel.js (84%) rename lib/ui/src/{modules/ui => }/containers/stories_panel.test.js (100%) create mode 100644 lib/ui/src/init-history-handler.js create mode 100644 lib/ui/src/init-key-handler.js rename lib/ui/src/{modules/ui => }/libs/filters.js (94%) rename lib/ui/src/{modules/ui => }/libs/filters.test.js (89%) rename lib/ui/src/{modules/ui => }/libs/hierarchy.js (100%) rename lib/ui/src/{modules/ui => }/libs/hierarchy.test.js (100%) rename lib/ui/src/{modules/ui => }/libs/is_mobile_device.js (100%) rename lib/ui/src/{modules/ui => }/libs/is_mobile_device.test.js (100%) delete mode 100755 lib/ui/src/modules/api/actions/api.js delete mode 100755 lib/ui/src/modules/api/actions/api.test.js delete mode 100755 lib/ui/src/modules/api/actions/index.js delete mode 100644 lib/ui/src/modules/api/configs/init_api.js delete mode 100644 lib/ui/src/modules/api/configs/init_api.test.js delete mode 100755 lib/ui/src/modules/api/index.js delete mode 100755 lib/ui/src/modules/shortcuts/actions/index.js delete mode 100755 lib/ui/src/modules/shortcuts/actions/shortcuts.js delete mode 100644 lib/ui/src/modules/shortcuts/actions/shortcuts.test.js delete mode 100755 lib/ui/src/modules/shortcuts/index.js delete mode 100755 lib/ui/src/modules/ui/actions/index.js delete mode 100755 lib/ui/src/modules/ui/actions/ui.js delete mode 100755 lib/ui/src/modules/ui/actions/ui.test.js delete mode 100644 lib/ui/src/modules/ui/components/stories_panel/index.stories.js delete mode 100644 lib/ui/src/modules/ui/components/stories_panel/stories_tree/index.stories.js delete mode 100644 lib/ui/src/modules/ui/components/stories_panel/stories_tree/index.test.js delete mode 100755 lib/ui/src/modules/ui/configs/handle_keyevents.js delete mode 100755 lib/ui/src/modules/ui/configs/handle_keyevents.test.js delete mode 100755 lib/ui/src/modules/ui/configs/handle_routing.js delete mode 100755 lib/ui/src/modules/ui/configs/handle_routing.test.js delete mode 100644 lib/ui/src/modules/ui/configs/init_panels.js delete mode 100644 lib/ui/src/modules/ui/configs/init_panels.test.js delete mode 100755 lib/ui/src/modules/ui/containers/layout.test.js delete mode 100644 lib/ui/src/modules/ui/containers/routed_link.js delete mode 100644 lib/ui/src/modules/ui/containers/search_box.js delete mode 100755 lib/ui/src/modules/ui/index.js delete mode 100644 lib/ui/src/modules/ui/libs/gen_podda_loader.js delete mode 100644 lib/ui/src/modules/ui/libs/gen_podda_loader.test.js delete mode 100755 lib/ui/src/modules/ui/routes.js diff --git a/lib/ui/package.json b/lib/ui/package.json index 69c10a4be072..beba08d0742a 100644 --- a/lib/ui/package.json +++ b/lib/ui/package.json @@ -28,6 +28,7 @@ "events": "^3.0.0", "fuse.js": "^3.2.1", "global": "^4.3.2", + "history": "^4.7.2", "keycode": "^2.2.0", "lodash.debounce": "^4.0.8", "lodash.pick": "^4.4.0", @@ -41,6 +42,7 @@ "react-fuzzy": "^0.5.2", "react-lifecycles-compat": "^3.0.4", "react-modal": "^3.5.1", + "react-router": "^4.3.1", "react-router-dom": "^4.3.1" }, "devDependencies": { diff --git a/lib/ui/src/app.js b/lib/ui/src/app.js index 90f0dce51845..0e47480eafe4 100644 --- a/lib/ui/src/app.js +++ b/lib/ui/src/app.js @@ -1,102 +1,13 @@ import React from 'react'; -import { autorun } from 'mobx'; -import qs from 'qs'; +import PropTypes from 'prop-types'; -import keyEvents from './libs/key_events'; -import Layout from './modules/ui/containers/layout'; -import StoriesPanel from './modules/ui/containers/stories_panel'; -import AddonPanel from './modules/ui/containers/addon_panel'; -import ShortcutsHelp from './modules/ui/containers/shortcuts_help'; -import SearchBox from './modules/ui/containers/search_box'; +import Layout from './containers/layout'; +import StoriesPanel from './containers/stories_panel'; +import AddonPanel from './containers/addon_panel'; +import ShortcutsHelp from './containers/shortcuts_help'; +import SearchBox from './containers/search_box'; class App extends React.Component { - parseInitialRoute() { - const { location, store } = this.props; - - const { - selectedKind, - selectedStory, - full = 0, - down = 1, - addons = down, - left = 1, - stories = left, - panelRight = 0, - downPanel, - addonPanel = downPanel, - ...customQueryParams - } = qs.parse(location.search.substring(1)); - - if (selectedKind) { - store.selectStory(selectedKind, selectedStory); - } - - store.setShortcutsOptions({ - goFullScreen: Boolean(Number(full)), - showAddonPanel: Boolean(Number(addons)), - showStoriesPanel: Boolean(Number(stories)), - addonPanelInRight: Boolean(Number(panelRight)) - }); - - if (addonPanel) { - store.selectAddonPanel(addonPanel); - } - store.setQueryParams(customQueryParams); - } - - initUrlUpdater() { - const { history, store } = this.props; - - this.cancelHistoryWatching = autorun(() => { - const { - selectedKind, - selectedStory, - selectedAddonPanel, - shortcutOptions: { - goFullScreen, - showAddonPanel, - showStoriesPanel, - addonPanelInRight - } - } = store; - - history.push({ - pathname: '/', - search: qs.stringify({ - selectedKind: selectedKind, - selectedStory: selectedStory, - full: Number(goFullScreen), - addons: Number(showAddonPanel), - stories: Number(showStoriesPanel), - panelRight: Number(addonPanelInRight), - addonPanel: selectedAddonPanel - }) - }); - }); - } - - initKeysHandler() { - const { store } = this.props; - document.addEventListener('keydown', e => { - const parsedEvent = keyEvents(e); - if (parsedEvent) { - store.handleEvent(parsedEvent); - } - }); - } - - componentDidMount() { - setTimeout(() => { - this.initKeysHandler(); - this.parseInitialRoute(); - // this.initUrlUpdater(); - }, 0); - } - - componentWillUnmount() { - this.cancelHistoryWatching(); - } - shouldComponentUpdate() { return false; } @@ -104,8 +15,7 @@ class App extends React.Component { render() { const { provider, store } = this.props; - const Preview = () => - provider.renderPreview(store.selectedKind, store.selectedStory); + const Preview = () => provider.renderPreview(store.selectedKind, store.selectedStory); return ( ( +// +// )) +// .add('with storiesHierarchies prop', () => ( +// +// )) +// .add('storiesHierarchies exists but is empty', () => ( +// +// )) +// .add('when open on mobile device', () => ( +// +// )); diff --git a/lib/ui/src/modules/ui/components/stories_panel/index.test.js b/lib/ui/src/components/stories_panel/index.test.js similarity index 100% rename from lib/ui/src/modules/ui/components/stories_panel/index.test.js rename to lib/ui/src/components/stories_panel/index.test.js diff --git a/lib/ui/src/modules/ui/components/stories_panel/stories_tree/__snapshots__/index.stories.storyshot b/lib/ui/src/components/stories_panel/stories_tree/__snapshots__/index.stories.storyshot similarity index 100% rename from lib/ui/src/modules/ui/components/stories_panel/stories_tree/__snapshots__/index.stories.storyshot rename to lib/ui/src/components/stories_panel/stories_tree/__snapshots__/index.stories.storyshot diff --git a/lib/ui/src/modules/ui/components/stories_panel/stories_tree/index.js b/lib/ui/src/components/stories_panel/stories_tree/index.js similarity index 100% rename from lib/ui/src/modules/ui/components/stories_panel/stories_tree/index.js rename to lib/ui/src/components/stories_panel/stories_tree/index.js diff --git a/lib/ui/src/components/stories_panel/stories_tree/index.stories.js b/lib/ui/src/components/stories_panel/stories_tree/index.stories.js new file mode 100644 index 000000000000..516fc8f218a5 --- /dev/null +++ b/lib/ui/src/components/stories_panel/stories_tree/index.stories.js @@ -0,0 +1,97 @@ +// import React from 'react'; +// import { storiesOf } from '@storybook/react'; + +// import Stories from './index'; +// import { setContext } from '../../../../../compose'; +// import { createHierarchies, prepareStoriesForHierarchy } from '../../../libs/hierarchy'; +// import { storyFilter } from '../../../libs/filters'; +// import withLifecyle from '../../../libs/withLifecycleDecorator'; + +// const data = createHierarchies([ +// { kind: 'a', name: 'a', namespaces: ['a'], stories: ['a1', 'a2'] }, +// { kind: 'b', name: 'b', namespaces: ['b'], stories: ['b1', 'b2'] }, +// ]); + +// const initialData = [ +// { +// kind: 'some.name.item1', +// stories: ['a1', 'a2'], +// }, +// { +// kind: 'another.space.20', +// stories: ['b1', 'b2'], +// }, +// ]; +// const dataWithoutSeparator = createHierarchies(prepareStoriesForHierarchy(initialData)); +// const dataWithSeparator = createHierarchies(prepareStoriesForHierarchy(initialData, '\\.')); + +// const filter = 'th'; +// const selectedKind = 'another.space.20'; + +// const filteredData = storyFilter( +// prepareStoriesForHierarchy(initialData, '\\.'), +// filter, +// selectedKind +// ); + +// const filteredDataHierarchy = createHierarchies(filteredData); + +// const decorator = withLifecyle({ +// beforeEach() { +// setContext({ +// clientStore: { +// getAll() { +// return { shortcutOptions: {} }; +// }, +// subscribe() {}, +// }, +// }); +// }, +// afterEach() { +// setContext(null); +// }, +// }); + +// storiesOf('UI|stories/Stories', module) +// .addDecorator(decorator) +// .add('empty', () => ( +// +// )) +// .add('simple', () => ( +// +// )) +// .add('with hierarchy - hierarchySeparator is defined', () => ( +// +// )) +// .add('without hierarchy - hierarchySeparator is defined', () => ( +// +// )) +// .add('with highlighting when storiesFilter is provided', () => ( +// +// )); diff --git a/lib/ui/src/components/stories_panel/stories_tree/index.test.js b/lib/ui/src/components/stories_panel/stories_tree/index.test.js new file mode 100644 index 000000000000..d9c4d382f185 --- /dev/null +++ b/lib/ui/src/components/stories_panel/stories_tree/index.test.js @@ -0,0 +1,411 @@ +// import { shallow, mount } from 'enzyme'; +// import React from 'react'; +// import Stories from './index'; +// import { setContext } from '../../../../../compose'; +// import { +// createHierarchies, +// createHierarchyRoot, +// prepareStoriesForHierarchy, +// } from '../../../libs/hierarchy'; +// import { storyFilter } from '../../../libs/filters'; + +// const leftClick = { button: 0 }; + +// describe('manager.ui.components.stories_panel.stories_tree', () => { +// beforeEach(() => +// setContext({ +// clientStore: { +// getAll() { +// return { shortcutOptions: {} }; +// }, +// subscribe() {}, +// }, +// })); + +// afterEach(() => setContext(null)); + +// const data = createHierarchies([ +// { kind: 'a', name: 'a', namespaces: ['a'], stories: ['a1', 'a2'] }, +// { kind: 'b', name: 'b', namespaces: ['b'], stories: ['b1', 'b2'] }, +// ])[0]; + +// const initialData = [ +// { +// kind: 'some.name.item1', +// stories: ['a1', 'a2'], +// }, +// { +// kind: 'another.space.20', +// stories: ['b1', 'b2'], +// }, +// ]; + +// const dataWithoutSeparator = createHierarchies(prepareStoriesForHierarchy(initialData))[0]; +// const dataWithSeparator = createHierarchies(prepareStoriesForHierarchy(initialData, '\\.'))[0]; + +// describe('render', () => { +// test('should render stories - empty', () => { +// const wrap = shallow( +// +// ); + +// const list = wrap +// .find('div') +// .first() +// .children('div') +// .last(); + +// expect(list.text()).toBe(''); +// }); + +// test('should render stories', () => { +// const wrap = shallow( +// +// ); + +// const output = wrap.html(); + +// expect(output).toMatch(/b/); +// expect(output).toMatch(/b2/); +// }); + +// test('should render stories with hierarchy - hierarchySeparator is defined', () => { +// const wrap = shallow( +// +// ); + +// const output = wrap.html(); + +// expect(output).toMatch(/some/); +// expect(output).not.toMatch(/>name { +// const wrap = shallow( +// +// ); + +// const output = wrap.html(); + +// expect(output).toMatch(/some.name.item1/); +// expect(output).not.toMatch(/a1/); +// expect(output).not.toMatch(/a2/); +// expect(output).toMatch(/another.space.20/); +// expect(output).toMatch(/b1/); +// expect(output).toMatch(/b2/); +// }); + +// test('should render stories with initially selected nodes according to the selectedHierarchy', () => { +// const wrap = shallow( +// +// ); + +// const { nodes } = wrap.state(); + +// expect(nodes).toEqual({ +// 'another@namespace': true, +// 'another@space@namespace': true, +// 'another@space@20@namespace': true, +// }); +// }); + +// test('should contain state with all selected nodes after clicking on the nodes', () => { +// const wrap = mount( +// +// ); + +// const kind = wrap.find('[data-name="some"]').first(); +// kind.simulate('click', leftClick); + +// const { nodes } = wrap.state(); + +// expect(nodes).toEqual({ +// 'another@namespace': true, +// 'another@space@namespace': true, +// 'another@space@20@namespace': true, +// 'some@namespace': true, +// }); +// }); + +// test('should recalculate selected nodes after selectedHierarchy changes', () => { +// const wrap = mount( +// +// ); + +// wrap.setProps({ selectedHierarchy: ['another', 'space', '20'] }); + +// const { nodes } = wrap.state(); + +// expect(nodes).toEqual({ +// 'another@namespace': true, +// 'another@space@namespace': true, +// 'another@space@20@namespace': true, +// }); +// }); + +// test('should add selected nodes to the state after selectedHierarchy changes with a new value', () => { +// const wrap = mount( +// +// ); + +// wrap.setProps({ selectedHierarchy: ['some', 'name', 'item1'] }); + +// const { nodes } = wrap.state(); + +// expect(nodes).toEqual({ +// 'another@namespace': true, +// 'another@space@namespace': true, +// 'another@space@20@namespace': true, +// 'some@namespace': true, +// 'some@name@namespace': true, +// 'some@name@item1@namespace': true, +// }); +// }); + +// test('should not call setState when selectedHierarchy prop changes with the same value', () => { +// const selectedHierarchy = ['another', 'space', '20']; +// const wrap = mount( +// +// ); + +// const setState = jest.fn(); +// wrap.instance().setState = setState; + +// wrap.setProps({ selectedHierarchy }); + +// expect(setState).not.toHaveBeenCalled(); +// }); + +// test('should render stories with with highlighting when storiesFilter is provided', () => { +// const filter = 'th'; +// const selectedKind = 'another.space.20'; + +// const filteredData = storyFilter( +// prepareStoriesForHierarchy(initialData, '\\.'), +// filter, +// selectedKind +// ); + +// const filteredDataHierarchy = createHierarchies(filteredData); + +// const wrap = mount( +// +// ); + +// const highlightedElements = wrap.find('strong'); + +// expect(highlightedElements.text()).toBe('th'); +// }); +// }); + +// describe('events', () => { +// test('should not call the onSelectStory prop when a collapsed kind is clicked', () => { +// const onSelectStory = jest.fn(); +// const wrap = mount( +// +// ); + +// const kind = wrap.find('[data-name="a"]').first(); +// kind.simulate('click', leftClick); + +// expect(onSelectStory).not.toHaveBeenCalled(); +// }); + +// test("shouldn't call the onSelectStory prop when an expanded kind is clicked", () => { +// const onSelectStory = jest.fn(); +// const wrap = mount( +// +// ); + +// const kind = wrap +// .find('[data-name="a"]') +// .filterWhere(el => el.text() === 'a') +// .last(); +// kind.simulate('click'); + +// onSelectStory.mockClear(); +// kind.simulate('click'); + +// expect(onSelectStory).not.toHaveBeenCalled(); +// }); + +// test('should call the onSelectStory prop when a story is clicked', () => { +// const onSelectStory = jest.fn(); +// const wrap = mount( +// +// ); + +// const kind = wrap.find('[data-name="b1"]').first(); +// kind.simulate('click', leftClick); + +// expect(onSelectStory).toHaveBeenCalledWith('b', 'b1'); +// }); + +// test('should call the onSelectStory prop when a story is clicked - hierarchySeparator is defined', () => { +// const onSelectStory = jest.fn(); +// const wrap = mount( +// +// ); + +// wrap +// .find('[data-name="another"]') +// .first() +// .simulate('click', leftClick); + +// wrap +// .find('[data-name="space"]') +// .first() +// .simulate('click', leftClick); + +// wrap +// .find('[data-name="20"]') +// .first() +// .simulate('click', leftClick); + +// expect(onSelectStory).not.toHaveBeenCalled(); + +// wrap +// .find('[data-name="b2"]') +// .first() +// .simulate('click', leftClick); + +// expect(onSelectStory).toHaveBeenCalledWith('another.space.20', 'b2'); +// }); + +// test('should call the onSelectStory prop when a story is selected with enter key', () => { +// const onSelectStory = jest.fn(); +// const wrap = mount( +// +// ); + +// wrap +// .find('[data-name="another"]') +// .first() +// .simulate('keyDown', { keyCode: 13 }); + +// expect(onSelectStory).not.toHaveBeenCalled(); + +// wrap +// .find('[data-name="space"]') +// .first() +// .simulate('keyDown', { keyCode: 13 }); + +// // enter press on native link triggers click event +// wrap +// .find('[data-name="20"]') +// .first() +// .simulate('click', leftClick); + +// wrap +// .find('[data-name="b2"]') +// .first() +// .simulate('click', leftClick); + +// expect(onSelectStory).toHaveBeenCalledWith('another.space.20', 'b2'); +// }); +// }); +// }); diff --git a/lib/ui/src/modules/ui/components/stories_panel/stories_tree/tree_decorators.js b/lib/ui/src/components/stories_panel/stories_tree/tree_decorators.js similarity index 100% rename from lib/ui/src/modules/ui/components/stories_panel/stories_tree/tree_decorators.js rename to lib/ui/src/components/stories_panel/stories_tree/tree_decorators.js diff --git a/lib/ui/src/modules/ui/components/stories_panel/stories_tree/tree_decorators_utils.js b/lib/ui/src/components/stories_panel/stories_tree/tree_decorators_utils.js similarity index 100% rename from lib/ui/src/modules/ui/components/stories_panel/stories_tree/tree_decorators_utils.js rename to lib/ui/src/components/stories_panel/stories_tree/tree_decorators_utils.js diff --git a/lib/ui/src/modules/ui/components/stories_panel/stories_tree/tree_decorators_utils.test.js b/lib/ui/src/components/stories_panel/stories_tree/tree_decorators_utils.test.js similarity index 100% rename from lib/ui/src/modules/ui/components/stories_panel/stories_tree/tree_decorators_utils.test.js rename to lib/ui/src/components/stories_panel/stories_tree/tree_decorators_utils.test.js diff --git a/lib/ui/src/modules/ui/components/stories_panel/stories_tree/tree_header.js b/lib/ui/src/components/stories_panel/stories_tree/tree_header.js similarity index 100% rename from lib/ui/src/modules/ui/components/stories_panel/stories_tree/tree_header.js rename to lib/ui/src/components/stories_panel/stories_tree/tree_header.js diff --git a/lib/ui/src/modules/ui/components/stories_panel/stories_tree/tree_header.test.js b/lib/ui/src/components/stories_panel/stories_tree/tree_header.test.js similarity index 100% rename from lib/ui/src/modules/ui/components/stories_panel/stories_tree/tree_header.test.js rename to lib/ui/src/components/stories_panel/stories_tree/tree_header.test.js diff --git a/lib/ui/src/modules/ui/components/stories_panel/stories_tree/tree_node_type.js b/lib/ui/src/components/stories_panel/stories_tree/tree_node_type.js similarity index 100% rename from lib/ui/src/modules/ui/components/stories_panel/stories_tree/tree_node_type.js rename to lib/ui/src/components/stories_panel/stories_tree/tree_node_type.js diff --git a/lib/ui/src/modules/ui/components/stories_panel/stories_tree/tree_style.js b/lib/ui/src/components/stories_panel/stories_tree/tree_style.js similarity index 100% rename from lib/ui/src/modules/ui/components/stories_panel/stories_tree/tree_style.js rename to lib/ui/src/components/stories_panel/stories_tree/tree_style.js diff --git a/lib/ui/src/modules/ui/components/stories_panel/text_filter.js b/lib/ui/src/components/stories_panel/text_filter.js similarity index 100% rename from lib/ui/src/modules/ui/components/stories_panel/text_filter.js rename to lib/ui/src/components/stories_panel/text_filter.js diff --git a/lib/ui/src/modules/ui/components/stories_panel/text_filter.stories.js b/lib/ui/src/components/stories_panel/text_filter.stories.js similarity index 100% rename from lib/ui/src/modules/ui/components/stories_panel/text_filter.stories.js rename to lib/ui/src/components/stories_panel/text_filter.stories.js diff --git a/lib/ui/src/modules/ui/components/stories_panel/text_filter.test.js b/lib/ui/src/components/stories_panel/text_filter.test.js similarity index 100% rename from lib/ui/src/modules/ui/components/stories_panel/text_filter.test.js rename to lib/ui/src/components/stories_panel/text_filter.test.js diff --git a/lib/ui/src/modules/ui/containers/addon_panel.js b/lib/ui/src/containers/addon_panel.js similarity index 84% rename from lib/ui/src/modules/ui/containers/addon_panel.js rename to lib/ui/src/containers/addon_panel.js index 9309475a334c..bf0de41b3b87 100644 --- a/lib/ui/src/modules/ui/containers/addon_panel.js +++ b/lib/ui/src/containers/addon_panel.js @@ -7,7 +7,7 @@ export function mapper(store, { panels }) { return { panels, selectedPanel, - onPanelSelect: panel => store.selectAddonPanel(panel) + onPanelSelect: panel => store.selectAddonPanel(panel), }; } diff --git a/lib/ui/src/modules/ui/containers/addon_panel.test.js b/lib/ui/src/containers/addon_panel.test.js similarity index 100% rename from lib/ui/src/modules/ui/containers/addon_panel.test.js rename to lib/ui/src/containers/addon_panel.test.js diff --git a/lib/ui/src/modules/ui/containers/header.js b/lib/ui/src/containers/header.js similarity index 74% rename from lib/ui/src/modules/ui/containers/header.js rename to lib/ui/src/containers/header.js index 28054e3a27db..0764eb1fd2f0 100755 --- a/lib/ui/src/modules/ui/containers/header.js +++ b/lib/ui/src/containers/header.js @@ -1,7 +1,8 @@ +// TODO: figure out if it's even used import pick from 'lodash.pick'; -import { Header } from '@storybook/components'; -import genPoddaLoader from '../libs/gen_podda_loader'; -import compose from '../../../compose'; +// import { Header } from '@storybook/components'; +// import genPoddaLoader from '../libs/gen_podda_loader'; +// import compose from '../../../compose'; export const mapper = (state, props, { actions }) => { const currentOptions = pick( @@ -34,7 +35,7 @@ export const mapper = (state, props, { actions }) => { }; }; -export default compose( - genPoddaLoader(mapper), - { withRef: false } -)(Header); +// export default compose( +// genPoddaLoader(mapper), +// { withRef: false } +// )(Header); diff --git a/lib/ui/src/modules/ui/containers/header.test.js b/lib/ui/src/containers/header.test.js similarity index 100% rename from lib/ui/src/modules/ui/containers/header.test.js rename to lib/ui/src/containers/header.test.js diff --git a/lib/ui/src/modules/ui/containers/layout.js b/lib/ui/src/containers/layout.js similarity index 96% rename from lib/ui/src/modules/ui/containers/layout.js rename to lib/ui/src/containers/layout.js index 852e0d17114f..012c96efc945 100755 --- a/lib/ui/src/modules/ui/containers/layout.js +++ b/lib/ui/src/containers/layout.js @@ -17,6 +17,6 @@ export default inject(stores => { return { ...currentOptions, isMobileDevice, - ...uiOptions + ...uiOptions, }; })(Layout); diff --git a/lib/ui/src/containers/routed_link.js b/lib/ui/src/containers/routed_link.js new file mode 100644 index 000000000000..1df7dd8a51f1 --- /dev/null +++ b/lib/ui/src/containers/routed_link.js @@ -0,0 +1,14 @@ +import { RoutedLink, MenuLink } from '@storybook/components'; +import { inject } from 'mobx-react'; + +export function mapper() { + return { + href: '', + }; +} + +const ComposedMenuLink = inject(({ store }, props) => mapper(store, props))(MenuLink); +const ComposedRoutedLink = inject(({ store }, props) => mapper(store, props))(RoutedLink); + +export { ComposedMenuLink as MenuLink }; +export { ComposedRoutedLink as RoutedLink }; diff --git a/lib/ui/src/modules/ui/containers/routed_link.test.js b/lib/ui/src/containers/routed_link.test.js similarity index 100% rename from lib/ui/src/modules/ui/containers/routed_link.test.js rename to lib/ui/src/containers/routed_link.test.js diff --git a/lib/ui/src/containers/search_box.js b/lib/ui/src/containers/search_box.js new file mode 100644 index 000000000000..cbedc7ffa207 --- /dev/null +++ b/lib/ui/src/containers/search_box.js @@ -0,0 +1,12 @@ +import { inject } from 'mobx-react'; + +import SearchBoxComponent from '../components/search_box'; + +export const mapper = store => ({ + showSearchBox: store.shortcutOptions.showSearchBox, + stories: store.stories, + onSelectStory: (kind, story) => store.selectStory(kind, story), + onClose: () => store.toggleSearchBox(), +}); + +export default inject(({ store }) => mapper(store))(SearchBoxComponent); diff --git a/lib/ui/src/modules/ui/containers/search_box.test.js b/lib/ui/src/containers/search_box.test.js similarity index 100% rename from lib/ui/src/modules/ui/containers/search_box.test.js rename to lib/ui/src/containers/search_box.test.js diff --git a/lib/ui/src/modules/ui/containers/shortcuts_help.js b/lib/ui/src/containers/shortcuts_help.js similarity index 85% rename from lib/ui/src/modules/ui/containers/shortcuts_help.js rename to lib/ui/src/containers/shortcuts_help.js index b5a9049483d8..b29ed2f70865 100755 --- a/lib/ui/src/modules/ui/containers/shortcuts_help.js +++ b/lib/ui/src/containers/shortcuts_help.js @@ -6,7 +6,7 @@ import ShortcutsHelp from '../components/shortcuts_help'; export const mapper = store => ({ isOpen: store.showShortcutsHelp, onClose: () => store.toggleShortcutsHelp(), - platform: window.navigator.platform.toLowerCase() + platform: window.navigator.platform.toLowerCase(), }); export default inject(({ store }) => mapper(store))(ShortcutsHelp); diff --git a/lib/ui/src/modules/ui/containers/shortcuts_help.test.js b/lib/ui/src/containers/shortcuts_help.test.js similarity index 100% rename from lib/ui/src/modules/ui/containers/shortcuts_help.test.js rename to lib/ui/src/containers/shortcuts_help.test.js diff --git a/lib/ui/src/modules/ui/containers/stories_panel.js b/lib/ui/src/containers/stories_panel.js similarity index 84% rename from lib/ui/src/modules/ui/containers/stories_panel.js rename to lib/ui/src/containers/stories_panel.js index d62120d16385..bd1e3819f9ec 100755 --- a/lib/ui/src/modules/ui/containers/stories_panel.js +++ b/lib/ui/src/containers/stories_panel.js @@ -6,7 +6,7 @@ import { prepareStoriesForHierarchy, resolveStoryHierarchy, resolveStoryHierarchyRoots, - createHierarchies + createHierarchies, } from '../libs/hierarchy'; export const mapper = store => { @@ -17,7 +17,7 @@ export const mapper = store => { uiOptions, storyFilter, shortcutOptions, - isMobileDevice + isMobileDevice, } = store; const { @@ -26,7 +26,7 @@ export const mapper = store => { hierarchyRootSeparator, sidebarAnimations, name, - url + url, } = uiOptions; const preparedStories = prepareStoriesForHierarchy( @@ -45,14 +45,8 @@ export const mapper = store => { const storiesHierarchies = createHierarchies(filteredStories); - const { storyName } = resolveStoryHierarchyRoots( - selectedKind, - hierarchyRootSeparator - ); - const selectedHierarchy = resolveStoryHierarchy( - storyName, - hierarchySeparator - ); + const { storyName } = resolveStoryHierarchyRoots(selectedKind, hierarchyRootSeparator); + const selectedHierarchy = resolveStoryHierarchy(storyName, hierarchySeparator); return { storiesHierarchies, @@ -67,7 +61,7 @@ export const mapper = store => { sidebarAnimations, isMobileDevice, name, - url + url, }; }; diff --git a/lib/ui/src/modules/ui/containers/stories_panel.test.js b/lib/ui/src/containers/stories_panel.test.js similarity index 100% rename from lib/ui/src/modules/ui/containers/stories_panel.test.js rename to lib/ui/src/containers/stories_panel.test.js diff --git a/lib/ui/src/index.js b/lib/ui/src/index.js index b1ce5ac0b066..f6c61646f2be 100644 --- a/lib/ui/src/index.js +++ b/lib/ui/src/index.js @@ -2,13 +2,18 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Provider as MobxProvider } from 'mobx-react'; import ReactModal from 'react-modal'; +import { Router } from 'react-router'; +import createHistory from 'history/createBrowserHistory'; -import handleRouting from './modules/ui/configs/handle_routing'; import App from './app'; import Provider from './provider'; import initProviderApi from './init-provider-api'; +import initHistoryHandler from './init-history-handler'; +import initKeyHandler from './init-key-handler'; import createStore from './store'; -import { Router, Route } from './routing'; +import { Route } from './routing'; + +const history = createHistory(); function renderStorybokUI(domNode, provider) { if (!(provider instanceof Provider)) { @@ -17,28 +22,21 @@ function renderStorybokUI(domNode, provider) { const store = createStore({ provider }); + /** Init external interaction with the state */ initProviderApi({ provider, store }); - - // handleRouting(store); - // store.selectAddonPanel(); + initHistoryHandler({ history, store }); + initKeyHandler({ store }); // Tell react-modal which element to mark as aria-hidden ReactModal.setAppElement(domNode); - const Container = process.env.STORYBOOK_EXAMPLE_APP - ? React.StrictMode - : 'div'; + const Container = process.env.STORYBOOK_EXAMPLE_APP ? React.StrictMode : 'div'; const root = ( - - ( - - )} - /> + + } /> diff --git a/lib/ui/src/init-history-handler.js b/lib/ui/src/init-history-handler.js new file mode 100644 index 000000000000..bd7cf0642eb7 --- /dev/null +++ b/lib/ui/src/init-history-handler.js @@ -0,0 +1,41 @@ +import { reaction } from 'mobx'; +import qs from 'qs'; + +export default ({ history, store }) => { + let currentAction; + + // update the history on story change, replace on POP + reaction( + () => ({ + selectedKind: store.selectedKind, + selectedStory: store.selectedStory, + }), + () => { + const newParams = qs.stringify({ + selectedKind: store.selectedKind, + selectedStory: store.selectedStory, + full: Number(store.shortcutOptions.goFullScreen), + addons: Number(store.shortcutOptions.showAddonPanel), + stories: Number(store.shortcutOptions.showStoriesPanel), + panelRight: Number(store.shortcutOptions.addonPanelInRight), + addonPanel: store.selectedAddonPanel, + }); + + // TODO: find a better way to ignore pop: maybe call this action from the selectStory action + if (currentAction === 'POP') return; + history.push({ search: newParams }); + } + ); + + // listen on url pop (navigator navigation) + history.listen((location, action) => { + currentAction = action; + if (action === 'POP') { + store.updateFromLocation(qs.parse(location.search, { ignoreQueryPrefix: true })); + } + currentAction = null; + }); + + // parse initial url + store.updateFromLocation(qs.parse(history.location.search, { ignoreQueryPrefix: true })); +}; diff --git a/lib/ui/src/init-key-handler.js b/lib/ui/src/init-key-handler.js new file mode 100644 index 000000000000..f90ed1775252 --- /dev/null +++ b/lib/ui/src/init-key-handler.js @@ -0,0 +1,13 @@ +import { document } from 'global'; +import keyEvents from './libs/key_events'; + +export default ({ store }) => { + const onKeyDown = e => { + const parsedEvent = keyEvents(e); + if (parsedEvent) { + store.handleEvent(parsedEvent); + } + }; + + document.addEventListener('keydown', onKeyDown); +}; diff --git a/lib/ui/src/init-provider-api.js b/lib/ui/src/init-provider-api.js index ce79cd83fbe5..253f05edbcaf 100644 --- a/lib/ui/src/init-provider-api.js +++ b/lib/ui/src/init-provider-api.js @@ -1,8 +1,6 @@ import { autorun } from 'mobx'; import { EventEmitter } from 'events'; -import { getUrlState } from './modules/ui/configs/handle_routing'; - export default ({ provider, store }) => { const callbacks = new EventEmitter(); let currentKind; @@ -46,9 +44,11 @@ export default ({ provider, store }) => { } return undefined; }, - getUrlState(overrideParams) { - return getUrlState({ ...store, ...overrideParams }); - } + getUrlState(/* overrideParams */) { + // TODO: find out what this is supposed to be used for + // return window.location.href; + return ''; + }, }; provider.handleAPI(api); @@ -56,10 +56,7 @@ export default ({ provider, store }) => { autorun(() => { if (!store.selectedKind) return; - if ( - store.selectedKind === currentKind && - store.selectedStory === currentStory - ) { + if (store.selectedKind === currentKind && store.selectedStory === currentStory) { // No change in the selected story so avoid emitting 'story' return; } diff --git a/lib/ui/src/modules/ui/libs/filters.js b/lib/ui/src/libs/filters.js similarity index 94% rename from lib/ui/src/modules/ui/libs/filters.js rename to lib/ui/src/libs/filters.js index 2987c17cdb37..0308baefe0b3 100755 --- a/lib/ui/src/modules/ui/libs/filters.js +++ b/lib/ui/src/libs/filters.js @@ -19,7 +19,11 @@ const searchOptions = { function sort(stories, sortStoriesByKind) { if (!sortStoriesByKind) return stories; - return sortBy(stories, ['kind']); + const sortedStories = sortBy(stories, ['kind']); + return sortedStories.map(item => ({ + ...item, + stories: item.stories.concat().sort(), + })); } function flattenStories(items) { diff --git a/lib/ui/src/modules/ui/libs/filters.test.js b/lib/ui/src/libs/filters.test.js similarity index 89% rename from lib/ui/src/modules/ui/libs/filters.test.js rename to lib/ui/src/libs/filters.test.js index 104dbba25699..790411ed490c 100755 --- a/lib/ui/src/modules/ui/libs/filters.test.js +++ b/lib/ui/src/libs/filters.test.js @@ -74,6 +74,20 @@ describe('manager.ui.libs.filters', () => { expect(res).toEqual([stories[1], stories[2], stories[0]]); }); + test('should sort nested stories', () => { + const unsorted = ['bb', 'aa']; + const stories = [ + { kind: 'ss', namespaces: ['ss'], stories: unsorted, rootName: '' }, + { kind: 'aa', namespaces: ['aa'], stories: ['bb'], rootName: '' }, + { kind: 'bb', namespaces: ['bb'], stories: ['bb'], rootName: '' }, + ]; + + const res = storyFilter(stories, null, null, null, true); + + expect(res[2].stories).not.toEqual(unsorted); + expect(res[2].stories).toEqual(unsorted.concat().sort()); + }); + test('should filter on story level', () => { const stories = [ { kind: 'aa', namespaces: ['aa'], stories: ['bb'], rootName: '' }, diff --git a/lib/ui/src/modules/ui/libs/hierarchy.js b/lib/ui/src/libs/hierarchy.js similarity index 100% rename from lib/ui/src/modules/ui/libs/hierarchy.js rename to lib/ui/src/libs/hierarchy.js diff --git a/lib/ui/src/modules/ui/libs/hierarchy.test.js b/lib/ui/src/libs/hierarchy.test.js similarity index 100% rename from lib/ui/src/modules/ui/libs/hierarchy.test.js rename to lib/ui/src/libs/hierarchy.test.js diff --git a/lib/ui/src/modules/ui/libs/is_mobile_device.js b/lib/ui/src/libs/is_mobile_device.js similarity index 100% rename from lib/ui/src/modules/ui/libs/is_mobile_device.js rename to lib/ui/src/libs/is_mobile_device.js diff --git a/lib/ui/src/modules/ui/libs/is_mobile_device.test.js b/lib/ui/src/libs/is_mobile_device.test.js similarity index 100% rename from lib/ui/src/modules/ui/libs/is_mobile_device.test.js rename to lib/ui/src/libs/is_mobile_device.test.js diff --git a/lib/ui/src/modules/api/actions/api.js b/lib/ui/src/modules/api/actions/api.js deleted file mode 100755 index d871a3d9bcfe..000000000000 --- a/lib/ui/src/modules/api/actions/api.js +++ /dev/null @@ -1,137 +0,0 @@ -import pick from 'lodash.pick'; - -export function jumpToStory(storyKinds, selectedKind, selectedStory, direction) { - const flatteredStories = []; - let currentIndex = -1; - - storyKinds.forEach(({ kind, stories }) => { - stories.forEach(story => { - flatteredStories.push({ kind, story }); - if (kind === selectedKind && story === selectedStory) { - currentIndex = flatteredStories.length - 1; - } - }); - }); - - const jumpedStory = flatteredStories[currentIndex + direction]; - if (!jumpedStory) { - return { selectedKind, selectedStory }; - } - - return { - selectedKind: jumpedStory.kind, - selectedStory: jumpedStory.story, - }; -} - -export function ensureKind(storyKinds, selectedKind) { - if (!storyKinds) return selectedKind; - - const found = storyKinds.find(item => item.kind === selectedKind); - if (found) return found.kind; - // if the selected kind is non-existant, select the first kind - const kinds = storyKinds.map(item => item.kind); - return kinds[0]; -} - -export function ensureStory(storyKinds, selectedKind, selectedStory) { - if (!storyKinds) return selectedStory; - - const kindInfo = storyKinds.find(item => item.kind === selectedKind); - if (!kindInfo) return null; - - const found = kindInfo.stories.find(item => item === selectedStory); - if (found) return found; - - // if the selected story is non-existant, select the first story - return kindInfo.stories[0]; -} - -export function ensurePanel(panels, selectedPanel, currentPanel) { - if (Object.keys(panels).indexOf(selectedPanel) >= 0) return selectedPanel; - // if the selected panel is non-existant, select the current panel - // and output to console all available panels - const logger = console; - logger.group('Available Panels ID:'); - Object.keys(panels).forEach(panelID => logger.log(`${panelID} (${panels[panelID].title})`)); - logger.groupEnd('Available Panels ID:'); - return currentPanel; -} - -export default { - setStories({ clientStore }, stories) { - clientStore.update(state => { - const selectedKind = ensureKind(stories, state.selectedKind); - const currentSelectedStory = selectedKind === state.selectedKind ? state.selectedStory : null; - const selectedStory = ensureStory(stories, selectedKind, currentSelectedStory); - - return { - stories, - selectedStory, - selectedKind, - }; - }); - }, - - selectStory({ clientStore }, kind, story) { - clientStore.update(state => { - const selectedKind = ensureKind(state.stories, kind); - const selectedStory = ensureStory(state.stories, selectedKind, story); - - return { selectedKind, selectedStory }; - }); - }, - - selectInCurrentKind({ clientStore }, story) { - clientStore.update(state => { - const selectedStory = ensureStory(state.stories, state.selectedKind, story); - - return { selectedStory }; - }); - }, - - jumpToStory({ clientStore }, direction) { - clientStore.update(state => - jumpToStory(state.stories, state.selectedKind, state.selectedStory, direction) - ); - }, - - setOptions(env, options) { - const { clientStore, provider } = env; - clientStore.update(state => { - const newOptions = pick(options, Object.keys(state.uiOptions)); - const updatedUiOptions = { - ...state.uiOptions, - ...newOptions, - }; - const otherOptions = {}; - if (Object.keys(pick(options, ['selectedAddonPanel'])).length) { - otherOptions.selectedAddonPanel = ensurePanel( - provider.getPanels(), - options.selectedAddonPanel, - state.selectedAddonPanel - ); - } - return { uiOptions: updatedUiOptions, ...otherOptions }; - }); - }, - - setQueryParams({ clientStore }, customQueryParams) { - clientStore.update(state => { - const updatedQueryParams = { - ...state.customQueryParams, - ...customQueryParams, - }; - - Object.keys(customQueryParams).forEach(key => { - if (updatedQueryParams[key] === null) { - delete updatedQueryParams[key]; - } - }); - - return { - customQueryParams: updatedQueryParams, - }; - }); - }, -}; diff --git a/lib/ui/src/modules/api/actions/api.test.js b/lib/ui/src/modules/api/actions/api.test.js deleted file mode 100755 index 2b467cd2e660..000000000000 --- a/lib/ui/src/modules/api/actions/api.test.js +++ /dev/null @@ -1,319 +0,0 @@ -import actions from './api'; - -class MockClientStore { - update(cb) { - this.updateCallback = cb; - } -} - -const stories = [ - { kind: 'abc', stories: ['a', 'b', 'c'] }, - { kind: 'bbc', stories: ['x', 'y', 'z'] }, -]; - -describe('manager.api.actions.api', () => { - describe('setStories', () => { - describe('no selected story', () => { - it('should set stories and select the first story', () => { - const clientStore = new MockClientStore(); - actions.setStories({ clientStore }, stories); - - const newState = clientStore.updateCallback({}); - expect(newState).toEqual({ - stories, - selectedKind: 'abc', - selectedStory: 'a', - }); - }); - }); - - describe('has a selected story', () => { - it('should set stories and select the existing story', () => { - const clientStore = new MockClientStore(); - actions.setStories({ clientStore }, stories); - - const state = { - selectedKind: 'abc', - selectedStory: 'c', - }; - const newState = clientStore.updateCallback(state); - expect(newState).toEqual({ - stories, - selectedKind: 'abc', - selectedStory: 'c', - }); - }); - }); - - describe('has a selected story, but it is story is not in new stories', () => { - it('should set stories and select the first story of the selected kind', () => { - const clientStore = new MockClientStore(); - actions.setStories({ clientStore }, stories); - - const state = { - selectedKind: 'bbc', - selectedStory: 'k', - }; - const newState = clientStore.updateCallback(state); - expect(newState).toEqual({ - stories, - selectedKind: 'bbc', - selectedStory: 'x', - }); - }); - }); - - describe('has a selected story, but it is kind is not in new stories', () => { - it('should set stories and select the first story', () => { - const clientStore = new MockClientStore(); - actions.setStories({ clientStore }, stories); - - const state = { - selectedKind: 'kky', - selectedStory: 'c', - }; - const newState = clientStore.updateCallback(state); - expect(newState).toEqual({ - stories, - selectedKind: 'abc', - selectedStory: 'a', - }); - }); - }); - }); - - describe('selectStory', () => { - describe('with both kind and story', () => { - it('should select the correct story', () => { - const clientStore = new MockClientStore(); - actions.selectStory({ clientStore }, 'bbc', 'y'); - - const state = { - stories, - selectedKind: 'abc', - selectedStory: 'c', - }; - const stateUpdates = clientStore.updateCallback(state); - expect(stateUpdates).toEqual({ - selectedKind: 'bbc', - selectedStory: 'y', - }); - }); - }); - - describe('with just the kind', () => { - it('should select the first of the kind', () => { - const clientStore = new MockClientStore(); - actions.selectStory({ clientStore }, 'bbc'); - - const state = { - stories, - selectedKind: 'abc', - selectedStory: 'c', - }; - const stateUpdates = clientStore.updateCallback(state); - expect(stateUpdates).toEqual({ - selectedKind: 'bbc', - selectedStory: 'x', - }); - }); - }); - }); - - describe('selectInCurrentKind', () => { - it('should select the correct story', () => { - const clientStore = new MockClientStore(); - actions.selectInCurrentKind({ clientStore }, 'y'); - - const state = { - stories, - selectedKind: 'bbc', - selectedStory: 'z', - }; - const stateUpdates = clientStore.updateCallback(state); - expect(stateUpdates).toEqual({ - selectedStory: 'y', - }); - }); - - it("should select the first story if there's no such story", () => { - const clientStore = new MockClientStore(); - actions.selectInCurrentKind({ clientStore }, 'y'); - - const state = { - stories, - selectedKind: 'abc', - selectedStory: 'c', - }; - const stateUpdates = clientStore.updateCallback(state); - expect(stateUpdates).toEqual({ - selectedStory: 'a', - }); - }); - }); - - describe('jumpToStory', () => { - describe('has enough stories', () => { - it('should select the next story', () => { - const clientStore = new MockClientStore(); - actions.jumpToStory({ clientStore }, 1); // eslint-disable-line - - const state = { - stories, - selectedKind: 'abc', - selectedStory: 'c', - }; - const stateUpdates = clientStore.updateCallback(state); - expect(stateUpdates).toEqual({ - selectedKind: 'bbc', - selectedStory: 'x', - }); - }); - - it('should select the prev story', () => { - const clientStore = new MockClientStore(); - actions.jumpToStory({ clientStore }, -1); // eslint-disable-line - - const state = { - stories, - selectedKind: 'abc', - selectedStory: 'c', - }; - const stateUpdates = clientStore.updateCallback(state); - expect(stateUpdates).toEqual({ - selectedKind: 'abc', - selectedStory: 'b', - }); - }); - }); - - describe('has not enough stories', () => { - it('should select the current story', () => { - const clientStore = new MockClientStore(); - actions.jumpToStory({ clientStore }, 1); // eslint-disable-line - - const state = { - stories, - selectedKind: 'bbc', - selectedStory: 'z', - }; - const stateUpdates = clientStore.updateCallback(state); - expect(stateUpdates).toEqual({ - selectedKind: 'bbc', - selectedStory: 'z', - }); - }); - }); - }); - - describe('setOptions', () => { - it('should update options', () => { - const clientStore = new MockClientStore(); - actions.setOptions({ clientStore }, { abc: 10 }); - - const state = { - uiOptions: { bbc: 50, abc: 40 }, - }; - - const stateUpdates = clientStore.updateCallback(state); - expect(stateUpdates).toEqual({ - uiOptions: { bbc: 50, abc: 10 }, - }); - }); - - const provider = { - getPanels: () => ({ - 'storybook/actions/actions-panel': { - title: 'Action logger', - }, - 'storybooks/storybook-addon-knobs': { - title: 'Knobs', - }, - }), - }; - - it('should update selectedAddonPanel', () => { - const clientStore = new MockClientStore(); - actions.setOptions( - { clientStore, provider }, - { selectedAddonPanel: 'storybooks/storybook-addon-knobs' } - ); - - const state = { - uiOptions: {}, - selectedAddonPanel: 'storybook/actions/actions-panel', - }; - - const stateUpdates = clientStore.updateCallback(state); - expect(stateUpdates.selectedAddonPanel).toEqual('storybooks/storybook-addon-knobs'); - }); - - it('should keep current addonPanel and output panel IDs', () => { - const clientStore = new MockClientStore(); - actions.setOptions({ clientStore, provider }, { selectedAddonPanel: null }); - - global.console = { - log: jest.fn(), - group: jest.fn(), - groupEnd: jest.fn(), - }; - const logger = console; - - const state = { - uiOptions: {}, - selectedAddonPanel: 'storybook/actions/actions-panel', - }; - - const stateUpdates = clientStore.updateCallback(state); - expect(stateUpdates.selectedAddonPanel).toEqual('storybook/actions/actions-panel'); - expect(logger.log.mock.calls).toEqual([ - ['storybook/actions/actions-panel (Action logger)'], - ['storybooks/storybook-addon-knobs (Knobs)'], - ]); - }); - - it('should only update options for the key already defined', () => { - const clientStore = new MockClientStore(); - actions.setOptions({ clientStore }, { abc: 10, notGoingToState: 20 }); - - const state = { - uiOptions: { bbc: 50, abc: 40 }, - }; - - const stateUpdates = clientStore.updateCallback(state); - expect(stateUpdates).toEqual({ - uiOptions: { bbc: 50, abc: 10 }, - }); - }); - }); - - describe('setQueryParams', () => { - it('shodul update query params', () => { - const clientStore = new MockClientStore(); - actions.setQueryParams({ clientStore }, { abc: 'aaa', cnn: 'ccc' }); - - const state = { - customQueryParams: { bbc: 'bbb', abc: 'sshd' }, - }; - - const stateUpdates = clientStore.updateCallback(state); - expect(stateUpdates).toEqual({ - customQueryParams: { bbc: 'bbb', abc: 'aaa', cnn: 'ccc' }, - }); - }); - - it('should delete the param if it is null', () => { - const clientStore = new MockClientStore(); - actions.setQueryParams({ clientStore }, { abc: null, bbc: 'ccc' }); - - const state = { - customQueryParams: { bbc: 'bbb', abc: 'sshd' }, - }; - - const stateUpdates = clientStore.updateCallback(state); - expect(stateUpdates).toEqual({ - customQueryParams: { bbc: 'ccc' }, - }); - }); - }); -}); diff --git a/lib/ui/src/modules/api/actions/index.js b/lib/ui/src/modules/api/actions/index.js deleted file mode 100755 index 7cb6cc41d693..000000000000 --- a/lib/ui/src/modules/api/actions/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import api from './api'; - -export default { - api, -}; diff --git a/lib/ui/src/modules/api/configs/init_api.js b/lib/ui/src/modules/api/configs/init_api.js deleted file mode 100644 index 8e00ce5d0732..000000000000 --- a/lib/ui/src/modules/api/configs/init_api.js +++ /dev/null @@ -1,65 +0,0 @@ -import { EventEmitter } from 'events'; -import { getUrlState } from '../../ui/configs/handle_routing'; - -export default function(provider, clientStore, actions) { - const callbacks = new EventEmitter(); - let currentKind; - let currentStory; - - const providerApi = { - onStory(cb) { - callbacks.on('story', cb); - if (currentKind && currentStory) { - // Using a setTimeout to call the callback to make sure it's - // not called on current event-loop. When users add callbacks - // they usually expect it to be called in a future event loop. - setTimeout(() => cb(currentKind, currentStory), 0); - } - return function stopListening() { - callbacks.removeListener('story', cb); - }; - }, - - setStories: actions.api.setStories, - selectStory: actions.api.selectStory, - selectInCurrentKind: actions.api.selectInCurrentKind, - handleShortcut: actions.shortcuts.handleEvent, - setQueryParams: actions.api.setQueryParams, - - setOptions(...args) { - actions.api.setOptions(...args); - actions.shortcuts.setOptions(...args); - }, - - getQueryParam(key) { - const state = clientStore.getAll(); - if (state.customQueryParams) { - return state.customQueryParams[key]; - } - return undefined; - }, - - getUrlState(overrideParams) { - const state = clientStore.getAll(); - return getUrlState({ ...state, ...overrideParams }); - }, - }; - - provider.handleAPI(providerApi); - - // subscribe to redux store and trigger onStory's callback - clientStore.subscribe(() => { - const state = clientStore.getAll(); - if (!state.selectedKind) return; - - if (state.selectedKind === currentKind && state.selectedStory === currentStory) { - // No change in the selected story so avoid emitting 'story' - return; - } - - currentKind = state.selectedKind; - currentStory = state.selectedStory; - callbacks.emit('story', state.selectedKind, state.selectedStory); - // providerApi._onStoryCallback(api.selectedKind, api.selectedStory); - }); -} diff --git a/lib/ui/src/modules/api/configs/init_api.test.js b/lib/ui/src/modules/api/configs/init_api.test.js deleted file mode 100644 index 5793613bad3d..000000000000 --- a/lib/ui/src/modules/api/configs/init_api.test.js +++ /dev/null @@ -1,187 +0,0 @@ -import initApi from './init_api'; - -describe('manager.api.config.initApi', () => { - it('should expose correct API methods', done => { - const actions = { - api: { - setStories: jest.fn(), - selectStory: jest.fn(), - selectInCurrentKind: jest.fn(), - setQueryParams: jest.fn(), - }, - shortcuts: { - handleEvent: jest.fn(), - }, - }; - - const clientStore = { - subscribe: jest.fn(), - }; - - const provider = { - handleAPI(api) { - expect(api.setStories).toBe(actions.api.setStories); - expect(api.selectStory).toBe(actions.api.selectStory); - expect(api.selectInCurrentKind).toBe(actions.api.selectInCurrentKind); - expect(api.handleShortcut).toBe(actions.shortcuts.handleEvent); - expect(typeof api.onStory).toBe('function'); - expect(typeof api.setQueryParams).toBe('function'); - expect(typeof api.getUrlState).toBe('function'); - done(); - }, - }; - - initApi(provider, clientStore, actions); - }); - - it('should trigger the onStory callback', done => { - const actions = { api: {}, shortcuts: {} }; - const selectedKind = 'XXdd'; - const selectedStory = 'u8sd'; - - const clientStore = { - subscribe: jest.fn(), - getAll: () => ({ - selectedKind, - selectedStory, - }), - }; - - const provider = { - handleAPI(api) { - api.onStory((kind, story) => { - expect(kind).toBe(selectedKind); - expect(story).toBe(selectedStory); - done(); - }); - }, - }; - - initApi(provider, clientStore, actions); - // calling the subscription - clientStore.subscribe.mock.calls[0][0](); - }); - - it('should support to add multiple onStory callback', done => { - const actions = { api: {}, shortcuts: {} }; - const selectedKind = 'XXdd'; - const selectedStory = 'u8sd'; - - const clientStore = { - subscribe: jest.fn(), - getAll: () => ({ - selectedKind, - selectedStory, - }), - }; - - const provider = { - handleAPI(api) { - let count = 0; - api.onStory(() => { - count += 1; - }); - - api.onStory(() => { - count += 1; - expect(count).toBe(2); - done(); - }); - }, - }; - - initApi(provider, clientStore, actions); - // calling the subscription - clientStore.subscribe.mock.calls[0][0](); - }); - - it('should support a way to remove onStory callback', done => { - const actions = { api: {}, shortcuts: {} }; - const selectedKind = 'XXdd'; - const selectedStory = 'u8sd'; - - const clientStore = { - subscribe: jest.fn(), - getAll: () => ({ - selectedKind, - selectedStory, - }), - }; - - const provider = { - handleAPI(api) { - let count = 0; - const stop = api.onStory(() => { - count += 1; - }); - stop(); - - api.onStory(() => { - count += 1; - expect(count).toBe(1); - done(); - }); - }, - }; - - initApi(provider, clientStore, actions); - // calling the subscription - clientStore.subscribe.mock.calls[0][0](); - }); - - describe('getQueryParam', () => { - it('should return the correct query param value', done => { - const actions = { api: {}, shortcuts: {} }; - - const clientStore = { - subscribe: jest.fn(), - getAll: () => ({ - customQueryParams: { - foo: 'foo value', - bar: 'bar value', - }, - }), - }; - - const provider = { - handleAPI(api) { - const value = api.getQueryParam('foo'); - expect(value).toBe('foo value'); - done(); - }, - }; - - initApi(provider, clientStore, actions); - }); - }); - - describe('getUrlState', () => { - it('should return the correct url state value', done => { - const actions = { api: {}, shortcuts: {} }; - - const clientStore = { - subscribe: jest.fn(), - getAll: () => ({ - selectedKind: 'kind', - selectedStory: 'story', - shortcutOptions: {}, - }), - }; - - const provider = { - handleAPI(api) { - const value = api.getUrlState({ selectedStory: 'newStory' }); - expect(value).toMatchObject({ - selectedKind: 'kind', - selectedStory: 'newStory', - }); - expect(value.url).toMatch(/selectedKind=kind/); - expect(value.url).toMatch(/selectedStory=newStory/); - done(); - }, - }; - - initApi(provider, clientStore, actions); - }); - }); -}); diff --git a/lib/ui/src/modules/api/index.js b/lib/ui/src/modules/api/index.js deleted file mode 100755 index a8b9e22af2ab..000000000000 --- a/lib/ui/src/modules/api/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import actions from './actions'; -import initApi from './configs/init_api'; - -export default { - actions, - defaultState: { - uiOptions: { - name: 'STORYBOOK', - url: 'https://github.com/storybooks/storybook', - sortStoriesByKind: false, - hierarchySeparator: '/', - hierarchyRootSeparator: null, - sidebarAnimations: true, - theme: null, - }, - }, - load({ clientStore, provider }, _actions) { - initApi(provider, clientStore, _actions); - }, -}; diff --git a/lib/ui/src/modules/shortcuts/actions/index.js b/lib/ui/src/modules/shortcuts/actions/index.js deleted file mode 100755 index 1f8965d3d05e..000000000000 --- a/lib/ui/src/modules/shortcuts/actions/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import shortcuts from './shortcuts'; - -export default { - shortcuts, -}; diff --git a/lib/ui/src/modules/shortcuts/actions/shortcuts.js b/lib/ui/src/modules/shortcuts/actions/shortcuts.js deleted file mode 100755 index 3a498f734df6..000000000000 --- a/lib/ui/src/modules/shortcuts/actions/shortcuts.js +++ /dev/null @@ -1,89 +0,0 @@ -import pick from 'lodash.pick'; -import { features } from '../../../libs/key_events'; -import apiActions from '../../api/actions'; - -const deprecationMessage = (oldName, newName) => - `The ${oldName} option has been renamed to ${newName} and will not be available in the next major Storybook release. Please update your config.`; - -export function keyEventToOptions(currentOptions, event) { - if (currentOptions.enableShortcuts) { - switch (event) { - case features.FULLSCREEN: - return { goFullScreen: !currentOptions.goFullScreen }; - case features.ADDON_PANEL: - return { showAddonPanel: !currentOptions.showAddonPanel }; - case features.STORIES_PANEL: - return { showStoriesPanel: !currentOptions.showStoriesPanel }; - case features.SHOW_SEARCH: - return { showSearchBox: true }; - case features.ADDON_PANEL_IN_RIGHT: - return { addonPanelInRight: !currentOptions.addonPanelInRight }; - default: - return {}; - } - } - return {}; -} - -const renamedOptions = { - showLeftPanel: 'showStoriesPanel', - showDownPanel: 'showAddonPanel', - downPanelInRight: 'addonPanelInRight', -}; - -export default { - handleEvent(context, event) { - const { clientStore } = context; - switch (event) { - case features.NEXT_STORY: - apiActions.api.jumpToStory(context, 1); - break; - case features.PREV_STORY: - apiActions.api.jumpToStory(context, -1); - break; - default: - clientStore.update(state => { - const newOptions = keyEventToOptions(state.shortcutOptions, event); - const updatedOptions = { - ...state.shortcutOptions, - ...newOptions, - }; - - return { - shortcutOptions: updatedOptions, - }; - }); - } - }, - - setOptions({ clientStore }, options) { - clientStore.update(state => { - const updatedOptions = { - ...state.shortcutOptions, - ...pick(options, Object.keys(state.shortcutOptions)), - }; - - const withNewNames = Object.keys(renamedOptions).reduce((acc, oldName) => { - const newName = renamedOptions[oldName]; - - if (oldName in options && !(newName in options)) { - if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line no-console - console.warn(deprecationMessage(oldName, newName)); - } - - return { - ...acc, - [newName]: options[oldName], - }; - } - - return acc; - }, updatedOptions); - - return { - shortcutOptions: withNewNames, - }; - }); - }, -}; diff --git a/lib/ui/src/modules/shortcuts/actions/shortcuts.test.js b/lib/ui/src/modules/shortcuts/actions/shortcuts.test.js deleted file mode 100644 index 370f06da2312..000000000000 --- a/lib/ui/src/modules/shortcuts/actions/shortcuts.test.js +++ /dev/null @@ -1,68 +0,0 @@ -// import actions from './shortcuts'; - -// class MockClientStore { -// update(cb) { -// this.updateCallback = cb; -// } -// } - -// describe('manager.shortcuts.actions.shortcuts', () => { -// describe('setOptions', () => { -// test('should update options', () => { -// const clientStore = new MockClientStore(); -// actions.setOptions({ clientStore }, { abc: 10 }); - -// const state = { -// shortcutOptions: { bbc: 50, abc: 40 }, -// }; - -// const stateUpdates = clientStore.updateCallback(state); -// expect(stateUpdates).toEqual({ -// shortcutOptions: { bbc: 50, abc: 10 }, -// }); -// }); - -// test('should only update options for the key already defined', () => { -// const clientStore = new MockClientStore(); -// actions.setOptions({ clientStore }, { abc: 10, kki: 50 }); - -// const state = { -// shortcutOptions: { bbc: 50, abc: 40 }, -// }; - -// const stateUpdates = clientStore.updateCallback(state); -// expect(stateUpdates).toEqual({ -// shortcutOptions: { bbc: 50, abc: 10 }, -// }); -// }); - -// test('should warn about deprecated option names', () => { -// const clientStore = new MockClientStore(); -// const spy = jest.spyOn(console, 'warn').mockImplementation(() => {}); -// actions.setOptions( -// { clientStore }, -// { -// showLeftPanel: 1, -// showDownPanel: 2, -// downPanelInRight: 3, -// } -// ); - -// const state = { -// shortcutOptions: {}, -// }; -// const stateUpdates = clientStore.updateCallback(state); -// expect(spy).toHaveBeenCalledTimes(3); -// expect(stateUpdates).toEqual({ -// shortcutOptions: { -// showStoriesPanel: 1, -// showAddonPanel: 2, -// addonPanelInRight: 3, -// }, -// }); - -// spy.mockReset(); -// spy.mockRestore(); -// }); -// }); -// }); diff --git a/lib/ui/src/modules/shortcuts/index.js b/lib/ui/src/modules/shortcuts/index.js deleted file mode 100755 index dd89d0708cec..000000000000 --- a/lib/ui/src/modules/shortcuts/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import actions from './actions'; -import checkIfMobileDevice from '../ui/libs/is_mobile_device'; - -const { userAgent } = global.window.navigator; -const isMobileDevice = checkIfMobileDevice(userAgent); - -export default { - actions, - defaultState: { - isMobileDevice, - shortcutOptions: { - goFullScreen: false, - showStoriesPanel: !isMobileDevice, - showAddonPanel: true, - showSearchBox: false, - addonPanelInRight: false, - enableShortcuts: true, - }, - }, -}; diff --git a/lib/ui/src/modules/ui/actions/index.js b/lib/ui/src/modules/ui/actions/index.js deleted file mode 100755 index f3bb72336a4e..000000000000 --- a/lib/ui/src/modules/ui/actions/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import ui from './ui'; - -export default { - ui, -}; diff --git a/lib/ui/src/modules/ui/actions/ui.js b/lib/ui/src/modules/ui/actions/ui.js deleted file mode 100755 index b0d279a8e7e8..000000000000 --- a/lib/ui/src/modules/ui/actions/ui.js +++ /dev/null @@ -1,13 +0,0 @@ -export default { - setStoryFilter({ clientStore }, filter) { - clientStore.set('storyFilter', filter); - }, - - toggleShortcutsHelp({ clientStore }) { - clientStore.toggle('showShortcutsHelp'); - }, - - selectAddonPanel({ clientStore }, panelName) { - clientStore.set('selectedAddonPanel', panelName); - }, -}; diff --git a/lib/ui/src/modules/ui/actions/ui.test.js b/lib/ui/src/modules/ui/actions/ui.test.js deleted file mode 100755 index 0226517cc5b3..000000000000 --- a/lib/ui/src/modules/ui/actions/ui.test.js +++ /dev/null @@ -1,38 +0,0 @@ -import actions from './ui'; - -describe('manager.ui.actions.ui', () => { - describe('setStoryFilter', () => { - it('should set the given filter', () => { - const clientStore = { - set: jest.fn(), - }; - const filter = 'kkkind'; - actions.setStoryFilter({ clientStore }, filter); - - expect(clientStore.set).toHaveBeenCalledWith('storyFilter', filter); - }); - }); - - describe('toggleShortcutsHelp', () => { - it('should toggle the client sotre accordingly', () => { - const clientStore = { - toggle: jest.fn(), - }; - actions.toggleShortcutsHelp({ clientStore }); - - expect(clientStore.toggle).toHaveBeenCalledWith('showShortcutsHelp'); - }); - }); - - describe('selectAddonPanel', () => { - it('should set the given panel name', () => { - const clientStore = { - set: jest.fn(), - }; - const panelName = 'kkkind'; - actions.selectAddonPanel({ clientStore }, panelName); - - expect(clientStore.set).toHaveBeenCalledWith('selectedAddonPanel', panelName); - }); - }); -}); diff --git a/lib/ui/src/modules/ui/components/stories_panel/index.stories.js b/lib/ui/src/modules/ui/components/stories_panel/index.stories.js deleted file mode 100644 index d7f6aa967c09..000000000000 --- a/lib/ui/src/modules/ui/components/stories_panel/index.stories.js +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import { storiesOf } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; - -import StoriesPanel from './index'; -import { createHierarchies } from '../../libs/hierarchy'; -import withLifecyle from '../../../../libs/withLifecycleDecorator'; -import { setContext } from '../../../../compose'; - -const decorator = withLifecyle({ - beforeEach() { - setContext({ - clientStore: { - getAll() { - return { shortcutOptions: {}, uiOptions: {}, actions: { ui: {} } }; - }, - subscribe() {}, - }, - }); - }, - afterEach() { - setContext(null); - }, -}); - -const storiesHierarchies = createHierarchies([{ kind: 'kk', namespaces: ['kk'], stories: ['bb'] }]); -const openShortcutsHelp = action('openShortcutsHelp'); -const onStoryFilter = action('onStoryFilter'); -storiesOf('UI|stories/StoriesPanel', module) - .addDecorator(decorator) - .add('default', () => ( - - )) - .add('with storiesHierarchies prop', () => ( - - )) - .add('storiesHierarchies exists but is empty', () => ( - - )) - .add('when open on mobile device', () => ( - - )); diff --git a/lib/ui/src/modules/ui/components/stories_panel/stories_tree/index.stories.js b/lib/ui/src/modules/ui/components/stories_panel/stories_tree/index.stories.js deleted file mode 100644 index 91ce0bc7f4f0..000000000000 --- a/lib/ui/src/modules/ui/components/stories_panel/stories_tree/index.stories.js +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import { storiesOf } from '@storybook/react'; - -import Stories from './index'; -import { setContext } from '../../../../../compose'; -import { createHierarchies, prepareStoriesForHierarchy } from '../../../libs/hierarchy'; -import { storyFilter } from '../../../libs/filters'; -import withLifecyle from '../../../../../libs/withLifecycleDecorator'; - -const data = createHierarchies([ - { kind: 'a', name: 'a', namespaces: ['a'], stories: ['a1', 'a2'] }, - { kind: 'b', name: 'b', namespaces: ['b'], stories: ['b1', 'b2'] }, -]); - -const initialData = [ - { - kind: 'some.name.item1', - stories: ['a1', 'a2'], - }, - { - kind: 'another.space.20', - stories: ['b1', 'b2'], - }, -]; -const dataWithoutSeparator = createHierarchies(prepareStoriesForHierarchy(initialData)); -const dataWithSeparator = createHierarchies(prepareStoriesForHierarchy(initialData, '\\.')); - -const filter = 'th'; -const selectedKind = 'another.space.20'; - -const filteredData = storyFilter( - prepareStoriesForHierarchy(initialData, '\\.'), - filter, - selectedKind -); - -const filteredDataHierarchy = createHierarchies(filteredData); - -const decorator = withLifecyle({ - beforeEach() { - setContext({ - clientStore: { - getAll() { - return { shortcutOptions: {} }; - }, - subscribe() {}, - }, - }); - }, - afterEach() { - setContext(null); - }, -}); - -storiesOf('UI|stories/Stories', module) - .addDecorator(decorator) - .add('empty', () => ( - - )) - .add('simple', () => ( - - )) - .add('with hierarchy - hierarchySeparator is defined', () => ( - - )) - .add('without hierarchy - hierarchySeparator is defined', () => ( - - )) - .add('with highlighting when storiesFilter is provided', () => ( - - )); diff --git a/lib/ui/src/modules/ui/components/stories_panel/stories_tree/index.test.js b/lib/ui/src/modules/ui/components/stories_panel/stories_tree/index.test.js deleted file mode 100644 index bce23b8cddbd..000000000000 --- a/lib/ui/src/modules/ui/components/stories_panel/stories_tree/index.test.js +++ /dev/null @@ -1,411 +0,0 @@ -import { shallow, mount } from 'enzyme'; -import React from 'react'; -import Stories from './index'; -import { setContext } from '../../../../../compose'; -import { - createHierarchies, - createHierarchyRoot, - prepareStoriesForHierarchy, -} from '../../../libs/hierarchy'; -import { storyFilter } from '../../../libs/filters'; - -const leftClick = { button: 0 }; - -describe('manager.ui.components.stories_panel.stories_tree', () => { - beforeEach(() => - setContext({ - clientStore: { - getAll() { - return { shortcutOptions: {} }; - }, - subscribe() {}, - }, - })); - - afterEach(() => setContext(null)); - - const data = createHierarchies([ - { kind: 'a', name: 'a', namespaces: ['a'], stories: ['a1', 'a2'] }, - { kind: 'b', name: 'b', namespaces: ['b'], stories: ['b1', 'b2'] }, - ])[0]; - - const initialData = [ - { - kind: 'some.name.item1', - stories: ['a1', 'a2'], - }, - { - kind: 'another.space.20', - stories: ['b1', 'b2'], - }, - ]; - - const dataWithoutSeparator = createHierarchies(prepareStoriesForHierarchy(initialData))[0]; - const dataWithSeparator = createHierarchies(prepareStoriesForHierarchy(initialData, '\\.'))[0]; - - describe('render', () => { - test('should render stories - empty', () => { - const wrap = shallow( - - ); - - const list = wrap - .find('div') - .first() - .children('div') - .last(); - - expect(list.text()).toBe(''); - }); - - test('should render stories', () => { - const wrap = shallow( - - ); - - const output = wrap.html(); - - expect(output).toMatch(/b/); - expect(output).toMatch(/b2/); - }); - - test('should render stories with hierarchy - hierarchySeparator is defined', () => { - const wrap = shallow( - - ); - - const output = wrap.html(); - - expect(output).toMatch(/some/); - expect(output).not.toMatch(/>name { - const wrap = shallow( - - ); - - const output = wrap.html(); - - expect(output).toMatch(/some.name.item1/); - expect(output).not.toMatch(/a1/); - expect(output).not.toMatch(/a2/); - expect(output).toMatch(/another.space.20/); - expect(output).toMatch(/b1/); - expect(output).toMatch(/b2/); - }); - - test('should render stories with initially selected nodes according to the selectedHierarchy', () => { - const wrap = shallow( - - ); - - const { nodes } = wrap.state(); - - expect(nodes).toEqual({ - 'another@namespace': true, - 'another@space@namespace': true, - 'another@space@20@namespace': true, - }); - }); - - test('should contain state with all selected nodes after clicking on the nodes', () => { - const wrap = mount( - - ); - - const kind = wrap.find('[data-name="some"]').first(); - kind.simulate('click', leftClick); - - const { nodes } = wrap.state(); - - expect(nodes).toEqual({ - 'another@namespace': true, - 'another@space@namespace': true, - 'another@space@20@namespace': true, - 'some@namespace': true, - }); - }); - - test('should recalculate selected nodes after selectedHierarchy changes', () => { - const wrap = mount( - - ); - - wrap.setProps({ selectedHierarchy: ['another', 'space', '20'] }); - - const { nodes } = wrap.state(); - - expect(nodes).toEqual({ - 'another@namespace': true, - 'another@space@namespace': true, - 'another@space@20@namespace': true, - }); - }); - - test('should add selected nodes to the state after selectedHierarchy changes with a new value', () => { - const wrap = mount( - - ); - - wrap.setProps({ selectedHierarchy: ['some', 'name', 'item1'] }); - - const { nodes } = wrap.state(); - - expect(nodes).toEqual({ - 'another@namespace': true, - 'another@space@namespace': true, - 'another@space@20@namespace': true, - 'some@namespace': true, - 'some@name@namespace': true, - 'some@name@item1@namespace': true, - }); - }); - - test('should not call setState when selectedHierarchy prop changes with the same value', () => { - const selectedHierarchy = ['another', 'space', '20']; - const wrap = mount( - - ); - - const setState = jest.fn(); - wrap.instance().setState = setState; - - wrap.setProps({ selectedHierarchy }); - - expect(setState).not.toHaveBeenCalled(); - }); - - test('should render stories with with highlighting when storiesFilter is provided', () => { - const filter = 'th'; - const selectedKind = 'another.space.20'; - - const filteredData = storyFilter( - prepareStoriesForHierarchy(initialData, '\\.'), - filter, - selectedKind - ); - - const filteredDataHierarchy = createHierarchies(filteredData); - - const wrap = mount( - - ); - - const highlightedElements = wrap.find('strong'); - - expect(highlightedElements.text()).toBe('th'); - }); - }); - - describe('events', () => { - test('should not call the onSelectStory prop when a collapsed kind is clicked', () => { - const onSelectStory = jest.fn(); - const wrap = mount( - - ); - - const kind = wrap.find('[data-name="a"]').first(); - kind.simulate('click', leftClick); - - expect(onSelectStory).not.toHaveBeenCalled(); - }); - - test("shouldn't call the onSelectStory prop when an expanded kind is clicked", () => { - const onSelectStory = jest.fn(); - const wrap = mount( - - ); - - const kind = wrap - .find('[data-name="a"]') - .filterWhere(el => el.text() === 'a') - .last(); - kind.simulate('click'); - - onSelectStory.mockClear(); - kind.simulate('click'); - - expect(onSelectStory).not.toHaveBeenCalled(); - }); - - test('should call the onSelectStory prop when a story is clicked', () => { - const onSelectStory = jest.fn(); - const wrap = mount( - - ); - - const kind = wrap.find('[data-name="b1"]').first(); - kind.simulate('click', leftClick); - - expect(onSelectStory).toHaveBeenCalledWith('b', 'b1'); - }); - - test('should call the onSelectStory prop when a story is clicked - hierarchySeparator is defined', () => { - const onSelectStory = jest.fn(); - const wrap = mount( - - ); - - wrap - .find('[data-name="another"]') - .first() - .simulate('click', leftClick); - - wrap - .find('[data-name="space"]') - .first() - .simulate('click', leftClick); - - wrap - .find('[data-name="20"]') - .first() - .simulate('click', leftClick); - - expect(onSelectStory).not.toHaveBeenCalled(); - - wrap - .find('[data-name="b2"]') - .first() - .simulate('click', leftClick); - - expect(onSelectStory).toHaveBeenCalledWith('another.space.20', 'b2'); - }); - - test('should call the onSelectStory prop when a story is selected with enter key', () => { - const onSelectStory = jest.fn(); - const wrap = mount( - - ); - - wrap - .find('[data-name="another"]') - .first() - .simulate('keyDown', { keyCode: 13 }); - - expect(onSelectStory).not.toHaveBeenCalled(); - - wrap - .find('[data-name="space"]') - .first() - .simulate('keyDown', { keyCode: 13 }); - - // enter press on native link triggers click event - wrap - .find('[data-name="20"]') - .first() - .simulate('click', leftClick); - - wrap - .find('[data-name="b2"]') - .first() - .simulate('click', leftClick); - - expect(onSelectStory).toHaveBeenCalledWith('another.space.20', 'b2'); - }); - }); -}); diff --git a/lib/ui/src/modules/ui/configs/handle_keyevents.js b/lib/ui/src/modules/ui/configs/handle_keyevents.js deleted file mode 100755 index 73bdc51ab729..000000000000 --- a/lib/ui/src/modules/ui/configs/handle_keyevents.js +++ /dev/null @@ -1,11 +0,0 @@ -import { window } from 'global'; -import keyEvents from '../../../libs/key_events'; - -export default function(actions) { - window.onkeydown = e => { - const parsedEvent = keyEvents(e); - if (parsedEvent) { - actions.shortcuts.handleEvent(parsedEvent); - } - }; -} diff --git a/lib/ui/src/modules/ui/configs/handle_keyevents.test.js b/lib/ui/src/modules/ui/configs/handle_keyevents.test.js deleted file mode 100755 index 74fb207ae8c8..000000000000 --- a/lib/ui/src/modules/ui/configs/handle_keyevents.test.js +++ /dev/null @@ -1,91 +0,0 @@ -import { window } from 'global'; -import keycode from 'keycode'; -import handleKeyEvents from './handle_keyevents'; - -describe('manager.ui.config.handle_keyevents', () => { - test('should call the correct action', () => { - const actions = { - shortcuts: { - handleEvent: jest.fn(), - }, - }; - const originalOnkeydown = window.onkeydown; - handleKeyEvents(actions); - - const e = { - ctrlKey: true, - shiftKey: true, - keyCode: keycode('F'), - preventDefault() {}, - target: { - tagName: 'DIV', - getAttribute() { - return null; - }, - }, - }; - window.onkeydown(e); - - expect(actions.shortcuts.handleEvent).toHaveBeenCalled(); - - window.onkeydown = originalOnkeydown; - }); - - test('should not call any actions if the event target is an input', () => { - const actions = { - shortcuts: { - handleEvent: jest.fn(), - }, - }; - const originalOnkeydown = window.onkeydown; - handleKeyEvents(actions); - - const e = { - ctrlKey: true, - shiftKey: true, - keyCode: keycode('F'), - preventDefault() {}, - target: { - tagName: 'INPUT', - getAttribute() { - return null; - }, - }, - }; - window.onkeydown(e); - - expect(actions.shortcuts.handleEvent).not.toHaveBeenCalled(); - - window.onkeydown = originalOnkeydown; - }); - - test('should not call any actions if the event target has contenteditable enabled', () => { - const actions = { - shortcuts: { - handleEvent: jest.fn(), - }, - }; - - const originalOnkeydown = window.onkeydown; - handleKeyEvents(actions); - - const e = { - ctrlKey: true, - shiftKey: true, - keyCode: keycode('F'), - preventDefault() {}, - target: { - tagName: 'DIV', - getAttribute(attr) { - return /contenteditable/i.test(attr) ? '' : null; - }, - }, - }; - - window.onkeydown(e); - - expect(actions.shortcuts.handleEvent).not.toHaveBeenCalled(); - - window.onkeydown = originalOnkeydown; - }); -}); diff --git a/lib/ui/src/modules/ui/configs/handle_routing.js b/lib/ui/src/modules/ui/configs/handle_routing.js deleted file mode 100755 index 815102362fcf..000000000000 --- a/lib/ui/src/modules/ui/configs/handle_routing.js +++ /dev/null @@ -1,247 +0,0 @@ -// import { window, location, history } from 'global'; -// import qs from 'qs'; - -// export const config = { -// insidePopState: false, -// }; - -// export function getUrlState(data) { -// const { selectedKind, selectedStory, customQueryParams } = data; - -// const { -// goFullScreen: full, -// showAddonPanel: addons, -// showStoriesPanel: stories, -// addonPanelInRight: panelRight, -// } = data.shortcutOptions; - -// const { selectedAddonPanel: addonPanel } = data; - -// const urlObj = { -// selectedKind, -// selectedStory, -// full: Number(full), -// addons: Number(addons), -// stories: Number(stories), -// panelRight: Number(panelRight), -// addonPanel, -// ...customQueryParams, -// }; - -// const url = `?${qs.stringify(urlObj)}`; - -// return { -// ...urlObj, -// full, -// addons, -// stories, -// panelRight, -// url, -// }; -// } - -// export function changeUrl(clientStore, usePush) { -// // Do not change the URL if we are inside a popState event. -// if (config.insidePopState) return; - -// const data = clientStore.getAll(); -// if (!data.selectedKind) return; - -// const state = getUrlState(data); -// history[usePush ? 'pushState' : 'replaceState'](state, '', state.url); -// } - -// export function updateStore(queryParams, actions) { -// const { -// selectedKind, -// selectedStory, -// full = 0, -// down = 1, -// addons = down, -// left = 1, -// stories = left, -// panelRight = 0, -// downPanel, -// addonPanel = downPanel, -// ...customQueryParams -// } = queryParams; - -// if (selectedKind) { -// actions.api.selectStory(selectedKind, selectedStory); -// } - -// actions.shortcuts.setOptions({ -// goFullScreen: Boolean(Number(full)), -// showAddonPanel: Boolean(Number(addons)), -// showStoriesPanel: Boolean(Number(stories)), -// addonPanelInRight: Boolean(Number(panelRight)), -// }); - -// if (addonPanel) { -// actions.ui.selectAddonPanel(addonPanel); -// } -// actions.api.setQueryParams(customQueryParams); -// } - -// export function handleInitialUrl(actions, l) { -// const queryString = l.search.substring(1); -// if (!queryString || queryString === '') return; - -// const parsedQs = qs.parse(queryString); -// updateStore(parsedQs, actions); -// } - -// export default function({ clientStore }, actions) { -// // handle initial URL -// handleInitialUrl(actions, location); - -// const data = clientStore.getAll(); -// let prevKind = data.selectedKind; -// let prevStory = data.selectedStory; - -// // subscribe to clientStore and change the URL -// clientStore.subscribe(() => { -// const { selectedKind, selectedStory } = clientStore.getAll(); -// // use pushState only when a new story is selected -// const usePush = -// prevKind != null && -// prevStory != null && -// (selectedKind !== prevKind || selectedStory !== prevStory); -// changeUrl(clientStore, usePush); -// prevKind = selectedKind; -// prevStory = selectedStory; -// }); -// changeUrl(clientStore); - -// // handle back button -// window.onpopstate = () => { -// config.insidePopState = true; -// handleInitialUrl(actions, location); -// config.insidePopState = false; -// }; -// } - -import { window, location, history } from 'global'; -import { autorun } from 'mobx'; -import qs from 'qs'; - -export const config = { - insidePopState: false -}; - -export function getUrlState(store) { - const { - selectedKind, - selectedStory, - customQueryParams, - selectedAddonPanel: addonPanel - } = store; - - const { - goFullScreen: full, - showAddonPanel: addons, - showStoriesPanel: stories, - addonPanelInRight: panelRight - } = store.shortcutOptions; - - const urlObj = { - selectedKind, - selectedStory, - full: Number(full), - addons: Number(addons), - stories: Number(stories), - panelRight: Number(panelRight), - addonPanel, - ...customQueryParams - }; - - const url = `?${qs.stringify(urlObj)}`; - - return { - ...urlObj, - full, - addons, - stories, - panelRight, - url - }; -} - -export function changeUrl(store, usePush) { - // Do not change the URL if we are inside a popState event. - if (config.insidePopState) return; - - if (!store.selectedKind) return; - - const state = getUrlState(store); - history[usePush ? 'pushState' : 'replaceState'](state, '', state.url); -} - -export function updateStore(store, queryParams) { - const { - selectedKind, - selectedStory, - full = 0, - down = 1, - addons = down, - left = 1, - stories = left, - panelRight = 0, - downPanel, - addonPanel = downPanel, - ...customQueryParams - } = queryParams; - - if (selectedKind) { - store.selectStory(selectedKind, selectedStory); - } - - store.setShortcutsOptions({ - goFullScreen: Boolean(Number(full)), - showAddonPanel: Boolean(Number(addons)), - showStoriesPanel: Boolean(Number(stories)), - addonPanelInRight: Boolean(Number(panelRight)) - }); - - if (addonPanel) { - store.selectAddonPanel(addonPanel); - } - store.setQueryParams(customQueryParams); -} - -export function handleInitialUrl(store, l) { - const queryString = l.search.substring(1); - if (!queryString || queryString === '') return; - - const parsedQs = qs.parse(queryString); - updateStore(store, parsedQs); -} - -export default function(store) { - // handle initial URL - handleInitialUrl(store, location); - - let prevKind = store.selectedKind; - let prevStory = store.selectedStory; - - // subscribe to clientStore and change the URL - autorun(() => { - // use pushState only when a new story is selected - const usePush = - prevKind != null && - prevStory != null && - (store.selectedKind !== prevKind || store.selectedStory !== prevStory); - changeUrl(store, usePush); - prevKind = store.selectedKind; - prevStory = store.selectedStory; - }); - - changeUrl(store); - - // handle back button - window.onpopstate = () => { - config.insidePopState = true; - handleInitialUrl(store, location); - config.insidePopState = false; - }; -} diff --git a/lib/ui/src/modules/ui/configs/handle_routing.test.js b/lib/ui/src/modules/ui/configs/handle_routing.test.js deleted file mode 100755 index 5bf2c83b467c..000000000000 --- a/lib/ui/src/modules/ui/configs/handle_routing.test.js +++ /dev/null @@ -1,130 +0,0 @@ -import { history } from 'global'; -import { changeUrl, handleInitialUrl, config } from './handle_routing'; - -jest.mock('global', () => ({ - window: global, - location: global.location, - history: { - ...global.history, - replaceState: jest.fn(), - }, -})); - -describe('manager.ui.config.handle_routing', () => { - describe('changeUrl', () => { - it('should not do anything if insidePopState=true', () => { - config.insidePopState = true; - // This should throws an error if insidePopState = false - changeUrl(null); - config.insidePopState = false; - }); - - it('should put the correct URL and state to replaceState', () => { - const state = { - selectedKind: 'kk', - selectedStory: 'ss', - shortcutOptions: { - goFullScreen: false, - showAddonPanel: true, - showStoriesPanel: true, - addonPanelInRight: true, - }, - selectedAddonPanel: 'pp', - customQueryParams: { - customText: 'test', - }, - }; - const clientStore = { - getAll: () => state, - }; - const url = - '?selectedKind=kk&selectedStory=ss&full=0&addons=1&stories=1&panelRight=1&addonPanel=pp&customText=test'; - - const d = { - url, - selectedKind: 'kk', - selectedStory: 'ss', - full: false, - addons: true, - stories: true, - panelRight: true, - addonPanel: 'pp', - customText: 'test', - }; - - changeUrl(clientStore); - - expect(history.replaceState).toHaveBeenCalledWith(d, '', url); - }); - }); - - describe('handleInitialUrl', () => { - it('should call the correct action according to URL', () => { - const actions = { - api: { - selectStory: jest.fn(), - setQueryParams: jest.fn(), - }, - shortcuts: { - setOptions: jest.fn(), - }, - ui: { - selectAddonPanel: jest.fn(), - }, - }; - const url = - '?selectedKind=kk&selectedStory=ss&full=1&addons=0&stories=0&panelRight=0&addonPanel=test&customText=teststring'; - - const location = { - search: url, - }; - - handleInitialUrl(actions, location); - - expect(actions.api.selectStory).toHaveBeenCalled(); - expect(actions.shortcuts.setOptions).toHaveBeenCalled(); - expect(actions.ui.selectAddonPanel).toHaveBeenCalled(); - expect(actions.shortcuts.setOptions).toHaveBeenCalledWith({ - goFullScreen: true, - showAddonPanel: false, - showStoriesPanel: false, - addonPanelInRight: false, - }); - expect(actions.ui.selectAddonPanel).toHaveBeenCalledWith('test'); - expect(actions.api.setQueryParams).toHaveBeenCalledWith({ - customText: 'teststring', - }); - }); - - test('should handle URLs with outdated param names', () => { - const actions = { - api: { - selectStory: jest.fn(), - setQueryParams: jest.fn(), - }, - shortcuts: { - setOptions: jest.fn(), - }, - ui: { - selectAddonPanel: jest.fn(), - }, - }; - const url = '?down=0&left=0&downPanel=test'; - - const location = { - search: url, - }; - - handleInitialUrl(actions, location); - - expect(actions.shortcuts.setOptions).toHaveBeenCalled(); - expect(actions.shortcuts.setOptions).toHaveBeenCalledWith({ - goFullScreen: false, - showAddonPanel: false, - showStoriesPanel: false, - addonPanelInRight: false, - }); - expect(actions.ui.selectAddonPanel).toHaveBeenCalledWith('test'); - }); - }); -}); diff --git a/lib/ui/src/modules/ui/configs/init_panels.js b/lib/ui/src/modules/ui/configs/init_panels.js deleted file mode 100644 index b1f7b58a23af..000000000000 --- a/lib/ui/src/modules/ui/configs/init_panels.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function({ provider }, actionMap) { - const panels = Object.keys(provider.getPanels()); - - if (panels.length > 0) { - actionMap.ui.selectAddonPanel(panels[0]); - } -} diff --git a/lib/ui/src/modules/ui/configs/init_panels.test.js b/lib/ui/src/modules/ui/configs/init_panels.test.js deleted file mode 100644 index 8c9040a507d1..000000000000 --- a/lib/ui/src/modules/ui/configs/init_panels.test.js +++ /dev/null @@ -1,25 +0,0 @@ -import initPanels from './init_panels'; - -describe('manager.ui.config.init_panels', () => { - test('should call the selectAddonPanel with first panel name', () => { - const actions = { - ui: { - selectAddonPanel: jest.fn(), - }, - }; - - const provider = { - getPanels() { - return { - test1: {}, - test2: {}, - test3: {}, - }; - }, - }; - - initPanels({ provider }, actions); - - expect(actions.ui.selectAddonPanel).toHaveBeenCalledWith('test1'); - }); -}); diff --git a/lib/ui/src/modules/ui/containers/layout.test.js b/lib/ui/src/modules/ui/containers/layout.test.js deleted file mode 100755 index 2aabf74f37d4..000000000000 --- a/lib/ui/src/modules/ui/containers/layout.test.js +++ /dev/null @@ -1,18 +0,0 @@ -import { mapper } from './layout'; - -describe('manager.ui.containers.layout', () => { - describe('mapper', () => { - test('should give correct data', () => { - const state = { - shortcutOptions: { - showStoriesPanel: 'aa', - showAddonPanel: 'bb', - goFullScreen: 'cc', - }, - }; - const data = mapper(state); - - expect(data).toEqual(state.shortcutOptions); - }); - }); -}); diff --git a/lib/ui/src/modules/ui/containers/routed_link.js b/lib/ui/src/modules/ui/containers/routed_link.js deleted file mode 100644 index 11191670b6eb..000000000000 --- a/lib/ui/src/modules/ui/containers/routed_link.js +++ /dev/null @@ -1,27 +0,0 @@ -import { RoutedLink, MenuLink } from '@storybook/components'; - -import React from 'react'; - -import { inject } from 'mobx-react'; - -// import genPoddaLoader from '../libs/gen_podda_loader'; -import { getUrlState } from '../configs/handle_routing'; -// import compose from '../../../compose'; - -export function mapper(store, props) { - const { url } = getUrlState({ ...store, ...props.overrideParams }); - - return { - href: url - }; -} - -const ComposedMenuLink = inject(({ store }, props) => mapper(store, props))( - MenuLink -); -const ComposedRoutedLink = inject(({ store }, props) => mapper(store, props))( - RoutedLink -); - -export { ComposedMenuLink as MenuLink }; -export { ComposedRoutedLink as RoutedLink }; diff --git a/lib/ui/src/modules/ui/containers/search_box.js b/lib/ui/src/modules/ui/containers/search_box.js deleted file mode 100644 index a90570e52314..000000000000 --- a/lib/ui/src/modules/ui/containers/search_box.js +++ /dev/null @@ -1,15 +0,0 @@ -import { inject } from 'mobx-react'; -import { observe } from 'mobx'; - -import SearchBox from '../components/search_box'; - -export const mapper = store => { - return { - showSearchBox: store.shortcutOptions.showSearchBox, - stories: store.stories, - onSelectStory: (kind, story) => store.selectStory(kind, story), - onClose: () => store.toggleSearchBox() - }; -}; - -export default inject(({ store }) => mapper(store))(SearchBox); diff --git a/lib/ui/src/modules/ui/index.js b/lib/ui/src/modules/ui/index.js deleted file mode 100755 index 321a4c535a0c..000000000000 --- a/lib/ui/src/modules/ui/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import routes from './routes'; -import actions from './actions'; -import initPanels from './configs/init_panels'; -import handleRouting from './configs/handle_routing'; -import handleKeyEvents from './configs/handle_keyevents'; - -export default { - routes, - actions, - defaultState: { - showShortcutsHelp: false, - }, - load(c, a) { - initPanels(c, a); - handleRouting(c, a); - handleKeyEvents(a); - }, -}; diff --git a/lib/ui/src/modules/ui/libs/gen_podda_loader.js b/lib/ui/src/modules/ui/libs/gen_podda_loader.js deleted file mode 100644 index fd669a40c9fc..000000000000 --- a/lib/ui/src/modules/ui/libs/gen_podda_loader.js +++ /dev/null @@ -1,18 +0,0 @@ -export default function genPoddaLoader(fn) { - return (props, onData, env) => { - const { clientStore } = env.context(); - - const processState = () => { - try { - const state = clientStore.getAll(); - const data = fn(state, props, env); - onData(null, data); - } catch (ex) { - onData(ex); - } - }; - - processState(); - return clientStore.subscribe(processState); - }; -} diff --git a/lib/ui/src/modules/ui/libs/gen_podda_loader.test.js b/lib/ui/src/modules/ui/libs/gen_podda_loader.test.js deleted file mode 100644 index 6498469cc245..000000000000 --- a/lib/ui/src/modules/ui/libs/gen_podda_loader.test.js +++ /dev/null @@ -1,114 +0,0 @@ -import Podda from '@storybook/podda'; -import genPoddaLoader from './gen_podda_loader'; - -/* eslint-disable react/destructuring-assignment */ - -describe('manager.ui.libs.gen_podda_loader', () => { - describe('mapper', () => { - it('should map the podda store state', () => { - const aa = 10; - const bb = 20; - const cc = 40; - const mapper = state => ({ - aa: state.aa, - bb: state.bb, - }); - const clientStore = new Podda({ aa, bb, cc }); - const loader = genPoddaLoader(mapper); - - const onData = jest.fn(); - loader({}, onData, { context: () => ({ clientStore }) }); - - expect(onData).toHaveBeenCalledWith(null, { aa, bb }); - }); - - it('should get props', () => { - const aa = 10; - const bb = 20; - const cc = 40; - const mapper = (state, props) => ({ - aa: props.aa, - bb: props.bb, - }); - const clientStore = new Podda(); - const loader = genPoddaLoader(mapper); - - const onData = jest.fn(); - loader({ aa, bb, cc }, onData, { context: () => ({ clientStore }) }); - - expect(onData).toHaveBeenCalledWith(null, { aa, bb }); - }); - - it('should get env', () => { - const aa = 10; - const bb = 20; - const cc = 40; - const mapper = (state, props, env) => ({ - aa: env.aa, - bb: env.bb, - }); - - const clientStore = new Podda(); - const loader = genPoddaLoader(mapper); - - const onData = jest.fn(); - loader({}, onData, { context: () => ({ clientStore }), aa, bb, cc }); - - expect(onData).toHaveBeenCalledWith(null, { aa, bb }); - }); - }); - - describe('core', () => { - it('should handle errors in the mapper', () => { - const mapper = () => { - throw new Error('this is the error'); - }; - const clientStore = new Podda(); - const loader = genPoddaLoader(mapper); - - const onData = jest.fn(); - loader({}, onData, { context: () => ({ clientStore }) }); - - expect(onData.mock.calls[0].toString()).toMatch(/this is the error/); - }); - - it('should run when the podda store changed', () => { - const aa = 10; - const bb = 20; - const cc = 40; - const mapper = state => ({ - aa: state.aa, - bb: state.bb, - }); - const clientStore = new Podda({ aa, bb, cc }); - const loader = genPoddaLoader(mapper); - - const onData = jest.fn(); - loader({ aa, bb, cc }, onData, { context: () => ({ clientStore }) }); - clientStore.set('aa', 1000); - - expect(onData).toHaveBeenCalledWith(null, { aa: 1000, bb }); - }); - - it('should not run when podda subscription stopped', () => { - const aa = 10; - const bb = 20; - const cc = 40; - const mapper = state => ({ - aa: state.aa, - bb: state.bb, - }); - const clientStore = new Podda({ aa, bb, cc }); - const loader = genPoddaLoader(mapper); - - const onData = jest.fn(); - const stop = loader({ aa, bb, cc }, onData, { - context: () => ({ clientStore }), - }); - stop(); - clientStore.set('aa', 1000); - - expect(onData).toHaveBeenCalled(); - }); - }); -}); diff --git a/lib/ui/src/modules/ui/routes.js b/lib/ui/src/modules/ui/routes.js deleted file mode 100755 index b6f5b715d796..000000000000 --- a/lib/ui/src/modules/ui/routes.js +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; - -import ReactModal from 'react-modal'; -import Layout from './containers/layout'; -import StoriesPanel from './containers/stories_panel'; -import AddonPanel from './containers/addon_panel'; -import ShortcutsHelp from './containers/shortcuts_help'; -import SearchBox from './containers/search_box'; - -export default function(injectDeps, { clientStore, provider, domNode }) { - const state = clientStore.getAll(); - // generate preview - const Preview = () => { - const preview = provider.renderPreview(state.selectedKind, state.selectedStory); - return preview; - }; - - // Tell react-modal which element to mark as aria-hidden - ReactModal.setAppElement(domNode); - - const Container = process.env.STORYBOOK_EXAMPLE_APP ? React.StrictMode : 'div'; - - const root = ( - - } - preview={() => } - addonPanel={() => } - shortcutsHelp={() => } - searchBox={() => } - /> - - ); - ReactDOM.render(root, domNode); -} diff --git a/lib/ui/src/routing.js b/lib/ui/src/routing.js index fd117eff5ed4..c268f0e71f66 100644 --- a/lib/ui/src/routing.js +++ b/lib/ui/src/routing.js @@ -1,31 +1,26 @@ import React from 'react'; +import PropTypes from 'prop-types'; import { matchPath } from 'react-router'; -import { BrowserRouter as Router, Route, Link } from 'react-router-dom'; +import { BrowserRouter as Router, Route } from 'react-router-dom'; import qs from 'qs'; -const QueryLink = props => ( - -); - const QueryRoute = ({ path, render }) => ( { - const match = matchPath(path, { - // keep backwards compatibility by asserting /components as default - path: - qs.parse(props.location.search, { ignoreQueryPrefix: true }).path || - '/components' - }); + render={({ location, ...props }) => { + const { path: queryPath } = qs.parse(location.search, { ignoreQueryPrefix: true }); + + // keep backwards compatibility by asserting /components as default + const match = matchPath(path, { path: queryPath || '/components' }); + if (match) return render({ ...props, location, match }); - if (match) { - return render({ ...props, match }); - } return null; }} /> ); -export { Router, QueryRoute as Route, QueryLink as Link }; +QueryRoute.propTypes = { + path: PropTypes.string.isRequired, + render: PropTypes.func.isRequired, +}; + +export { Router, QueryRoute as Route }; diff --git a/lib/ui/src/store.js b/lib/ui/src/store.js index 8ffe687b1148..e045c1067540 100644 --- a/lib/ui/src/store.js +++ b/lib/ui/src/store.js @@ -2,7 +2,7 @@ import { observable, action, set } from 'mobx'; import pick from 'lodash.pick'; import { features } from './libs/key_events'; -import checkIfMobileDevice from './modules/ui/libs/is_mobile_device'; +import checkIfMobileDevice from './libs/is_mobile_device'; const { userAgent } = global.window.navigator; const isMobileDevice = checkIfMobileDevice(userAgent); @@ -13,23 +13,23 @@ const deprecationMessage = (oldName, newName) => const renamedOptions = { showLeftPanel: 'showStoriesPanel', showDownPanel: 'showAddonPanel', - downPanelInRight: 'addonPanelInRight' + downPanelInRight: 'addonPanelInRight', }; -function ensureKind(storyKinds, selectedKind) { - if (!storyKinds) return selectedKind; +function ensureKind(stories, selectedKind) { + if (stories.length === 0) return selectedKind; + + const found = stories.find(item => item.kind === selectedKind); + if (found) return selectedKind; - const found = storyKinds.find(item => item.kind === selectedKind); - if (found) return found.kind; // if the selected kind is non-existant, select the first kind - const kinds = storyKinds.map(item => item.kind); - return kinds[0]; + return stories[0].kind; } -function ensureStory(storyKinds, selectedKind, selectedStory) { - if (!storyKinds) return selectedStory; +function ensureStory(stories, selectedKind, selectedStory) { + if (!stories.length === 0) return selectedStory; - const kindInfo = storyKinds.find(item => item.kind === selectedKind); + const kindInfo = stories.find(item => item.kind === selectedKind); if (!kindInfo) return null; const found = kindInfo.stories.find(item => item === selectedStory); @@ -45,9 +45,7 @@ export function ensurePanel(panels, selectedPanel, currentPanel) { // and output to console all available panels const logger = console; logger.group('Available Panels ID:'); - Object.keys(panels).forEach(panelID => - logger.log(`${panelID} (${panels[panelID].title})`) - ); + Object.keys(panels).forEach(panelID => logger.log(`${panelID} (${panels[panelID].title})`)); logger.groupEnd('Available Panels ID:'); return currentPanel; } @@ -55,6 +53,7 @@ export function ensurePanel(panels, selectedPanel, currentPanel) { const createStore = ({ provider }) => { const store = observable( { + stories: [], showShortcutsHelp: false, storyFilter: null, selectedAddonPanel: null, @@ -65,7 +64,7 @@ const createStore = ({ provider }) => { showAddonPanel: true, showSearchBox: false, addonPanelInRight: false, - enableShortcuts: true + enableShortcuts: true, }, uiOptions: { name: 'STORYBOOK', @@ -74,14 +73,14 @@ const createStore = ({ provider }) => { hierarchySeparator: '/', hierarchyRootSeparator: null, sidebarAnimations: true, - theme: null + theme: null, }, setOptions(options) { const newOptions = pick(options, Object.keys(this.uiOptions)); set(this.uiOptions, newOptions); - if (options.hasOwnProperty('selectedAddonPanel')) { + if (Object.prototype.hasOwnProperty.call(options, 'selectedAddonPanel')) { this.selectedAddonPanel = ensurePanel( provider.getPanels(), options.selectedAddonPanel, @@ -93,29 +92,26 @@ const createStore = ({ provider }) => { setShortcutsOptions(options) { const updatedOptions = { ...this.shortcutOptions, - ...pick(options, Object.keys(this.shortcutOptions)) + ...pick(options, Object.keys(this.shortcutOptions)), }; - const withNewNames = Object.keys(renamedOptions).reduce( - (acc, oldName) => { - const newName = renamedOptions[oldName]; - - if (oldName in options && !(newName in options)) { - if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line no-console - console.warn(deprecationMessage(oldName, newName)); - } + const withNewNames = Object.keys(renamedOptions).reduce((acc, oldName) => { + const newName = renamedOptions[oldName]; - return { - ...acc, - [newName]: options[oldName] - }; + if (oldName in options && !(newName in options)) { + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.warn(deprecationMessage(oldName, newName)); } - return acc; - }, - updatedOptions - ); + return { + ...acc, + [newName]: options[oldName], + }; + } + + return acc; + }, updatedOptions); set(this.shortcutOptions, withNewNames); }, @@ -155,18 +151,15 @@ const createStore = ({ provider }) => { break; } case features.FULLSCREEN: { - this.shortcutOptions.goFullScreen = !this.shortcutOptions - .goFullScreen; + this.shortcutOptions.goFullScreen = !this.shortcutOptions.goFullScreen; break; } case features.ADDON_PANEL: { - this.shortcutOptions.showAddonPanel = !this.shortcutOptions - .showAddonPanel; + this.shortcutOptions.showAddonPanel = !this.shortcutOptions.showAddonPanel; break; } case features.STORIES_PANEL: { - this.shortcutOptions.showStoriesPanel = !this.shortcutOptions - .showStoriesPanel; + this.shortcutOptions.showStoriesPanel = !this.shortcutOptions.showStoriesPanel; break; } case features.SHOW_SEARCH: { @@ -174,17 +167,16 @@ const createStore = ({ provider }) => { break; } case features.ADDON_PANEL_IN_RIGHT: { - this.shortcutOptions.addonPanelInRight = !this.shortcutOptions - .addonPanelInRight; + this.shortcutOptions.addonPanelInRight = !this.shortcutOptions.addonPanelInRight; + break; } default: - return; + break; } }, toggleSearchBox() { - this.shortcutOptions.showSearchBox = !this.shortcutOptions - .showSearchBox; + this.shortcutOptions.showSearchBox = !this.shortcutOptions.showSearchBox; }, /** UI actions */ @@ -202,13 +194,8 @@ const createStore = ({ provider }) => { setStories(stories) { const selectedKind = ensureKind(stories, this.selectedKind); - const currentSelectedStory = - this.selectedKind === selectedKind ? this.selectedStory : null; - const selectedStory = ensureStory( - stories, - selectedKind, - currentSelectedStory - ); + const currentSelectedStory = this.selectedKind === selectedKind ? this.selectedStory : null; + const selectedStory = ensureStory(stories, selectedKind, currentSelectedStory); this.stories = stories; this.selectedStory = selectedStory; @@ -224,11 +211,7 @@ const createStore = ({ provider }) => { }, selectInCurrentKind(story) { - const selectedStory = ensureStory( - state.stories, - state.selectedKind, - story - ); + const selectedStory = ensureStory(this.stories, this.selectedKind, story); this.selectedStory = selectedStory; }, @@ -236,7 +219,7 @@ const createStore = ({ provider }) => { setQueryParams(customQueryParams) { const updatedQueryParams = { ...this.customQueryParams, - ...customQueryParams + ...customQueryParams, }; Object.keys(customQueryParams).forEach(key => { @@ -246,7 +229,41 @@ const createStore = ({ provider }) => { }); this.customQueryParams = updatedQueryParams; - } + }, + + updateFromLocation(params) { + const { + selectedKind, + selectedStory, + full = 0, + down = 1, + addons = down, + left = 1, + stories = left, + panelRight = 0, + downPanel, + addonPanel = downPanel, + ...customQueryParams + } = params; + + if (selectedKind) { + this.selectedKind = selectedKind; + this.selectedStory = selectedStory; + } + + this.setShortcutsOptions({ + goFullScreen: Boolean(Number(full)), + showAddonPanel: Boolean(Number(addons)), + showStoriesPanel: Boolean(Number(stories)), + addonPanelInRight: Boolean(Number(panelRight)), + }); + + if (addonPanel) { + this.selectAddonPanel(addonPanel); + } + + this.setQueryParams(customQueryParams); + }, }, { setOptions: action, @@ -260,7 +277,8 @@ const createStore = ({ provider }) => { setStories: action, selectStory: action, selectInCurrentKind: action, - setQueryParams: action + setQueryParams: action, + updateFromLocation: action, } ); diff --git a/yarn.lock b/yarn.lock index 795faa5daf0d..4875dda63e3a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7968,13 +7968,7 @@ graphql-request@^1.4.0: dependencies: cross-fetch "1.1.1" -graphql@^0.12.3: - version "0.12.3" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.12.3.tgz#11668458bbe28261c0dcb6e265f515ba79f6ce07" - dependencies: - iterall "1.1.3" - -graphql@^0.13.2: +graphql@^0.12.3, graphql@^0.13.2: version "0.13.2" resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.13.2.tgz#4c740ae3c222823e7004096f832e7b93b2108270" dependencies: @@ -9491,10 +9485,6 @@ istanbul@^0.4.5: which "^1.1.1" wordwrap "^1.0.0" -iterall@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.3.tgz#1cbbff96204056dde6656e2ed2e2226d0e6d72c9" - iterall@^1.2.1: version "1.2.2" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" From a8bbc1a3f928ef5f815c193a653711c0607a9f60 Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Thu, 9 Aug 2018 19:30:36 +0200 Subject: [PATCH 009/413] And urlState for components need an href --- addons/links/src/manager.js | 1 + lib/ui/src/app.js | 39 +++-------- lib/ui/src/containers/addon_panel.js | 8 +-- lib/ui/src/containers/header.js | 41 ------------ lib/ui/src/containers/header.test.js | 45 ------------- lib/ui/src/containers/routed_link.js | 17 +++-- lib/ui/src/index.js | 12 ++-- lib/ui/src/init-history-handler.js | 10 +-- lib/ui/src/init-provider-api.js | 58 ++++++++-------- lib/ui/src/routing.js | 6 +- lib/ui/src/store.js | 98 +++++++++++++--------------- 11 files changed, 116 insertions(+), 219 deletions(-) delete mode 100755 lib/ui/src/containers/header.js delete mode 100755 lib/ui/src/containers/header.test.js diff --git a/addons/links/src/manager.js b/addons/links/src/manager.js index f3060807538f..a4c1c0bb2891 100644 --- a/addons/links/src/manager.js +++ b/addons/links/src/manager.js @@ -5,6 +5,7 @@ import { ADDON_ID, EVENT_ID, REQUEST_HREF_EVENT_ID, RECEIVE_HREF_EVENT_ID } from export function register() { addons.register(ADDON_ID, api => { const channel = addons.getChannel(); + channel.on(EVENT_ID, selection => { if (selection.kind != null) { api.selectStory(selection.kind, selection.story); diff --git a/lib/ui/src/app.js b/lib/ui/src/app.js index 0e47480eafe4..c3c36749bbaa 100644 --- a/lib/ui/src/app.js +++ b/lib/ui/src/app.js @@ -7,37 +7,18 @@ import AddonPanel from './containers/addon_panel'; import ShortcutsHelp from './containers/shortcuts_help'; import SearchBox from './containers/search_box'; -class App extends React.Component { - shouldComponentUpdate() { - return false; - } - - render() { - const { provider, store } = this.props; - - const Preview = () => provider.renderPreview(store.selectedKind, store.selectedStory); - - return ( - } - storiesPanel={() => } - shortcutsHelp={() => } - searchBox={() => } - addonPanel={() => } - /> - ); - } -} +const App = ({ preview: Preview }) => ( + } + storiesPanel={() => } + shortcutsHelp={() => } + searchBox={() => } + addonPanel={() => } + /> +); App.propTypes = { - provider: PropTypes.shape({ - renderPreview: PropTypes.func.isRequired, - getPanels: PropTypes.func.isRequired, - }).isRequired, - store: PropTypes.shape({ - selectedKind: PropTypes.string, - selectedStory: PropTypes.string, - }).isRequired, + preview: PropTypes.func.isRequired, }; export default App; diff --git a/lib/ui/src/containers/addon_panel.js b/lib/ui/src/containers/addon_panel.js index bf0de41b3b87..12c0a98845c4 100644 --- a/lib/ui/src/containers/addon_panel.js +++ b/lib/ui/src/containers/addon_panel.js @@ -1,12 +1,10 @@ import { inject } from 'mobx-react'; import { AddonPanel } from '@storybook/components'; -export function mapper(store, { panels }) { - const selectedPanel = store.selectedAddonPanel; - +export function mapper(store) { return { - panels, - selectedPanel, + panels: store.panels, + selectedPanel: store.selectedAddonPanel, onPanelSelect: panel => store.selectAddonPanel(panel), }; } diff --git a/lib/ui/src/containers/header.js b/lib/ui/src/containers/header.js deleted file mode 100755 index 0764eb1fd2f0..000000000000 --- a/lib/ui/src/containers/header.js +++ /dev/null @@ -1,41 +0,0 @@ -// TODO: figure out if it's even used -import pick from 'lodash.pick'; -// import { Header } from '@storybook/components'; -// import genPoddaLoader from '../libs/gen_podda_loader'; -// import compose from '../../../compose'; - -export const mapper = (state, props, { actions }) => { - const currentOptions = pick( - state.shortcutOptions, - 'showStoriesPanel', - 'enableShortcuts', - 'addonPanelInRight' - ); - - const actionMap = actions(); - const { uiOptions, isMobileDevice } = state; - const { name = '', url = '' } = uiOptions; - - const handleBurgerButtonClick = () => { - actionMap.shortcuts.setOptions({ - showStoriesPanel: !currentOptions.showStoriesPanel, - }); - }; - - const addonPanelInRight = isMobileDevice ? false : currentOptions.addonPanelInRight; - - return { - name, - url, - enableShortcutsHelp: currentOptions.enableShortcuts, - openShortcutsHelp: actionMap.ui.toggleShortcutsHelp, - onBurgerButtonClick: handleBurgerButtonClick, - isMobileDevice, - addonPanelInRight, - }; -}; - -// export default compose( -// genPoddaLoader(mapper), -// { withRef: false } -// )(Header); diff --git a/lib/ui/src/containers/header.test.js b/lib/ui/src/containers/header.test.js deleted file mode 100755 index ffce9a3aa751..000000000000 --- a/lib/ui/src/containers/header.test.js +++ /dev/null @@ -1,45 +0,0 @@ -import { mapper } from './header'; - -describe('manager.ui.containers.header', () => { - describe('mapper', () => { - test('should give correct data', () => { - const uiOptions = { - name: 'foo', - url: 'bar', - }; - const shortcutOptions = { - showStoriesPanel: 'aa', - enableShortcuts: true, - addonPanelInRight: true, - }; - const toggleShortcutsHelp = () => 'toggleShortcutsHelp'; - const props = {}; - const env = { - actions: () => ({ - ui: { - toggleShortcutsHelp, - }, - }), - }; - const state = { - uiOptions, - shortcutOptions, - }; - - const result = mapper(state, props, env); - - expect(result.openShortcutsHelp).toBe(toggleShortcutsHelp); - expect(result.enableShortcutsHelp).toEqual(shortcutOptions.enableShortcuts); - expect(result.addonPanelInRight).toEqual(true); - - const stateWhenMobileDevice = { - uiOptions, - shortcutOptions, - isMobileDevice: true, - }; - - const resultWhenMobileDevice = mapper(stateWhenMobileDevice, props, env); - expect(resultWhenMobileDevice.addonPanelInRight).toEqual(false); - }); - }); -}); diff --git a/lib/ui/src/containers/routed_link.js b/lib/ui/src/containers/routed_link.js index 1df7dd8a51f1..3c8a061851dd 100644 --- a/lib/ui/src/containers/routed_link.js +++ b/lib/ui/src/containers/routed_link.js @@ -1,14 +1,21 @@ -import { RoutedLink, MenuLink } from '@storybook/components'; +import { MenuLink } from '@storybook/components'; import { inject } from 'mobx-react'; +import qs from 'qs'; + +export function mapper(store, { overrideParams = {} }) { + const search = qs.stringify( + { + ...store.searchState, + ...overrideParams, + }, + { addQueryPrefix: true } + ); -export function mapper() { return { - href: '', + href: search, }; } const ComposedMenuLink = inject(({ store }, props) => mapper(store, props))(MenuLink); -const ComposedRoutedLink = inject(({ store }, props) => mapper(store, props))(RoutedLink); export { ComposedMenuLink as MenuLink }; -export { ComposedRoutedLink as RoutedLink }; diff --git a/lib/ui/src/index.js b/lib/ui/src/index.js index f6c61646f2be..8cbd9edce99f 100644 --- a/lib/ui/src/index.js +++ b/lib/ui/src/index.js @@ -2,8 +2,8 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { Provider as MobxProvider } from 'mobx-react'; import ReactModal from 'react-modal'; -import { Router } from 'react-router'; -import createHistory from 'history/createBrowserHistory'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { createBrowserHistory } from 'history'; import App from './app'; import Provider from './provider'; @@ -13,7 +13,7 @@ import initKeyHandler from './init-key-handler'; import createStore from './store'; import { Route } from './routing'; -const history = createHistory(); +const history = createBrowserHistory(); function renderStorybokUI(domNode, provider) { if (!(provider instanceof Provider)) { @@ -27,6 +27,8 @@ function renderStorybokUI(domNode, provider) { initHistoryHandler({ history, store }); initKeyHandler({ store }); + const Preview = () => provider.renderPreview(store.selectedKind, store.selectedStory); + // Tell react-modal which element to mark as aria-hidden ReactModal.setAppElement(domNode); @@ -35,8 +37,8 @@ function renderStorybokUI(domNode, provider) { const root = ( - - } /> + + } /> diff --git a/lib/ui/src/init-history-handler.js b/lib/ui/src/init-history-handler.js index bd7cf0642eb7..f569eb92a10e 100644 --- a/lib/ui/src/init-history-handler.js +++ b/lib/ui/src/init-history-handler.js @@ -11,15 +11,7 @@ export default ({ history, store }) => { selectedStory: store.selectedStory, }), () => { - const newParams = qs.stringify({ - selectedKind: store.selectedKind, - selectedStory: store.selectedStory, - full: Number(store.shortcutOptions.goFullScreen), - addons: Number(store.shortcutOptions.showAddonPanel), - stories: Number(store.shortcutOptions.showStoriesPanel), - panelRight: Number(store.shortcutOptions.addonPanelInRight), - addonPanel: store.selectedAddonPanel, - }); + const newParams = qs.stringify(store.searchState); // TODO: find a better way to ignore pop: maybe call this action from the selectStory action if (currentAction === 'POP') return; diff --git a/lib/ui/src/init-provider-api.js b/lib/ui/src/init-provider-api.js index 253f05edbcaf..76687fe03a78 100644 --- a/lib/ui/src/init-provider-api.js +++ b/lib/ui/src/init-provider-api.js @@ -1,23 +1,22 @@ -import { autorun } from 'mobx'; +import { reaction } from 'mobx'; import { EventEmitter } from 'events'; +import qs from 'qs'; export default ({ provider, store }) => { - const callbacks = new EventEmitter(); - let currentKind; - let currentStory; + const onStoryListeners = new EventEmitter(); const api = { onStory(cb) { - callbacks.on('story', cb); - if (currentKind && currentStory) { + onStoryListeners.on('story', cb); + if (store.selectedKind && store.selectedStory) { // Using a setTimeout to call the callback to make sure it's // not called on current event-loop. When users add callbacks // they usually expect it to be called in a future event loop. - setTimeout(() => cb(currentKind, currentStory), 0); + setTimeout(() => cb(store.selectedKind, store.selectedStory), 0); } - return function stopListening() { - callbacks.removeListener('story', cb); - }; + + // removeListener + return () => onStoryListeners.removeListener('story', cb); }, setStories(stories) { store.setStories(stories); @@ -26,7 +25,7 @@ export default ({ provider, store }) => { store.selectInCurrentKind(story); }, selectStory(kind, story) { - store.selectedStory(kind, story); + store.selectStory(kind, story); }, handleShortcut(event) { store.handleEvent(event); @@ -44,25 +43,32 @@ export default ({ provider, store }) => { } return undefined; }, - getUrlState(/* overrideParams */) { - // TODO: find out what this is supposed to be used for - // return window.location.href; - return ''; + getUrlState(overrideParams) { + const url = qs.stringify( + { + ...store.searchState, + ...overrideParams, + }, + { addQueryPrefix: true } + ); + + return { + ...store.urlState, + url, + }; }, }; provider.handleAPI(api); - autorun(() => { - if (!store.selectedKind) return; - - if (store.selectedKind === currentKind && store.selectedStory === currentStory) { - // No change in the selected story so avoid emitting 'story' - return; + reaction( + () => ({ + selectedKind: store.selectedKind, + selectedStory: store.selectedStory, + }), + () => { + if (!store.selectedKind) return; + onStoryListeners.emit('story', store.selectedKind, store.selectedStory); } - - currentKind = store.selectedKind; - currentStory = store.selectedStory; - callbacks.emit('story', store.selectedKind, store.selectedStory); - }); + ); }; diff --git a/lib/ui/src/routing.js b/lib/ui/src/routing.js index c268f0e71f66..31ed5be92530 100644 --- a/lib/ui/src/routing.js +++ b/lib/ui/src/routing.js @@ -7,11 +7,11 @@ import qs from 'qs'; const QueryRoute = ({ path, render }) => ( { - const { path: queryPath } = qs.parse(location.search, { ignoreQueryPrefix: true }); + const query = qs.parse(location.search, { ignoreQueryPrefix: true }); // keep backwards compatibility by asserting /components as default - const match = matchPath(path, { path: queryPath || '/components' }); - if (match) return render({ ...props, location, match }); + const match = matchPath(path, { path: query.path || '/components' }); + if (match) return render({ ...props, location, match, query }); return null; }} diff --git a/lib/ui/src/store.js b/lib/ui/src/store.js index e045c1067540..9197a07fb11d 100644 --- a/lib/ui/src/store.js +++ b/lib/ui/src/store.js @@ -1,3 +1,4 @@ +import { console as logger } from 'global'; import { observable, action, set } from 'mobx'; import pick from 'lodash.pick'; @@ -7,15 +8,6 @@ import checkIfMobileDevice from './libs/is_mobile_device'; const { userAgent } = global.window.navigator; const isMobileDevice = checkIfMobileDevice(userAgent); -const deprecationMessage = (oldName, newName) => - `The ${oldName} option has been renamed to ${newName} and will not be available in the next major Storybook release. Please update your config.`; - -const renamedOptions = { - showLeftPanel: 'showStoriesPanel', - showDownPanel: 'showAddonPanel', - downPanelInRight: 'addonPanelInRight', -}; - function ensureKind(stories, selectedKind) { if (stories.length === 0) return selectedKind; @@ -43,7 +35,6 @@ export function ensurePanel(panels, selectedPanel, currentPanel) { if (Object.keys(panels).indexOf(selectedPanel) >= 0) return selectedPanel; // if the selected panel is non-existant, select the current panel // and output to console all available panels - const logger = console; logger.group('Available Panels ID:'); Object.keys(panels).forEach(panelID => logger.log(`${panelID} (${panels[panelID].title})`)); logger.groupEnd('Available Panels ID:'); @@ -75,45 +66,29 @@ const createStore = ({ provider }) => { sidebarAnimations: true, theme: null, }, + customQueryParams: {}, + + get panels() { + return provider.getPanels(); + }, setOptions(options) { - const newOptions = pick(options, Object.keys(this.uiOptions)); - set(this.uiOptions, newOptions); + const { selectedAddonPanel, ...uiOptions } = options; + const newOptions = pick(uiOptions, Object.keys(this.uiOptions)); - if (Object.prototype.hasOwnProperty.call(options, 'selectedAddonPanel')) { + if (selectedAddonPanel) { this.selectedAddonPanel = ensurePanel( - provider.getPanels(), - options.selectedAddonPanel, + this.panels, + selectedAddonPanel, this.selectedAddonPanel ); } + + set(this.uiOptions, newOptions); }, setShortcutsOptions(options) { - const updatedOptions = { - ...this.shortcutOptions, - ...pick(options, Object.keys(this.shortcutOptions)), - }; - - const withNewNames = Object.keys(renamedOptions).reduce((acc, oldName) => { - const newName = renamedOptions[oldName]; - - if (oldName in options && !(newName in options)) { - if (process.env.NODE_ENV !== 'production') { - // eslint-disable-next-line no-console - console.warn(deprecationMessage(oldName, newName)); - } - - return { - ...acc, - [newName]: options[oldName], - }; - } - - return acc; - }, updatedOptions); - - set(this.shortcutOptions, withNewNames); + set(this.shortcutOptions, pick(options, Object.keys(this.shortcutOptions))); }, jumpToStory(direction) { @@ -175,6 +150,32 @@ const createStore = ({ provider }) => { } }, + get urlState() { + return { + selectedKind: this.selectedKind, + selectedStory: this.selectedStory, + full: this.shortcutOptions.goFullScreen, + addons: this.shortcutOptions.showAddonPanel, + stories: this.shortcutOptions.showStoriesPanel, + panelRight: this.shortcutOptions.addonPanelInRight, + addonPanel: this.selectedAddonPanel, + ...this.customQueryParams, + }; + }, + + get searchState() { + return { + selectedKind: this.selectedKind, + selectedStory: this.selectedStory, + full: Number(this.shortcutOptions.goFullScreen), + addons: Number(this.shortcutOptions.showAddonPanel), + stories: Number(this.shortcutOptions.showStoriesPanel), + panelRight: Number(this.shortcutOptions.addonPanelInRight), + addonPane: this.selectedAddonPanel, + ...this.customQueryParams, + }; + }, + toggleSearchBox() { this.shortcutOptions.showSearchBox = !this.shortcutOptions.showSearchBox; }, @@ -217,18 +218,13 @@ const createStore = ({ provider }) => { }, setQueryParams(customQueryParams) { - const updatedQueryParams = { - ...this.customQueryParams, - ...customQueryParams, - }; - - Object.keys(customQueryParams).forEach(key => { - if (updatedQueryParams[key] === null) { - delete updatedQueryParams[key]; - } - }); - - this.customQueryParams = updatedQueryParams; + set( + this.customQueryParams, + Object.keys(customQueryParams).reduce((acc, key) => { + if (customQueryParams[key] !== null) acc[key] = customQueryParams[key]; + return acc; + }, {}) + ); }, updateFromLocation(params) { From d9c57d2fd7b047ea1190d7f843c7e463d6f46d7b Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Fri, 10 Aug 2018 15:59:17 +0200 Subject: [PATCH 010/413] REFACTOR WIP --- .../stories/addon-graphql.stories.js | 24 +- lib/components/package.json | 8 + lib/components/src/alert/alert.js | 24 + lib/components/src/alert/alert.stories.js | 23 + .../src/explorer/__tests__}/menu_item.test.js | 2 +- .../explorer/__tests__}/text_filter.test.js | 2 +- .../src/explorer/__tests__/tree.test.js} | 0 .../__tests__}/tree_decorators_utils.test.js | 2 +- .../explorer/__tests__}/tree_header.test.js | 2 +- lib/components/src/explorer/explorer.js | 82 ++ .../src/explorer}/menu_item.js | 0 .../src/explorer}/menu_item.stories.js | 0 .../src/explorer}/search_box.js | 0 .../src/explorer}/search_box.stories.js | 0 .../src/explorer}/search_box.test.js | 0 .../src/explorer}/text_filter.js | 0 .../src/explorer}/text_filter.stories.js | 0 .../src/explorer/tree.js} | 6 +- .../src/explorer/tree.stories.js} | 0 .../src/explorer}/tree_decorators.js | 7 +- .../src/explorer}/tree_decorators_utils.js | 0 .../src/explorer}/tree_header.js | 0 .../src/explorer}/tree_node_type.js | 0 .../src/explorer}/tree_style.js | 0 lib/components/src/grid/grid.js | 55 ++ lib/components/src/grid/grid.stories.js | 72 ++ lib/components/src/heading/heading.js | 71 ++ lib/components/src/heading/heading.stories.js | 30 + lib/components/src/icons/component.js | 15 + lib/components/src/icons/gear.js | 15 + lib/components/src/icons/index.js | 4 + lib/components/src/index.js | 4 + lib/components/src/logo/logo.js | 60 ++ lib/components/src/logo/logo.stories.js | 8 + lib/components/src/nav/nav.js | 144 +++ lib/components/src/nav/nav.stories.js | 53 ++ lib/components/src/root/clock.js | 24 + lib/components/src/root/container.js | 61 ++ lib/components/src/root/desktop.js | 94 ++ lib/components/src/root/mobile.js | 117 +++ lib/components/src/root/root.js | 29 + lib/components/src/root/root.stories.js | 155 ++++ lib/ui/src/app.js | 28 +- lib/ui/src/components/stories_panel/index.js | 44 +- .../__snapshots__/index.stories.storyshot | 818 ------------------ lib/ui/src/containers/layout.js | 22 - .../containers/{stories_panel.js => nav.js} | 44 +- .../{stories_panel.test.js => nav.test.js} | 0 lib/ui/src/containers/preview_panel.js | 11 + lib/ui/src/containers/root.js | 15 + lib/ui/src/containers/routed_link.js | 2 +- lib/ui/src/index.js | 12 +- lib/ui/src/init-history-handler.js | 2 +- lib/ui/src/init-provider-api.js | 2 +- lib/ui/src/libs/is_mobile_device.js | 7 - lib/ui/src/libs/is_mobile_device.test.js | 28 - lib/ui/src/store.js | 74 +- yarn.lock | 69 +- 58 files changed, 1337 insertions(+), 1034 deletions(-) create mode 100644 lib/components/src/alert/alert.js create mode 100644 lib/components/src/alert/alert.stories.js rename lib/{ui/src/components => components/src/explorer/__tests__}/menu_item.test.js (97%) rename lib/{ui/src/components/stories_panel => components/src/explorer/__tests__}/text_filter.test.js (97%) rename lib/{ui/src/components/stories_panel/stories_tree/index.test.js => components/src/explorer/__tests__/tree.test.js} (100%) rename lib/{ui/src/components/stories_panel/stories_tree => components/src/explorer/__tests__}/tree_decorators_utils.test.js (94%) rename lib/{ui/src/components/stories_panel/stories_tree => components/src/explorer/__tests__}/tree_header.test.js (89%) create mode 100644 lib/components/src/explorer/explorer.js rename lib/{ui/src/components => components/src/explorer}/menu_item.js (100%) rename lib/{ui/src/components => components/src/explorer}/menu_item.stories.js (100%) rename lib/{ui/src/components => components/src/explorer}/search_box.js (100%) rename lib/{ui/src/components => components/src/explorer}/search_box.stories.js (100%) rename lib/{ui/src/components => components/src/explorer}/search_box.test.js (100%) rename lib/{ui/src/components/stories_panel => components/src/explorer}/text_filter.js (100%) rename lib/{ui/src/components/stories_panel => components/src/explorer}/text_filter.stories.js (100%) rename lib/{ui/src/components/stories_panel/stories_tree/index.js => components/src/explorer/tree.js} (97%) rename lib/{ui/src/components/stories_panel/stories_tree/index.stories.js => components/src/explorer/tree.stories.js} (100%) rename lib/{ui/src/components/stories_panel/stories_tree => components/src/explorer}/tree_decorators.js (97%) rename lib/{ui/src/components/stories_panel/stories_tree => components/src/explorer}/tree_decorators_utils.js (100%) rename lib/{ui/src/components/stories_panel/stories_tree => components/src/explorer}/tree_header.js (100%) rename lib/{ui/src/components/stories_panel/stories_tree => components/src/explorer}/tree_node_type.js (100%) rename lib/{ui/src/components/stories_panel/stories_tree => components/src/explorer}/tree_style.js (100%) create mode 100644 lib/components/src/grid/grid.js create mode 100644 lib/components/src/grid/grid.stories.js create mode 100644 lib/components/src/heading/heading.js create mode 100644 lib/components/src/heading/heading.stories.js create mode 100644 lib/components/src/icons/component.js create mode 100644 lib/components/src/icons/gear.js create mode 100644 lib/components/src/logo/logo.js create mode 100644 lib/components/src/logo/logo.stories.js create mode 100644 lib/components/src/nav/nav.js create mode 100644 lib/components/src/nav/nav.stories.js create mode 100644 lib/components/src/root/clock.js create mode 100644 lib/components/src/root/container.js create mode 100644 lib/components/src/root/desktop.js create mode 100644 lib/components/src/root/mobile.js create mode 100644 lib/components/src/root/root.js create mode 100644 lib/components/src/root/root.stories.js delete mode 100644 lib/ui/src/components/stories_panel/stories_tree/__snapshots__/index.stories.storyshot delete mode 100755 lib/ui/src/containers/layout.js rename lib/ui/src/containers/{stories_panel.js => nav.js} (68%) rename lib/ui/src/containers/{stories_panel.test.js => nav.test.js} (100%) create mode 100644 lib/ui/src/containers/preview_panel.js create mode 100755 lib/ui/src/containers/root.js delete mode 100644 lib/ui/src/libs/is_mobile_device.js delete mode 100644 lib/ui/src/libs/is_mobile_device.test.js diff --git a/examples/official-storybook/stories/addon-graphql.stories.js b/examples/official-storybook/stories/addon-graphql.stories.js index c51a69e856d7..ed83daffe2f7 100644 --- a/examples/official-storybook/stories/addon-graphql.stories.js +++ b/examples/official-storybook/stories/addon-graphql.stories.js @@ -1,15 +1,15 @@ -import { storiesOf } from '@storybook/react'; -import { setupGraphiQL } from '@storybook/addon-graphql'; +// import { storiesOf } from '@storybook/react'; +// import { setupGraphiQL } from '@storybook/addon-graphql'; // setup the graphiql helper which can be used with the add method later -const graphiql = setupGraphiQL({ url: 'http://localhost:3000/graphql' }); +// const graphiql = setupGraphiQL({ url: 'http://localhost:3000/graphql' }); -// run yarn graphql in examples/official-storybook to start graphql server -storiesOf('Addons|GraphQL', module).add( - 'get user info', - graphiql(`{ - user(id: "1") { - name - } - }`) -); +// // run yarn graphql in examples/official-storybook to start graphql server +// storiesOf('Addons|GraphQL', module).add( +// 'get user info', +// graphiql(`{ +// user(id: "1") { +// name +// } +// }`) +// ); diff --git a/lib/components/package.json b/lib/components/package.json index e65a4fbed633..b5e2b2a4454f 100644 --- a/lib/components/package.json +++ b/lib/components/package.json @@ -15,14 +15,22 @@ "storybook": "start-storybook -p 6006" }, "dependencies": { + "@ndelangen/react-treebeard": "^2.1.0", "emotion": "^9.2.6", "emotion-theming": "^9.2.6", + "fast-deep-equal": "^2.0.1", "global": "^4.3.2", + "lodash.debounce": "^4.0.8", "lodash.pick": "^4.4.0", "lodash.throttle": "^4.1.1", + "memoizee": "^0.4.13", "prop-types": "^15.6.2", "react-emotion": "^9.2.6", + "react-fuzzy": "^0.5.2", "react-inspector": "^2.3.0", + "react-lifecycles-compat": "^3.0.4", + "react-modal": "^3.5.1", + "react-resize-detector": "^3.1.1", "react-split-pane": "^0.1.82", "react-textarea-autosize": "^7.0.4", "render-fragment": "^0.1.1" diff --git a/lib/components/src/alert/alert.js b/lib/components/src/alert/alert.js new file mode 100644 index 000000000000..dc59c9d84110 --- /dev/null +++ b/lib/components/src/alert/alert.js @@ -0,0 +1,24 @@ +import React from 'react'; +import styled from 'react-emotion'; + +const Container = styled('div')(({ theme, type }) => ({ + display: 'flex', + background: theme[`${type}Color`] || type, + borderRadius: theme.mainBorderRadius, +})); +const Body = styled('div')(({ theme }) => ({ + flex: 1, + padding: theme.layoutMargin, +})); +const Icon = styled('div')(({ theme }) => ({ + padding: theme.layoutMargin, + paddingRight: 0, + fontSize: 11, +})); + +export default ({ children, icon, type }) => ( + + {icon ? {icon} : null} + {children} + +); diff --git a/lib/components/src/alert/alert.stories.js b/lib/components/src/alert/alert.stories.js new file mode 100644 index 000000000000..ab6480b0d248 --- /dev/null +++ b/lib/components/src/alert/alert.stories.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; + +import Alert from './alert'; + +storiesOf('Components|Alert', module) + .add('success', () => You did everything correctly!) + .add('warn', () => Just want to mention, it's not perfect.) + .add('fail', () => Computer says no.) + .add('custom', () => Computer says I love you.) + .add('with icon', () => ( + + It's bad. + + )) + .add('long text', () => ( + + Nulla facilisi. Nullam vitae nunc nibh. Vivamus massa felis, hendrerit imperdiet sollicitudin + consequat, pulvinar nec felis. Curabitur semper eu metus non pulvinar. Donec quis laoreet + velit. Nullam molestie gravida cursus. Vestibulum ullamcorper neque a nisi pellentesque + pulvinar. Proin tristique at nunc non venenatis. Fusce vitae dictum justo. + + )); diff --git a/lib/ui/src/components/menu_item.test.js b/lib/components/src/explorer/__tests__/menu_item.test.js similarity index 97% rename from lib/ui/src/components/menu_item.test.js rename to lib/components/src/explorer/__tests__/menu_item.test.js index 37b8b5d3cb44..02f2cd070bc5 100644 --- a/lib/ui/src/components/menu_item.test.js +++ b/lib/components/src/explorer/__tests__/menu_item.test.js @@ -1,6 +1,6 @@ import { shallow } from 'enzyme'; import React from 'react'; -import MenuItem from './menu_item'; +import MenuItem from '../menu_item'; const keyCodeEnter = 13; diff --git a/lib/ui/src/components/stories_panel/text_filter.test.js b/lib/components/src/explorer/__tests__/text_filter.test.js similarity index 97% rename from lib/ui/src/components/stories_panel/text_filter.test.js rename to lib/components/src/explorer/__tests__/text_filter.test.js index d64f318ff4a8..9fc397c04ecd 100755 --- a/lib/ui/src/components/stories_panel/text_filter.test.js +++ b/lib/components/src/explorer/__tests__/text_filter.test.js @@ -1,6 +1,6 @@ import { shallow, mount } from 'enzyme'; import React from 'react'; -import TextFilter from './text_filter'; +import TextFilter from '../text_filter'; jest.mock('lodash.debounce', () => jest.fn(fn => fn)); diff --git a/lib/ui/src/components/stories_panel/stories_tree/index.test.js b/lib/components/src/explorer/__tests__/tree.test.js similarity index 100% rename from lib/ui/src/components/stories_panel/stories_tree/index.test.js rename to lib/components/src/explorer/__tests__/tree.test.js diff --git a/lib/ui/src/components/stories_panel/stories_tree/tree_decorators_utils.test.js b/lib/components/src/explorer/__tests__/tree_decorators_utils.test.js similarity index 94% rename from lib/ui/src/components/stories_panel/stories_tree/tree_decorators_utils.test.js rename to lib/components/src/explorer/__tests__/tree_decorators_utils.test.js index ce73ae569b9d..eabffa57398e 100644 --- a/lib/ui/src/components/stories_panel/stories_tree/tree_decorators_utils.test.js +++ b/lib/components/src/explorer/__tests__/tree_decorators_utils.test.js @@ -1,5 +1,5 @@ import { shallow } from 'enzyme'; -import { highlightNode } from './tree_decorators_utils'; +import { highlightNode } from '../tree_decorators_utils'; describe('manager.ui.components.stories_panel.tree_decorators_utils.test', () => { describe('highlightNode', () => { diff --git a/lib/ui/src/components/stories_panel/stories_tree/tree_header.test.js b/lib/components/src/explorer/__tests__/tree_header.test.js similarity index 89% rename from lib/ui/src/components/stories_panel/stories_tree/tree_header.test.js rename to lib/components/src/explorer/__tests__/tree_header.test.js index 407c5be7343a..874cc4f2e5ea 100644 --- a/lib/ui/src/components/stories_panel/stories_tree/tree_header.test.js +++ b/lib/components/src/explorer/__tests__/tree_header.test.js @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import TreeHeader from './tree_header'; +import TreeHeader from '../tree_header'; describe('manager.ui.components.stories_panel.stories_tree.tree_header', () => { test('should render tree header content', () => { diff --git a/lib/components/src/explorer/explorer.js b/lib/components/src/explorer/explorer.js new file mode 100644 index 000000000000..a271fafd3c78 --- /dev/null +++ b/lib/components/src/explorer/explorer.js @@ -0,0 +1,82 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import pick from 'lodash.pick'; +import styled from 'react-emotion'; + +import Stories from './tree'; +import TextFilter from './text_filter'; + +const Wrapper = styled('div')( + ({ isMobileDevice }) => + isMobileDevice + ? { + padding: '10px', + } + : { + padding: '10px 0 10px 10px', + } +); + +const storyProps = [ + 'selectedKind', + 'selectedHierarchy', + 'selectedStory', + 'onSelectStory', + 'storyFilter', + 'sidebarAnimations', +]; + +function hierarchyContainsStories(storiesHierarchy) { + return storiesHierarchy && storiesHierarchy.map.size > 0; +} + +// This component gets a ref so it needs to be a class +// eslint-disable-next-line react/prefer-stateless-function +class Explorer extends Component { + render() { + const { onStoryFilter, storiesHierarchies, storyFilter } = this.props; + + return ( + + onStoryFilter('')} + onChange={text => onStoryFilter(text)} + /> + {storiesHierarchies.map( + hierarchy => + hierarchyContainsStories(hierarchy) && ( + + ) + )} + + ); + } +} + +Explorer.defaultProps = { + storiesHierarchies: [], + storyFilter: null, + onStoryFilter: () => {}, + openShortcutsHelp: null, +}; + +Explorer.propTypes = { + storiesHierarchies: PropTypes.arrayOf( + PropTypes.shape({ + namespaces: PropTypes.arrayOf(PropTypes.string), + name: PropTypes.string, + map: PropTypes.object, + }) + ), + storyFilter: PropTypes.string, + onStoryFilter: PropTypes.func, + + openShortcutsHelp: PropTypes.func, +}; + +export { Explorer }; diff --git a/lib/ui/src/components/menu_item.js b/lib/components/src/explorer/menu_item.js similarity index 100% rename from lib/ui/src/components/menu_item.js rename to lib/components/src/explorer/menu_item.js diff --git a/lib/ui/src/components/menu_item.stories.js b/lib/components/src/explorer/menu_item.stories.js similarity index 100% rename from lib/ui/src/components/menu_item.stories.js rename to lib/components/src/explorer/menu_item.stories.js diff --git a/lib/ui/src/components/search_box.js b/lib/components/src/explorer/search_box.js similarity index 100% rename from lib/ui/src/components/search_box.js rename to lib/components/src/explorer/search_box.js diff --git a/lib/ui/src/components/search_box.stories.js b/lib/components/src/explorer/search_box.stories.js similarity index 100% rename from lib/ui/src/components/search_box.stories.js rename to lib/components/src/explorer/search_box.stories.js diff --git a/lib/ui/src/components/search_box.test.js b/lib/components/src/explorer/search_box.test.js similarity index 100% rename from lib/ui/src/components/search_box.test.js rename to lib/components/src/explorer/search_box.test.js diff --git a/lib/ui/src/components/stories_panel/text_filter.js b/lib/components/src/explorer/text_filter.js similarity index 100% rename from lib/ui/src/components/stories_panel/text_filter.js rename to lib/components/src/explorer/text_filter.js diff --git a/lib/ui/src/components/stories_panel/text_filter.stories.js b/lib/components/src/explorer/text_filter.stories.js similarity index 100% rename from lib/ui/src/components/stories_panel/text_filter.stories.js rename to lib/components/src/explorer/text_filter.stories.js diff --git a/lib/ui/src/components/stories_panel/stories_tree/index.js b/lib/components/src/explorer/tree.js similarity index 97% rename from lib/ui/src/components/stories_panel/stories_tree/index.js rename to lib/components/src/explorer/tree.js index f09c9c8e41e8..3714eb33eebe 100644 --- a/lib/ui/src/components/stories_panel/stories_tree/index.js +++ b/lib/components/src/explorer/tree.js @@ -3,12 +3,12 @@ import PropTypes from 'prop-types'; import React from 'react'; import { polyfill } from 'react-lifecycles-compat'; -import deepEqual from 'deep-equal'; +import deepEqual from 'fast-deep-equal'; import styled from 'react-emotion'; import TreeHeader from './tree_header'; import treeNodeTypes from './tree_node_type'; -import treeDecorators from './tree_decorators'; +// import treeDecorators from './tree_decorators'; import treeStyle from './tree_style'; const namespaceSeparator = '@'; @@ -148,8 +148,8 @@ class Stories extends React.Component { data={data} onToggle={this.onToggle} animations={sidebarAnimations ? undefined : false} - decorators={treeDecorators} /> + {/* decorators={treeDecorators} */} ); } diff --git a/lib/ui/src/components/stories_panel/stories_tree/index.stories.js b/lib/components/src/explorer/tree.stories.js similarity index 100% rename from lib/ui/src/components/stories_panel/stories_tree/index.stories.js rename to lib/components/src/explorer/tree.stories.js diff --git a/lib/ui/src/components/stories_panel/stories_tree/tree_decorators.js b/lib/components/src/explorer/tree_decorators.js similarity index 97% rename from lib/ui/src/components/stories_panel/stories_tree/tree_decorators.js rename to lib/components/src/explorer/tree_decorators.js index a9356992dadf..7c2f3defd9c0 100644 --- a/lib/ui/src/components/stories_panel/stories_tree/tree_decorators.js +++ b/lib/components/src/explorer/tree_decorators.js @@ -1,9 +1,10 @@ -import { decorators } from '@ndelangen/react-treebeard'; -import { Icons } from '@storybook/components'; import React from 'react'; import PropTypes from 'prop-types'; + +import { decorators } from '@ndelangen/react-treebeard'; +import Icons from '../icons/index'; import { MenuLink } from '../../../containers/routed_link'; -import MenuItem from '../../menu_item'; +import MenuItem from '../menu_item'; import treeNodeTypes from './tree_node_type'; import { highlightNode } from './tree_decorators_utils'; diff --git a/lib/ui/src/components/stories_panel/stories_tree/tree_decorators_utils.js b/lib/components/src/explorer/tree_decorators_utils.js similarity index 100% rename from lib/ui/src/components/stories_panel/stories_tree/tree_decorators_utils.js rename to lib/components/src/explorer/tree_decorators_utils.js diff --git a/lib/ui/src/components/stories_panel/stories_tree/tree_header.js b/lib/components/src/explorer/tree_header.js similarity index 100% rename from lib/ui/src/components/stories_panel/stories_tree/tree_header.js rename to lib/components/src/explorer/tree_header.js diff --git a/lib/ui/src/components/stories_panel/stories_tree/tree_node_type.js b/lib/components/src/explorer/tree_node_type.js similarity index 100% rename from lib/ui/src/components/stories_panel/stories_tree/tree_node_type.js rename to lib/components/src/explorer/tree_node_type.js diff --git a/lib/ui/src/components/stories_panel/stories_tree/tree_style.js b/lib/components/src/explorer/tree_style.js similarity index 100% rename from lib/ui/src/components/stories_panel/stories_tree/tree_style.js rename to lib/components/src/explorer/tree_style.js diff --git a/lib/components/src/grid/grid.js b/lib/components/src/grid/grid.js new file mode 100644 index 000000000000..8fe0a4edf506 --- /dev/null +++ b/lib/components/src/grid/grid.js @@ -0,0 +1,55 @@ +import React from 'react'; +import styled from 'react-emotion'; + +const toNumber = input => (typeof input === 'number' ? input : Number(input)); + +const Container = styled('div')( + ({ theme, col, row = 1 }) => + col + ? { + display: 'inline-block', + '& > *': { + marginLeft: col * theme.layoutMargin, + }, + '& > *:first-child': { + marginLeft: 0, + }, + } + : { + '& > *': { + marginTop: row * theme.layoutMargin, + }, + '& > *:first-child': { + marginTop: 0, + }, + }, + ({ theme, outer, col, row }) => { + switch (true) { + case !!(outer && col): { + return { + marginLeft: outer * theme.layoutMargin, + marginRight: outer * theme.layoutMargin, + }; + } + case !!(outer && row): { + return { + marginTop: outer * theme.layoutMargin, + marginBottom: outer * theme.layoutMargin, + }; + } + default: { + return {}; + } + } + } +); + +export const Spaced = ({ col, row, outer, children }) => { + const outerAmount = toNumber(typeof outer === 'number' || !outer ? outer : col || row); + + return ( + + {children} + + ); +}; diff --git a/lib/components/src/grid/grid.stories.js b/lib/components/src/grid/grid.stories.js new file mode 100644 index 000000000000..21943ebb941d --- /dev/null +++ b/lib/components/src/grid/grid.stories.js @@ -0,0 +1,72 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import styled from 'react-emotion'; + +import { Spaced } from './grid'; + +const PlaceholderBlock = styled('div')(({ color }) => ({ + background: color || 'hotpink', + padding: 20, +})); +const PlaceholderInline = styled('span')(({ color }) => ({ + background: color || 'hotpink', + display: 'inline-block', + padding: 20, +})); + +storiesOf('Components|Grid', module) + .add('row', () => ( +
+ + + + + + + +
+ )) + .add('row outer', () => ( +
+ + + + + + + +
+ )) + .add('row multiply', () => ( +
+ + + + + + + +
+ )) + .add('col', () => ( +
+ + + + + + + +
+ )) + .add('col outer', () => ( +
+ + + + + + + +
+ )); diff --git a/lib/components/src/heading/heading.js b/lib/components/src/heading/heading.js new file mode 100644 index 000000000000..7e54773242ab --- /dev/null +++ b/lib/components/src/heading/heading.js @@ -0,0 +1,71 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; +import memoize from 'memoizee'; + +const modifiers = { + underline: {}, +}; + +const types = { + page: ({ theme }) => ({ + color: theme.mainTextColor, + lineHeight: '1em', + '& > sub': { + color: theme.dimmedTextColor, + }, + }), +}; + +const getElement = memoize((el, type, ...mods) => + styled(el)( + ({ theme }) => ({ + color: 'currentColor', + fontWeight: 'normal', + fontFamily: theme.mainTextFace, + margin: 0, + padding: 0, + lineHeight: '1.2em', + display: 'block', + + '& > sub': { + display: 'block', + paddingTop: 5, + lineHeight: '1.2em', + fontSize: 14, + }, + }), + p => types[type](p) || {}, + p => mods.reduce((acc, item) => ({ ...acc, ...(modifiers[item](p) || {}) }), {}) + ) +); + +const Container = styled('header')({}); + +const Heading = ({ type = 'page', el = 'h1', sub, mods = [], children }) => { + const Element = getElement(el, type, ...mods); + + return ( + + + {typeof children === 'string' ? {children} : children} + {sub ? {sub} : null} + + + ); +}; +Heading.propTypes = { + sub: PropTypes.node, + type: PropTypes.oneOf(Object.keys(types)), + mods: PropTypes.arrayOf(Object.keys(modifiers)), + el: PropTypes.oneOf(['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'span', 'div']), + children: PropTypes.node.isRequired, +}; +Heading.defaultProps = { + sub: undefined, + type: undefined, + mods: undefined, + el: undefined, +}; + +export { Heading as default }; diff --git a/lib/components/src/heading/heading.stories.js b/lib/components/src/heading/heading.stories.js new file mode 100644 index 000000000000..dd3dc4482c99 --- /dev/null +++ b/lib/components/src/heading/heading.stories.js @@ -0,0 +1,30 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import styled from 'react-emotion'; + +import Heading from './heading'; + +const Holder = styled('div')({ + margin: 10, + border: '1px dashed deepskyblue', + // overflow: 'hidden', +}); + +storiesOf('Components|Heading', module).add('types', () => ( +
+ + DEFAULT WITH ALL CAPS + + + THIS LONG DEFAULT WITH ALL CAPS & SUB + + + page type + + + + page type + + +
+)); diff --git a/lib/components/src/icons/component.js b/lib/components/src/icons/component.js new file mode 100644 index 000000000000..462d010e2cb3 --- /dev/null +++ b/lib/components/src/icons/component.js @@ -0,0 +1,15 @@ +import React from 'react'; +import styled from 'react-emotion'; + +const Svg = styled('svg')({ + display: 'block', +}); + +export default props => ( + +); diff --git a/lib/components/src/icons/gear.js b/lib/components/src/icons/gear.js new file mode 100644 index 000000000000..9cff45c02142 --- /dev/null +++ b/lib/components/src/icons/gear.js @@ -0,0 +1,15 @@ +import React from 'react'; +import styled from 'react-emotion'; + +const Svg = styled('svg')({ + display: 'block', +}); + +export default props => ( + +); diff --git a/lib/components/src/icons/index.js b/lib/components/src/icons/index.js index 3488ac9f2fb6..b2b784b5f7fc 100644 --- a/lib/components/src/icons/index.js +++ b/lib/components/src/icons/index.js @@ -1,7 +1,11 @@ import ChevronRight from './chevronRight'; +import Gear from './gear'; +import Component from './component'; const Icons = { + Gear, ChevronRight, + Component, }; export { Icons as default }; diff --git a/lib/components/src/index.js b/lib/components/src/index.js index d5df80bbcb75..2ce2051bffd4 100644 --- a/lib/components/src/index.js +++ b/lib/components/src/index.js @@ -17,5 +17,9 @@ export { ActionBar, ActionButton } from './panel_actionbar/panel_actionbar'; export { default as Placeholder } from './placeholder/placeholder'; export { default as AddonPanel } from './addon_panel/index'; export { default as Layout } from './layout/index'; +export { Root } from './root/root'; +export { Nav } from './nav/nav'; +export { Explorer } from './explorer/explorer'; + export { default as Header } from './header/header'; export { default as Icons } from './icons/index'; diff --git a/lib/components/src/logo/logo.js b/lib/components/src/logo/logo.js new file mode 100644 index 000000000000..546686cd30ff --- /dev/null +++ b/lib/components/src/logo/logo.js @@ -0,0 +1,60 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'react-emotion'; + +const Svg = styled('svg')({ + display: 'block', + maxWidth: '100%', +}); + +const silver = 'rgba(0,0,0,0.1)'; + +const Logo = ({ colored, ...rest }) => ( + + + + + + + + + + + + +); +Logo.propTypes = { + colored: PropTypes.bool, +}; +Logo.defaultProps = { + colored: false, +}; + +export { Logo as default }; diff --git a/lib/components/src/logo/logo.stories.js b/lib/components/src/logo/logo.stories.js new file mode 100644 index 000000000000..7c6f88926771 --- /dev/null +++ b/lib/components/src/logo/logo.stories.js @@ -0,0 +1,8 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; + +import Logo from './logo'; + +storiesOf('Components|Logo', module) + .add('gray', () => ) + .add('colored', () => ); diff --git a/lib/components/src/nav/nav.js b/lib/components/src/nav/nav.js new file mode 100644 index 000000000000..ddab18813c37 --- /dev/null +++ b/lib/components/src/nav/nav.js @@ -0,0 +1,144 @@ +import React from 'react'; +import styled from 'react-emotion'; + +import Heading from '../heading/heading'; + +const Container = styled('nav')({ + background: 'silver', + position: 'absolute', + zIndex: 1, + left: 0, + top: 0, + bottom: 0, + right: 0, + width: '100%', + height: '100%', + boxSizing: 'border-box', +}); +const Inner = styled('div')({ + position: 'absolute', + zIndex: 1, + left: 0, + top: 0, + bottom: 0, + right: 0, + width: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + alignItems: 'stretch', + padding: 10, + overflow: 'auto', + minHeight: '100%', + boxSizing: 'border-box', +}); + +const Head = styled('div')({}); +const Main = styled('div')({ + flex: 1, + marginTop: 20, +}); +const Foot = styled('div')({}); + +const Bar = styled('ul')({ + display: 'flex', + margin: 0, + padding: 0, + borderRadius: 5, + overflow: 'hidden', + background: 'gray', + justifyContent: 'space-between', + height: 40, + marginTop: 20, +}); +const BarLi = styled('li')(({ active }) => ({ + display: 'block', + margin: 0, + padding: 0, + flex: 1, + background: active ? 'rgba(255,255,255,0.2)' : 'transparent', + borderTop: '2px solid transparent', + borderBottom: active ? '2px solid hotpink' : '2px solid transparent', +})); +const BarLink = styled('a')({ + color: 'inherit', + display: 'block', + padding: 10, + textAlign: 'center', +}); +const BarItem = ({ href, children, active }) => ( + + {children} + +); + +const Notifications = styled('ul')({ + position: 'absolute', + display: 'block', + bottom: 0, + margin: 0, + padding: 0, + width: '100%', + zIndex: 2, +}); +const Notification = styled('li')({ + margin: 0, + padding: 0, + display: 'flex', + height: 50, + marginTop: 10, + alignItems: 'center', + justifyContent: 'center', + background: 'rgba(255, 0, 0, 0.4)', +}); +const NotificationSpacer = styled('div')({ + height: 60, +}); + +const A = styled('a')({ + color: 'inherit', + textDecoration: 'none', +}); + +const Nav = ({ + title, + url, + components, + notifications = [], + sections = [], + selected = sections.find(i => i.active), +}) => ( + + + + + {title} + + {sections.length ? ( + + {sections.map(({ id, name, active }) => ( + + {name} + + ))} + + ) : null} + +
{selected ? selected.render() : 'NOTHING'}
+ + {notifications.map(({ id }) => ( + + ))} + +
+ {notifications.length ? ( + + {notifications.map(({ id, content }) => ( + {content} + ))} + + ) : null} +
+); + +export { Nav }; diff --git a/lib/components/src/nav/nav.stories.js b/lib/components/src/nav/nav.stories.js new file mode 100644 index 000000000000..b26f2af474aa --- /dev/null +++ b/lib/components/src/nav/nav.stories.js @@ -0,0 +1,53 @@ +import React from 'react'; +import styled from 'react-emotion'; +import { storiesOf } from '@storybook/react'; + +import { Nav } from './nav'; + +const SearchBox = styled('input')({ + display: 'block', + padding: 10, + boxSizing: 'border-box', + borderRadius: 5, + height: 40, + width: '100%', + margin: 0, + border: '0 none', +}); + +const List = styled('div')({ + display: 'block', + marginTop: 10, +}); + +const sections = [ + { + name: 'components', + id: 'components', + render: () => ( +
+ + LIST GOES HERE +
+ ), + active: true, + }, + { name: 'settings', render: () =>
Settings NAV
, active: false }, +]; + +const notifications = [ + { + content: 'happy birthday', + id: 'birthday', + }, +]; + +storiesOf('Components|Nav', module).add('default', () => ( +