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', () =>
first table
) + .add('second', () =>
first table
); + +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']); + }); + }); +});