diff --git a/.gitignore b/.gitignore
index cbaf6162bbd4..b81f584793d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,4 +15,3 @@ yarn.lock
/**/LICENSE
docs/public
packs/*.tgz
-package-lock.json
diff --git a/addons/actions/README.md b/addons/actions/README.md
index bd2801310850..19117bc40d4e 100644
--- a/addons/actions/README.md
+++ b/addons/actions/README.md
@@ -25,9 +25,7 @@ npm i -D @storybook/addon-actions
Then, add following content to `.storybook/addons.js`
-```
-import '@storybook/addon-actions/register';
-```
+ import '@storybook/addon-actions/register';
Import the `action` function and use it to create actions handlers. When creating action handlers, provide a **name** to make it easier to identify.
diff --git a/addons/info/README.md b/addons/info/README.md
index e567e7240d69..1f5288b06842 100644
--- a/addons/info/README.md
+++ b/addons/info/README.md
@@ -55,24 +55,6 @@ storiesOf('Component')
> Have a look at [this example](example/story.js) stories to learn more about the `addWithInfo` API.
-
-To customize your defaults:
-
-```js
-// config.js
-import infoAddon, { setDefaults } from '@storybook/addon-info';
-
-// addon-info
-setDefaults({
- inline: true,
- maxPropsIntoLine: 1,
- maxPropObjectKeys: 10,
- maxPropArrayLength: 10,
- maxPropStringLength: 100,
-});
-setAddon(infoAddon);
-```
-
## The FAQ
**Components lose their names on static build**
diff --git a/addons/info/src/components/Story.js b/addons/info/src/components/Story.js
index 618d64d4f312..7e412085cc40 100644
--- a/addons/info/src/components/Story.js
+++ b/addons/info/src/components/Story.js
@@ -125,7 +125,11 @@ export default class Story extends React.Component {
_renderInline() {
return (
- {this._renderInlineHeader()}
+
+
+ {this._getInfoHeader()}
+
+
{this._renderStory()}
@@ -141,19 +145,6 @@ export default class Story extends React.Component {
);
}
- _renderInlineHeader() {
- const infoHeader = this._getInfoHeader();
-
- return (
- infoHeader &&
-
- );
- }
-
_renderOverlay() {
const linkStyle = {
...stylesheet.link.base,
diff --git a/app/react-native/src/server/index.js b/app/react-native/src/server/index.js
index 09e0a34284b9..ed284ab31b4d 100755
--- a/app/react-native/src/server/index.js
+++ b/app/react-native/src/server/index.js
@@ -12,12 +12,14 @@ export default class Server {
this.expressApp.use(storybook(options));
this.httpServer.on('request', this.expressApp);
this.wsServer = new ws.Server({ server: this.httpServer });
- this.wsServer.on('connection', (s, req) => this.handleWS(s, req));
+ this.wsServer.on('connection', s => this.handleWS(s));
}
- handleWS(socket, req) {
+ handleWS(socket) {
if (this.options.manualId) {
- const params = req.url ? querystring.parse(req.url.substr(1)) : {};
+ const params = socket.upgradeReq && socket.upgradeReq.url
+ ? querystring.parse(socket.upgradeReq.url.substr(1))
+ : {};
if (params.pairedId) {
socket.pairedId = params.pairedId; // eslint-disable-line
diff --git a/docs/pages/basics/faq/index.md b/docs/pages/basics/faq/index.md
index a5da8ca8a29c..87a3c3759fc7 100644
--- a/docs/pages/basics/faq/index.md
+++ b/docs/pages/basics/faq/index.md
@@ -17,5 +17,5 @@ npm test -- --coverage --collectCoverageFrom='["src/**/*.{js,jsx}","!src/**/stor
Next automatically defines `React` for all of your files via a babel plugin. You must define `React` for JSX to work. You can solve this either by:
-1. Adding `import React from 'react'` to your component files.
-1. Adding a `.babelrc` that includes [`babel-plugin-react-require`](https://www.npmjs.com/package/babel-plugin-react-require)
+1. Adding `import React from 'react'` to your component files.
+2. Adding a `.babelrc` that includes [`babel-plugin-react-require`](https://www.npmjs.com/package/babel-plugin-react-require)
diff --git a/examples/cra-kitchen-sink/.storybook/config.js b/examples/cra-kitchen-sink/.storybook/config.js
index 7e57d3f7fef0..cd396ec63556 100644
--- a/examples/cra-kitchen-sink/.storybook/config.js
+++ b/examples/cra-kitchen-sink/.storybook/config.js
@@ -11,7 +11,8 @@ setOptions({
showSearchBox: false,
downPanelInRight: true,
sortStoriesByKind: false,
-})
+ resolveStoryHierarchy: (storyName) => storyName.split('.'),
+});
setAddon(infoAddon);
diff --git a/examples/cra-kitchen-sink/src/stories/index.js b/examples/cra-kitchen-sink/src/stories/index.js
index fabd50e219a2..b36b36fbc2f8 100644
--- a/examples/cra-kitchen-sink/src/stories/index.js
+++ b/examples/cra-kitchen-sink/src/stories/index.js
@@ -153,3 +153,43 @@ storiesOf('WithEvents', module)
)
.add('Logger', () =>
);
+
+storiesOf('component.base.Link')
+ .addDecorator(withKnobs)
+ .add('first', () =>
{text('firstLink', 'first link')})
+ .add('second', () =>
{text('secondLink', 'second link')});
+
+storiesOf('component.base.Span')
+ .add('first', () =>
first span)
+ .add('second', () =>
second span);
+
+storiesOf('component.common.Div')
+ .add('first', () =>
first div
)
+ .add('second', () =>
second div
);
+
+storiesOf('component.common.Table')
+ .add('first', () =>
)
+ .add('second', () =>
);
+
+storiesOf('component.Button')
+ .add('first', () =>
)
+ .add('second', () =>
);
+
+// Atomic
+
+storiesOf('Atoms.Molecules.Cells.simple', module)
+ .addDecorator(withKnobs)
+ .add('with text', () =>
)
+ .add('with some emoji', () =>
);
+
+storiesOf('Atoms.Molecules.Cells.more', module)
+ .add('with text2', () =>
)
+ .add('with some emoji2', () =>
);
+
+storiesOf('Atoms.Molecules', module)
+ .add('with text', () =>
)
+ .add('with some emoji', () =>
);
+
+storiesOf('Atoms.Molecules.Cells', module)
+ .add('with text2', () =>
)
+ .add('with some emoji2', () =>
);
diff --git a/lib/cli/generators/REACT_NATIVE/index.js b/lib/cli/generators/REACT_NATIVE/index.js
index e60007786675..f0318e212508 100644
--- a/lib/cli/generators/REACT_NATIVE/index.js
+++ b/lib/cli/generators/REACT_NATIVE/index.js
@@ -31,8 +31,7 @@ module.exports = latestVersion('@storybook/react-native').then(version => {
packageJson.devDependencies['@storybook/react-native'] = `^${version}`;
if (!packageJson.dependencies['react-dom'] && !packageJson.devDependencies['react-dom']) {
- const reactVersion = packageJson.dependencies.react;
- packageJson.devDependencies['react-dom'] = reactVersion;
+ packageJson.devDependencies['react-dom'] = '^15.5.4';
}
packageJson.scripts = packageJson.scripts || {};
diff --git a/lib/cli/generators/REACT_NATIVE_SCRIPTS/index.js b/lib/cli/generators/REACT_NATIVE_SCRIPTS/index.js
index b163469d2082..fe05fbdbf474 100644
--- a/lib/cli/generators/REACT_NATIVE_SCRIPTS/index.js
+++ b/lib/cli/generators/REACT_NATIVE_SCRIPTS/index.js
@@ -15,8 +15,7 @@ module.exports = latestVersion('@storybook/react-native').then(version => {
packageJson.devDependencies['@storybook/react-native'] = `^${version}`;
if (!packageJson.dependencies['react-dom'] && !packageJson.devDependencies['react-dom']) {
- const reactVersion = packageJson.dependencies.react;
- packageJson.devDependencies['react-dom'] = reactVersion;
+ packageJson.devDependencies['react-dom'] = '^15.5.4';
}
packageJson.scripts = packageJson.scripts || {};
diff --git a/lib/ui/example/client/provider.js b/lib/ui/example/client/provider.js
index e95f76c55c00..541db9d9c5c9 100644
--- a/lib/ui/example/client/provider.js
+++ b/lib/ui/example/client/provider.js
@@ -78,25 +78,62 @@ export default class ReactProvider extends Provider {
this.api = api;
this.api.setOptions({
name: 'REACT-STORYBOOK',
+ sortStoriesByKind: true,
+ resolveStoryHierarchy: storyName => storyName.split('/')
});
// set stories
- this.api.setStories([
+ this.api.setStories(this.createStories());
+
+ // listen to the story change and update the preview.
+ this.api.onStory((kind, story) => {
+ this.globalState.emit('change', kind, story);
+ });
+ }
+
+ createStories() {
+ return [
{
- kind: 'Component 1',
+ kind: 'some/name/Component 1',
stories: ['State 1', 'State 2'],
},
-
{
- kind: 'Component 2',
+ kind: 'some/name/Component 2',
stories: ['State a', 'State b'],
},
- ]);
-
- // listen to the story change and update the preview.
- this.api.onStory((kind, story) => {
- this.globalState.emit('change', kind, story);
- });
+ {
+ kind: 'some/name2/Component 3',
+ stories: ['State a', 'State b'],
+ },
+ {
+ kind: 'some/name2',
+ stories: ['State a', 'State b'],
+ },
+ {
+ kind: 'some/name2/Component 4',
+ stories: ['State a', 'State b'],
+ },
+ {
+ kind: 'another/name3/Component 5',
+ stories: ['State a', 'State b'],
+ },
+ {
+ kind: 'another/name3/Component 6',
+ stories: ['State a', 'State b'],
+ },
+ {
+ kind: 'Bla 1',
+ stories: ['State 1', 'State 2'],
+ },
+ {
+ kind: 'Bla 2',
+ stories: ['State 1', 'State 2'],
+ },
+ {
+ kind: 'anotherComponent in the middle',
+ stories: ['State a', 'State b'],
+ },
+ ]
}
_handlePreviewEvents() {
diff --git a/lib/ui/src/modules/api/index.js b/lib/ui/src/modules/api/index.js
index 704def8cde85..6141f8cdeeec 100755
--- a/lib/ui/src/modules/api/index.js
+++ b/lib/ui/src/modules/api/index.js
@@ -8,6 +8,7 @@ export default {
name: 'STORYBOOK',
url: 'https://github.com/storybooks/storybook',
sortStoriesByKind: false,
+ resolveStoryHierarchy: storyName => [storyName],
},
},
load({ clientStore, provider }, _actions) {
diff --git a/lib/ui/src/modules/ui/components/left_panel/index.js b/lib/ui/src/modules/ui/components/left_panel/index.js
index b38381643b21..e2f87da845d6 100755
--- a/lib/ui/src/modules/ui/components/left_panel/index.js
+++ b/lib/ui/src/modules/ui/components/left_panel/index.js
@@ -15,7 +15,13 @@ const mainStyle = {
padding: '10px 0 10px 10px',
};
-const storyProps = ['stories', 'selectedKind', 'selectedStory', 'onSelectStory'];
+const storyProps = [
+ 'storiesHierarchy',
+ 'selectedKind',
+ 'selectedHierarchy',
+ 'selectedStory',
+ 'onSelectStory',
+];
const LeftPanel = props =>
@@ -26,12 +32,12 @@ const LeftPanel = props =>
onChange={text => props.onStoryFilter(text)}
/>
- {props.stories ? : null}
+ {props.storiesHierarchy ? : null}
;
LeftPanel.defaultProps = {
- stories: null,
+ storiesHierarchy: null,
storyFilter: null,
onStoryFilter: () => {},
openShortcutsHelp: null,
@@ -40,7 +46,11 @@ LeftPanel.defaultProps = {
};
LeftPanel.propTypes = {
- stories: PropTypes.arrayOf(PropTypes.object),
+ storiesHierarchy: PropTypes.shape({
+ namespaces: PropTypes.arrayOf(PropTypes.string),
+ current: PropTypes.string,
+ map: PropTypes.object,
+ }),
storyFilter: PropTypes.string,
onStoryFilter: PropTypes.func,
diff --git a/lib/ui/src/modules/ui/components/left_panel/index.test.js b/lib/ui/src/modules/ui/components/left_panel/index.test.js
index 7fc17e522cd9..b52d35bf383e 100755
--- a/lib/ui/src/modules/ui/components/left_panel/index.test.js
+++ b/lib/ui/src/modules/ui/components/left_panel/index.test.js
@@ -4,6 +4,7 @@ import LeftPanel from './index';
import Header from './header';
import TextFilter from './text_filter';
import Stories from './stories';
+import { createHierarchy } from '../../libs/hierarchy';
describe('manager.ui.components.left_panel.index', () => {
test('should render Header and TextFilter by default', () => {
@@ -22,17 +23,22 @@ describe('manager.ui.components.left_panel.index', () => {
expect(wrap.find(Stories)).toBeEmpty();
});
- test('should render stories only if stories prop exists', () => {
+ test('should render stories only if storiesHierarchy prop exists', () => {
const selectedKind = 'kk';
const selectedStory = 'bb';
- const stories = [{ kind: 'kk', stories: ['bb'] }];
+ const storiesHierarchy = createHierarchy([{ kind: 'kk', stories: ['bb'] }]);
const wrap = shallow(
-
+
);
const header = wrap.find(Stories).first();
expect(header.props()).toMatchObject({
- stories,
+ storiesHierarchy,
selectedKind,
selectedStory,
});
diff --git a/lib/ui/src/modules/ui/components/left_panel/stories.js b/lib/ui/src/modules/ui/components/left_panel/stories.js
index aba168aded2c..2e91653faf6a 100755
--- a/lib/ui/src/modules/ui/components/left_panel/stories.js
+++ b/lib/ui/src/modules/ui/components/left_panel/stories.js
@@ -1,6 +1,32 @@
import PropTypes from 'prop-types';
import React from 'react';
import { baseFonts } from '../theme';
+import { isSelectedHierarchy } from '../../libs/hierarchy';
+
+const hierarchySeparatorColor = '#CCC';
+const hierarchySeparatorOffset = '15px';
+
+const baseListItemStyle = {
+ display: 'block',
+ cursor: 'pointer',
+};
+
+const kindStyle = {
+ ...baseListItemStyle,
+ fontSize: 15,
+ padding: '5px 0px',
+};
+
+const nameSpaceStyle = {
+ ...kindStyle,
+ color: '#8aa4d1',
+};
+
+const storyStyle = {
+ ...baseListItemStyle,
+ fontSize: 13,
+ padding: '5px 0px',
+};
const listStyle = {
...baseFonts,
@@ -9,19 +35,24 @@ const listStyle = {
const listStyleType = {
listStyleType: 'none',
paddingLeft: 0,
+ margin: 0,
};
-const kindStyle = {
- fontSize: 15,
- padding: '10px 0px',
- cursor: 'pointer',
- borderBottom: '1px solid #EEE',
+const nestedListStyle = {
+ ...listStyleType,
+ paddingLeft: hierarchySeparatorOffset,
+ borderLeft: `1px solid ${hierarchySeparatorColor}`,
};
-const storyStyle = {
- fontSize: 13,
- padding: '8px 0px 8px 10px',
- cursor: 'pointer',
+const separatorStyle = {
+ margin: 0,
+ padding: 0,
+ width: '5px',
+ position: 'absolute',
+ left: `-${hierarchySeparatorOffset}`,
+ top: '50%',
+ border: 'none',
+ borderTop: `1px solid ${hierarchySeparatorColor}`,
};
class Stories extends React.Component {
@@ -41,9 +72,28 @@ class Stories extends React.Component {
if (onSelectStory) onSelectStory(selectedKind, story);
}
+ renderMenuItem(item, style, onClick, displayName) {
+ return (
+
+ {displayName}
+
+ );
+ }
+
+ renderMenuListItem(item, style, onClick, displayName) {
+ const listItemStyle = { position: 'relative' };
+
+ return (
+
+
+ {this.renderMenuItem(item, style, onClick, displayName)}
+
+ );
+ }
+
renderStory(story) {
const { selectedStory } = this.props;
- const style = { display: 'block', ...storyStyle };
+ const style = { ...storyStyle };
const props = {
onClick: this.fireOnStory.bind(this, story),
};
@@ -52,75 +102,92 @@ class Stories extends React.Component {
style.fontWeight = 'bold';
}
- return (
-
-
- {story}
-
-
- );
+ return this.renderMenuListItem(story, style, props.onClick, story);
}
- renderKind({ kind, stories }) {
+ renderKind({ kind, stories, name }) {
const { selectedKind } = this.props;
- const style = { display: 'block', ...kindStyle };
+ const storyKindStyle = { ...kindStyle };
const onClick = this.fireOnKind.bind(this, kind);
+ const displayName = name || kind;
+
+ const children = [this.renderMenuListItem(kind, storyKindStyle, onClick, displayName)];
if (kind === selectedKind) {
- style.fontWeight = 'bold';
- return (
-
-
- {kind}
-
-
-
- {stories.map(this.renderStory)}
-
-
+ storyKindStyle.fontWeight = 'bold';
+
+ children.push(
+
+
+ {stories.map(this.renderStory)}
+
);
}
- return (
-
-
- {kind}
-
-
- );
+ return children;
+ }
+
+ renderHierarchy({ map }) {
+ const { selectedHierarchy } = this.props;
+ const children = [];
+
+ map.forEach((childItems, key) => {
+ childItems.forEach(value => {
+ const style = { ...nameSpaceStyle };
+ const onClick = this.fireOnKind.bind(this, value.firstKind);
+ const isSelected = isSelectedHierarchy(value.namespaces, selectedHierarchy);
+
+ if (isSelected) {
+ style.fontWeight = 'bold';
+ }
+
+ if (value.isNamespace) {
+ children.push(
+
+ {this.renderMenuListItem(value.current, style, onClick, key)}
+ {isSelected &&
+ -
+ {this.renderHierarchy(value)}
+
}
+
+ );
+ } else {
+ children.push(
+
+ {this.renderKind(value)}
+
+ );
+ }
+ });
+ });
+
+ return children;
}
render() {
- const { stories } = this.props;
+ const { storiesHierarchy } = this.props;
+
return (
-
- {stories.map(this.renderKind)}
-
+ {this.renderHierarchy(storiesHierarchy)}
);
}
}
Stories.defaultProps = {
- stories: [],
onSelectStory: null,
+ storiesHierarchy: null,
};
Stories.propTypes = {
- stories: PropTypes.arrayOf(
- PropTypes.shape({
- kind: PropTypes.string,
- stories: PropTypes.array,
- })
- ),
+ storiesHierarchy: PropTypes.shape({
+ namespaces: PropTypes.arrayOf(PropTypes.string),
+ current: PropTypes.string,
+ map: PropTypes.object,
+ }),
+ selectedHierarchy: PropTypes.arrayOf(PropTypes.string).isRequired,
selectedKind: PropTypes.string.isRequired,
selectedStory: PropTypes.string.isRequired,
onSelectStory: PropTypes.func,
diff --git a/lib/ui/src/modules/ui/components/left_panel/stories.test.js b/lib/ui/src/modules/ui/components/left_panel/stories.test.js
index 841b06b456e6..7a810f011ddf 100755
--- a/lib/ui/src/modules/ui/components/left_panel/stories.test.js
+++ b/lib/ui/src/modules/ui/components/left_panel/stories.test.js
@@ -1,12 +1,20 @@
import { shallow } from 'enzyme';
import React from 'react';
import Stories from './stories';
+import { createHierarchy } from '../../libs/hierarchy';
describe('manager.ui.components.left_panel.stories', () => {
describe('render', () => {
test('should render stories - empty', () => {
- const data = [];
- const wrap = shallow(
);
+ const data = createHierarchy([]);
+ const wrap = shallow(
+
+ );
const list = wrap.find('div').first().children('div').last();
@@ -14,22 +22,96 @@ describe('manager.ui.components.left_panel.stories', () => {
});
test('should render stories', () => {
- const data = [{ kind: 'a', stories: ['a1', 'a2'] }, { kind: '20', stories: ['b1', 'b2'] }];
- const wrap = shallow(
);
+ const data = createHierarchy([
+ { kind: 'a', stories: ['a1', 'a2'] },
+ { kind: '20', stories: ['b1', 'b2'] },
+ ]);
+ const wrap = shallow(
+
+ );
+
+ const output = wrap.html();
+
+ expect(output).toMatch(/20/);
+ expect(output).toMatch(/b2/);
+ });
+
+ test('should render stories with hierarchy - resolveStoryHierarchy is defined', () => {
+ const data = createHierarchy(
+ [
+ { kind: 'some.name.item1', stories: ['a1', 'a2'] },
+ { kind: 'another.space.20', stories: ['b1', 'b2'] },
+ ],
+ name => name.split('.')
+ );
+ const wrap = shallow(
+
+ );
const output = wrap.html();
+ expect(output).toMatch(/some/);
+ expect(output).not.toMatch(/name/);
+ expect(output).not.toMatch(/item1/);
+ expect(output).not.toMatch(/a1/);
+ expect(output).not.toMatch(/a2/);
+ expect(output).toMatch(/another/);
+ expect(output).toMatch(/space/);
expect(output).toMatch(/20/);
+ expect(output).toMatch(/b1/);
+ expect(output).toMatch(/b2/);
+ });
+
+ test('should render stories without hierarchy - resolveStoryHierarchy is not defined', () => {
+ const data = createHierarchy([
+ { kind: 'some.name.item1', stories: ['a1', 'a2'] },
+ { kind: 'another.space.20', stories: ['b1', 'b2'] },
+ ]);
+ 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/);
});
});
describe('events', () => {
test('should call the onSelectStory prop when a kind is clicked', () => {
- const data = [{ kind: 'a', stories: ['a1', 'a2'] }, { kind: 'b', stories: ['b1', 'b2'] }];
+ const data = createHierarchy([
+ { kind: 'a', stories: ['a1', 'a2'] },
+ { kind: 'b', stories: ['b1', 'b2'] },
+ ]);
const onSelectStory = jest.fn();
const wrap = shallow(
-
+
);
const kind = wrap.find('a').filterWhere(el => el.text() === 'a').last();
@@ -39,10 +121,19 @@ describe('manager.ui.components.left_panel.stories', () => {
});
test('should call the onSelectStory prop when a story is clicked', () => {
- const data = [{ kind: 'a', stories: ['a1', 'a2'] }, { kind: 'b', stories: ['b1', 'b2'] }];
+ const data = createHierarchy([
+ { kind: 'a', stories: ['a1', 'a2'] },
+ { kind: 'b', stories: ['b1', 'b2'] },
+ ]);
const onSelectStory = jest.fn();
const wrap = shallow(
-
+
);
const kind = wrap.find('a').filterWhere(el => el.text() === 'b1').last();
@@ -50,5 +141,31 @@ describe('manager.ui.components.left_panel.stories', () => {
expect(onSelectStory).toHaveBeenCalledWith('b', 'b1');
});
+
+ test('should call the onSelectStory prop when a namespace is clicked - resolveStoryHierarchy is defined', () => {
+ const data = createHierarchy(
+ [
+ { kind: 'some.name.item1', stories: ['a1', 'a2'] },
+ { kind: 'another.space.20', stories: ['b1', 'b2'] },
+ ],
+ name => name.split('.')
+ );
+
+ const onSelectStory = jest.fn();
+ const wrap = shallow(
+
+ );
+
+ const kind = wrap.find('a').filterWhere(el => el.text() === 'another').last();
+ kind.simulate('click');
+
+ expect(onSelectStory).toHaveBeenCalledWith('another.space.20', null);
+ });
});
});
diff --git a/lib/ui/src/modules/ui/containers/left_panel.js b/lib/ui/src/modules/ui/containers/left_panel.js
index 8191ba886e11..5df4ac0c1d1a 100755
--- a/lib/ui/src/modules/ui/containers/left_panel.js
+++ b/lib/ui/src/modules/ui/containers/left_panel.js
@@ -2,16 +2,24 @@ import LeftPanel from '../components/left_panel';
import * as filters from '../libs/filters';
import genPoddaLoader from '../libs/gen_podda_loader';
import compose from '../../../compose';
+import { createHierarchy } from '../libs/hierarchy';
export const mapper = (state, props, { actions }) => {
const actionMap = actions();
const { stories, selectedKind, selectedStory, uiOptions, storyFilter } = state;
- const { name, url, sortStoriesByKind } = uiOptions;
+ const { name, url, sortStoriesByKind, resolveStoryHierarchy } = uiOptions;
+ const filteredStores = filters.storyFilter(stories, storyFilter, selectedKind, sortStoriesByKind);
+
+ const storiesHierarchy = createHierarchy(filteredStores, resolveStoryHierarchy);
+ const selectedHierarchy = resolveStoryHierarchy
+ ? resolveStoryHierarchy(selectedKind)
+ : [selectedKind];
const data = {
- stories: filters.storyFilter(stories, storyFilter, selectedKind, sortStoriesByKind),
+ storiesHierarchy,
selectedKind,
selectedStory,
+ selectedHierarchy,
onSelectStory: actionMap.api.selectStory,
storyFilter,
diff --git a/lib/ui/src/modules/ui/containers/left_panel.test.js b/lib/ui/src/modules/ui/containers/left_panel.test.js
index e0476046d13d..f5b4778ccc3e 100755
--- a/lib/ui/src/modules/ui/containers/left_panel.test.js
+++ b/lib/ui/src/modules/ui/containers/left_panel.test.js
@@ -6,9 +6,11 @@ describe('manager.ui.containers.left_panel', () => {
const stories = [{ kind: 'sk', stories: ['dd'] }];
const selectedKind = 'sk';
const selectedStory = 'dd';
+ const selectedHierarchy = ['sk'];
const uiOptions = {
name: 'foo',
url: 'bar',
+ resolveStoryHierarchy: name => [name],
};
const selectStory = () => 'selectStory';
const toggleShortcutsHelp = () => 'toggleShortcutsHelp';
@@ -34,8 +36,11 @@ describe('manager.ui.containers.left_panel', () => {
};
const result = mapper(state, props, env);
- expect(result.stories).toEqual(stories);
+ expect(result.storiesHierarchy.map).toEqual(
+ new Map([['sk', [{ ...stories[0], name: 'sk', namespaces: ['sk'] }]]])
+ );
expect(result.selectedKind).toBe(selectedKind);
+ expect(result.selectedHierarchy).toEqual(selectedHierarchy);
expect(result.selectedStory).toBe(selectedStory);
expect(result.storyFilter).toBe(null);
expect(result.onSelectStory).toBe(selectStory);
@@ -54,6 +59,7 @@ describe('manager.ui.containers.left_panel', () => {
const uiOptions = {
name: 'foo',
url: 'bar',
+ resolveStoryHierarchy: name => [name],
};
const selectStory = () => 'selectStory';
const toggleShortcutsHelp = () => 'toggleShortcutsHelp';
@@ -79,10 +85,12 @@ describe('manager.ui.containers.left_panel', () => {
};
const result = mapper(state, props, env);
- expect(result.stories).toEqual([
- stories[0], // selected kind is always there. That's why this is here.
- stories[1],
- ]);
+ expect(result.storiesHierarchy.map).toEqual(
+ new Map([
+ ['pk', [{ ...stories[0], name: 'pk', namespaces: ['pk'] }]], // selected kind is always there. That's why this is here.
+ ['ss', [{ ...stories[1], name: 'ss', namespaces: ['ss'] }]],
+ ])
+ );
});
test('should filter and sort stories according to the given filter', () => {
@@ -97,6 +105,7 @@ describe('manager.ui.containers.left_panel', () => {
name: 'foo',
url: 'bar',
sortStoriesByKind: true,
+ resolveStoryHierarchy: name => [name],
};
const selectStory = () => 'selectStory';
const toggleShortcutsHelp = () => 'toggleShortcutsHelp';
@@ -122,10 +131,12 @@ describe('manager.ui.containers.left_panel', () => {
};
const result = mapper(state, props, env);
- expect(result.stories).toEqual([
- stories[1], // selected kind is always there. That's why this is here.
- stories[0],
- ]);
+ expect(result.storiesHierarchy.map).toEqual(
+ new Map([
+ ['pk', [{ ...stories[1], name: 'pk', namespaces: ['pk'] }]], // selected kind is always there. That's why this is here.
+ ['ss', [{ ...stories[0], name: 'ss', namespaces: ['ss'] }]],
+ ])
+ );
});
});
});
diff --git a/lib/ui/src/modules/ui/libs/hierarchy.js b/lib/ui/src/modules/ui/libs/hierarchy.js
new file mode 100644
index 000000000000..3376a977bb21
--- /dev/null
+++ b/lib/ui/src/modules/ui/libs/hierarchy.js
@@ -0,0 +1,70 @@
+function fillHierarchy(namespaces, hierarchy, story) {
+ if (namespaces.length === 1) {
+ const namespace = namespaces[0];
+ const childItems = hierarchy.map.get(namespace) || [];
+
+ childItems.push(story);
+ hierarchy.map.set(namespace, childItems);
+ return;
+ }
+
+ const namespace = namespaces[0];
+ const childItems = hierarchy.map.get(namespace) || [];
+ let childHierarchy = childItems.find(item => item.isNamespace);
+
+ if (!childHierarchy) {
+ childHierarchy = {
+ isNamespace: true,
+ current: namespace,
+ namespaces: [...hierarchy.namespaces, namespace],
+ firstKind: story.kind,
+ map: new Map(),
+ };
+
+ childItems.push(childHierarchy);
+ hierarchy.map.set(namespace, childItems);
+ }
+
+ fillHierarchy(namespaces.slice(1), childHierarchy, story);
+}
+
+export function createHierarchy(stories, resolveNamespace) {
+ const hierarchyRoot = {
+ namespaces: [],
+ current: '',
+ map: new Map(),
+ };
+
+ if (!stories) {
+ return hierarchyRoot;
+ }
+
+ const groupedStories = stories.map(story => {
+ const namespaces = resolveNamespace ? resolveNamespace(story.kind) : [story.kind];
+
+ return {
+ namespaces,
+ name: namespaces[namespaces.length - 1],
+ ...story,
+ };
+ });
+
+ groupedStories.forEach(story => fillHierarchy(story.namespaces, hierarchyRoot, story));
+
+ return hierarchyRoot;
+}
+
+export function isSelectedHierarchy(namespaces, selectedHierarchy) {
+ if (!namespaces || !selectedHierarchy) {
+ return false;
+ }
+
+ if (namespaces.length > selectedHierarchy.length) {
+ return false;
+ }
+
+ return namespaces.reduce(
+ (isSelected, namespace, index) => isSelected && namespace === selectedHierarchy[index],
+ true
+ );
+}
diff --git a/lib/ui/src/modules/ui/libs/hierarchy.test.js b/lib/ui/src/modules/ui/libs/hierarchy.test.js
new file mode 100644
index 000000000000..b0fac255e041
--- /dev/null
+++ b/lib/ui/src/modules/ui/libs/hierarchy.test.js
@@ -0,0 +1,193 @@
+import { createHierarchy, isSelectedHierarchy } from './hierarchy';
+
+describe('manager.ui.libs.hierarchy', () => {
+ describe('createHierarchy', () => {
+ test('should return root hierarchy node if stories are undefined', () => {
+ const result = createHierarchy();
+
+ expect(result).toEqual({
+ namespaces: [],
+ current: '',
+ map: new Map(),
+ });
+ });
+
+ test('should return root hierarchy node if stories are empty', () => {
+ const result = createHierarchy([]);
+
+ expect(result).toEqual({
+ namespaces: [],
+ current: '',
+ map: new Map(),
+ });
+ });
+
+ test('should return flat hierarchy if resolve function is undefined', () => {
+ const stories = [
+ { kind: 'some.name.item1', stories: ['a1', 'a2'] },
+ { kind: 'another.space.20', stories: ['b1', 'b2'] },
+ ];
+
+ const result = createHierarchy(stories);
+
+ const expected = [
+ [
+ 'some.name.item1',
+ [
+ {
+ kind: 'some.name.item1',
+ name: 'some.name.item1',
+ namespaces: ['some.name.item1'],
+ stories: ['a1', 'a2'],
+ },
+ ],
+ ],
+ [
+ 'another.space.20',
+ [
+ {
+ kind: 'another.space.20',
+ name: 'another.space.20',
+ namespaces: ['another.space.20'],
+ stories: ['b1', 'b2'],
+ },
+ ],
+ ],
+ ];
+
+ expect(result.map).toEqual(new Map(expected));
+ });
+
+ test('should return hierarchy if resolve function is defined', () => {
+ const stories = [
+ { kind: 'some.name.item1', stories: ['a1', 'a2'] },
+ { kind: 'another.space.20', stories: ['b1', 'b2'] },
+ ];
+
+ const result = createHierarchy(stories, name => name.split('.'));
+
+ const expected = new Map([
+ [
+ 'some',
+ [
+ {
+ current: 'some',
+ firstKind: 'some.name.item1',
+ isNamespace: true,
+ namespaces: ['some'],
+ map: new Map([
+ [
+ 'name',
+ [
+ {
+ current: 'name',
+ firstKind: 'some.name.item1',
+ isNamespace: true,
+ namespaces: ['some', 'name'],
+ map: new Map([
+ [
+ 'item1',
+ [
+ {
+ kind: 'some.name.item1',
+ name: 'item1',
+ namespaces: ['some', 'name', 'item1'],
+ stories: ['a1', 'a2'],
+ },
+ ],
+ ],
+ ]),
+ },
+ ],
+ ],
+ ]),
+ },
+ ],
+ ],
+ [
+ 'another',
+ [
+ {
+ current: 'another',
+ firstKind: 'another.space.20',
+ isNamespace: true,
+ namespaces: ['another'],
+ map: new Map([
+ [
+ 'space',
+ [
+ {
+ current: 'space',
+ firstKind: 'another.space.20',
+ isNamespace: true,
+ namespaces: ['another', 'space'],
+ map: new Map([
+ [
+ '20',
+ [
+ {
+ kind: 'another.space.20',
+ name: '20',
+ namespaces: ['another', 'space', '20'],
+ stories: ['b1', 'b2'],
+ },
+ ],
+ ],
+ ]),
+ },
+ ],
+ ],
+ ]),
+ },
+ ],
+ ],
+ ]);
+
+ expect(result.map).toEqual(expected);
+ });
+ });
+
+ describe('isSelectedHierarchy', () => {
+ test('no parameters', () => {
+ const result = isSelectedHierarchy();
+
+ expect(result).toBeFalsy();
+ });
+
+ test('namespaces array is bigger then selectedHierarchy array', () => {
+ const namespaces = ['some', 'namespace', 'here', 'it', 'is'];
+ const selectedHierarchy = ['some', 'namespace'];
+
+ const result = isSelectedHierarchy(namespaces, selectedHierarchy);
+
+ expect(result).toBeFalsy();
+ });
+
+ test('namespaces array is not matching selectedHierarchy array', () => {
+ const namespaces = ['some', 'namespace'];
+ const selectedHierarchy = ['some', 'namespace2'];
+
+ const result = isSelectedHierarchy(namespaces, selectedHierarchy);
+
+ expect(result).toBeFalsy();
+ });
+
+ test('namespaces array is matching selectedHierarchy array', () => {
+ const namespaces = ['some', 'namespace'];
+ const selectedHierarchy = ['some', 'namespace'];
+
+ const result = isSelectedHierarchy(namespaces, selectedHierarchy);
+
+ expect(result).toBeTruthy();
+ });
+
+ test('namespaces array is matching selectedHierarchy array when selectedHierarchy is bigger', () => {
+ const namespaces = ['some', 'namespace'];
+ const selectedHierarchy = ['some', 'namespace', 'here', 'it', 'is'];
+
+ const result = isSelectedHierarchy(namespaces, selectedHierarchy);
+
+ expect(result).toBeTruthy();
+ });
+ });
+});
diff --git a/package.json b/package.json
index 076b0be0e40c..a3ef97b387f3 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,7 @@
"jest": "^20.0.4",
"jest-enzyme": "^3.2.0",
"lerna": "2.0.0-rc.5",
- "lint-staged": "^4.0.0",
+ "lint-staged": "^3.5.1",
"markdown-it-anchor": "^4.0.0",
"markdownlint-cli": "^0.3.1",
"nodemon": "^1.11.0",