diff --git a/examples/cra-kitchen-sink/.storybook/config.js b/examples/cra-kitchen-sink/.storybook/config.js
index 7e57d3f7fef0..d1ed22531245 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,
-})
+ hierarchySeparator: '\\/|\\.|¯\\\\_\\(ツ\\)_\\/¯'
+});
setAddon(infoAddon);
diff --git a/examples/cra-kitchen-sink/src/stories/index.js b/examples/cra-kitchen-sink/src/stories/index.js
index fabd50e219a2..cf967cab7009 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('Cells¯\\_(ツ)_/¯Molecules.Atoms/simple', module)
+ .addDecorator(withKnobs)
+ .add('with text', () => )
+ .add('with some emoji', () => );
+
+storiesOf('Cells/Molecules/Atoms.more', module)
+ .add('with text2', () => )
+ .add('with some emoji2', () => );
+
+storiesOf('Cells/Molecules', module)
+ .add('with text', () => )
+ .add('with some emoji', () => );
+
+storiesOf('Cells.Molecules.Atoms', module)
+ .add('with text2', () => )
+ .add('with some emoji2', () => );
diff --git a/lib/ui/example/client/provider.js b/lib/ui/example/client/provider.js
index e95f76c55c00..50dc9fb3a095 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,
+ hierarchySeparator: '/'
});
// 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..8f78704bc789 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,
+ hierarchySeparator: '',
},
},
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..04252f4817ce 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 - hierarchySeparator is 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/);
+ 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 - hierarchySeparator 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 - hierarchySeparator is defined', () => {
+ const data = createHierarchy(
+ [
+ { kind: 'some.name.item1', stories: ['a1', 'a2'] },
+ { kind: 'another.space.20', stories: ['b1', 'b2'] },
+ ],
+ '\\.'
+ );
+
+ 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..3ee95809c249 100755
--- a/lib/ui/src/modules/ui/containers/left_panel.js
+++ b/lib/ui/src/modules/ui/containers/left_panel.js
@@ -2,16 +2,22 @@ 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, resolveStoryHierarchy } 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, hierarchySeparator } = uiOptions;
+ const filteredStores = filters.storyFilter(stories, storyFilter, selectedKind, sortStoriesByKind);
+
+ const storiesHierarchy = createHierarchy(filteredStores, hierarchySeparator);
+ const selectedHierarchy = resolveStoryHierarchy(selectedKind, hierarchySeparator);
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..8896e6ed983c 100755
--- a/lib/ui/src/modules/ui/containers/left_panel.test.js
+++ b/lib/ui/src/modules/ui/containers/left_panel.test.js
@@ -6,6 +6,7 @@ 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',
@@ -34,8 +35,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);
@@ -79,10 +83,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', () => {
@@ -122,10 +128,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..aa79785e9509
--- /dev/null
+++ b/lib/ui/src/modules/ui/libs/hierarchy.js
@@ -0,0 +1,78 @@
+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 resolveStoryHierarchy(storyName, hierarchySeparator) {
+ if (!hierarchySeparator) {
+ return [storyName];
+ }
+
+ return storyName.split(new RegExp(hierarchySeparator));
+}
+
+export function createHierarchy(stories, hierarchySeparator) {
+ const hierarchyRoot = {
+ namespaces: [],
+ current: '',
+ map: new Map(),
+ };
+
+ if (!stories) {
+ return hierarchyRoot;
+ }
+
+ const groupedStories = stories.map(story => {
+ const namespaces = resolveStoryHierarchy(story.kind, hierarchySeparator);
+
+ 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..bd770ddf93e4
--- /dev/null
+++ b/lib/ui/src/modules/ui/libs/hierarchy.test.js
@@ -0,0 +1,207 @@
+import { createHierarchy, isSelectedHierarchy, resolveStoryHierarchy } 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 hierarchySeparator 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 hierarchySeparator is defined', () => {
+ const stories = [
+ { kind: 'some.name.item1', stories: ['a1', 'a2'] },
+ { kind: 'another.space.20', stories: ['b1', 'b2'] },
+ ];
+
+ const result = createHierarchy(stories, '\\.');
+
+ 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();
+ });
+ });
+
+ describe('resolveStoryHierarchy', () => {
+ test('should return array with initial namespace when hierarchySeparator is undefined', () => {
+ const result = resolveStoryHierarchy('some.name.item1');
+
+ expect(result).toEqual(['some.name.item1']);
+ });
+
+ test('should return array with separated namespaces when hierarchySeparator is defined', () => {
+ const result = resolveStoryHierarchy('some/name.item1', '\\.|\\/');
+
+ expect(result).toEqual(['some', 'name', 'item1']);
+ });
+ });
+});