From 8abcfb7f9dad32a704767b563f99e56b1807c516 Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Fri, 18 Aug 2017 16:23:14 +0200 Subject: [PATCH] Revert "Improve search and highlighting" --- lib/ui/package.json | 2 +- .../ui/components/left_panel/index.test.js | 2 +- .../left_panel/stories_tree/index.js | 32 ++++- .../left_panel/stories_tree/index.test.js | 49 +++---- .../stories_tree/tree_decorators.js | 63 ++++++--- .../stories_tree/tree_decorators_utils.js | 59 -------- .../tree_decorators_utils.test.js | 32 ----- .../src/modules/ui/containers/left_panel.js | 19 +-- .../modules/ui/containers/left_panel.test.js | 68 +-------- lib/ui/src/modules/ui/libs/filters.js | 130 ++++-------------- lib/ui/src/modules/ui/libs/filters.test.js | 90 +++++------- lib/ui/src/modules/ui/libs/hierarchy.js | 91 ++++-------- lib/ui/src/modules/ui/libs/hierarchy.test.js | 86 ++---------- 13 files changed, 197 insertions(+), 526 deletions(-) delete mode 100644 lib/ui/src/modules/ui/components/left_panel/stories_tree/tree_decorators_utils.js delete mode 100644 lib/ui/src/modules/ui/components/left_panel/stories_tree/tree_decorators_utils.test.js diff --git a/lib/ui/package.json b/lib/ui/package.json index 9c82f46b0a7f..fcb5280dd90d 100644 --- a/lib/ui/package.json +++ b/lib/ui/package.json @@ -19,7 +19,7 @@ "babel-runtime": "^6.23.0", "deep-equal": "^1.0.1", "events": "^1.1.1", - "@hypnosphi/fuse.js": "^3.0.9", + "fuzzysearch": "^1.0.3", "global": "^4.3.2", "json-stringify-safe": "^5.0.1", "keycode": "^2.1.8", 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 e653f4e0335a..03c3e2da6aae 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 @@ -26,7 +26,7 @@ describe('manager.ui.components.left_panel.index', () => { test('should render stories only if storiesHierarchy prop exists', () => { const selectedKind = 'kk'; const selectedStory = 'bb'; - const storiesHierarchy = createHierarchy([{ kind: 'kk', namespaces: ['kk'], stories: ['bb'] }]); + const storiesHierarchy = createHierarchy([{ kind: 'kk', stories: ['bb'] }]); const wrap = shallow( ({ ...nodesMap, [createNodeKey(node)]: true }), {}); } +function getStoryFilterRegex(storyFilter) { + if (!storyFilter) { + return null; + } + + const validFilter = storyFilter.replace(/[$^*()+[\]{}|\\.?<>'"/;`%]/g, '\\$&'); + + if (!validFilter) { + return null; + } + + return new RegExp(`(${validFilter})`, 'i'); +} + class Stories extends React.Component { constructor(...args) { super(...args); this.onToggle = this.onToggle.bind(this); - const { selectedHierarchy } = this.props; + const { selectedHierarchy, storyFilter } = this.props; this.state = { + storyFilter: getStoryFilterRegex(storyFilter), overriddenFilteredNodes: {}, nodes: getSelectedNodes(selectedHierarchy), }; @@ -65,6 +80,7 @@ class Stories extends React.Component { const selectedNodes = getSelectedNodes(nextSelectedHierarchy); this.setState(prevState => ({ + storyFilter: getStoryFilterRegex(nextStoryFilter), overriddenFilteredNodes: shouldClearFilteredNodes ? {} : prevState.overriddenFilteredNodes, nodes: { ...prevState.nodes, @@ -108,10 +124,11 @@ class Stories extends React.Component { } mapStoriesHierarchy(storiesHierarchy) { + const { storyFilter } = this.state; + const treeModel = { namespaces: storiesHierarchy.namespaces, name: storiesHierarchy.name, - highlight: storiesHierarchy.highlight, }; if (storiesHierarchy.isNamespace) { @@ -133,17 +150,18 @@ class Stories extends React.Component { treeModel.type = treeNodeTypes.COMPONENT; treeModel.children = storiesHierarchy.stories.map(story => ({ - name: story.name, - story: story.name, + story, + storyFilter, kind: storiesHierarchy.kind, - active: selectedStory === story.name && selectedKind === storiesHierarchy.kind, + name: story, + active: selectedStory === story && selectedKind === storiesHierarchy.kind, type: treeNodeTypes.STORY, - highlight: story.highlight, })); } treeModel.key = createNodeKey(treeModel); treeModel.toggled = this.isToggled(treeModel); + treeModel.storyFilter = storyFilter; return treeModel; } @@ -153,7 +171,7 @@ class Stories extends React.Component { } isFilteredNode(key) { - if (!this.props.storyFilter) { + if (!this.state.storyFilter) { return false; } diff --git a/lib/ui/src/modules/ui/components/left_panel/stories_tree/index.test.js b/lib/ui/src/modules/ui/components/left_panel/stories_tree/index.test.js index b4854236cb2d..708fdcc8d183 100644 --- a/lib/ui/src/modules/ui/components/left_panel/stories_tree/index.test.js +++ b/lib/ui/src/modules/ui/components/left_panel/stories_tree/index.test.js @@ -2,8 +2,7 @@ import { shallow, mount } from 'enzyme'; import React from 'react'; import Stories from './index'; import { setContext } from '../../../../../compose'; -import { createHierarchy, prepareStoriesForHierarchy } from '../../../libs/hierarchy'; -import { storyFilter } from '../../../libs/filters'; +import { createHierarchy } from '../../../libs/hierarchy'; const leftClick = { button: 0 }; @@ -22,23 +21,20 @@ describe('manager.ui.components.left_panel.stories', () => { afterEach(() => setContext(null)); const data = createHierarchy([ - { kind: 'a', name: 'a', namespaces: ['a'], stories: ['a1', 'a2'] }, - { kind: 'b', name: 'b', namespaces: ['b'], stories: ['b1', 'b2'] }, + { kind: 'a', stories: ['a1', 'a2'] }, + { kind: 'b', stories: ['b1', 'b2'] }, ]); - - const initialData = [ - { - kind: 'some.name.item1', - stories: ['a1', 'a2'], - }, - { - kind: 'another.space.20', - stories: ['b1', 'b2'], - }, - ]; - - const dataWithoutSeparator = createHierarchy(prepareStoriesForHierarchy(initialData)); - const dataWithSeparator = createHierarchy(prepareStoriesForHierarchy(initialData, '\\.')); + const dataWithoutSeparator = createHierarchy([ + { kind: 'some.name.item1', stories: ['a1', 'a2'] }, + { kind: 'another.space.20', stories: ['b1', 'b2'] }, + ]); + const dataWithSeparator = createHierarchy( + [ + { kind: 'some.name.item1', stories: ['a1', 'a2'] }, + { kind: 'another.space.20', stories: ['b1', 'b2'] }, + ], + '\\.' + ); describe('render', () => { test('should render stories - empty', () => { @@ -223,24 +219,13 @@ describe('manager.ui.components.left_panel.stories', () => { }); 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 = createHierarchy(filteredData); - const wrap = mount( ); diff --git a/lib/ui/src/modules/ui/components/left_panel/stories_tree/tree_decorators.js b/lib/ui/src/modules/ui/components/left_panel/stories_tree/tree_decorators.js index 5bddf499ad82..5b511a36ad4a 100644 --- a/lib/ui/src/modules/ui/components/left_panel/stories_tree/tree_decorators.js +++ b/lib/ui/src/modules/ui/components/left_panel/stories_tree/tree_decorators.js @@ -5,7 +5,6 @@ import PropTypes from 'prop-types'; import { MenuLink } from '../../../containers/routed_link'; import MenuItem from '../../menu_item'; import treeNodeTypes from './tree_node_type'; -import { highlightNode } from './tree_decorators_utils'; function noop() {} @@ -81,26 +80,56 @@ ContainerDecorator.propTypes = { onClick: PropTypes.func.isRequired, }; -function HeaderDecorator(props) { - const { style, node, ...restProps } = props; +class HeaderDecorator extends React.Component { + decorateNameMatchedToSearchTerm(node, style) { + const { storyFilter, name } = node; + + if (!storyFilter) { + return name; + } + + const nameParts = name.split(storyFilter); + + return nameParts.filter(part => part).map((part, index) => { + const key = `${part}-${index}`; + + if (!storyFilter.test(part)) { + return ( + + {part} + + ); + } + + return ( + + {part} + + ); + }); + } - let newStyle = style; + render() { + const { style, node, ...restProps } = this.props; - if (node.type === treeNodeTypes.STORY) { - newStyle = { - ...style, - title: null, - }; - } + let newStyle = style; - const name = highlightNode(node, style); + if (node.type === treeNodeTypes.STORY) { + newStyle = { + ...style, + title: null, + }; + } - const newNode = { - ...node, - name, - }; + const name = this.decorateNameMatchedToSearchTerm(node, style); - return ; + const newNode = { + ...node, + name, + }; + + return ; + } } HeaderDecorator.propTypes = { @@ -110,7 +139,7 @@ HeaderDecorator.propTypes = { }).isRequired, node: PropTypes.shape({ type: PropTypes.oneOf([treeNodeTypes.NAMESPACE, treeNodeTypes.COMPONENT, treeNodeTypes.STORY]), - highlight: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)), + storyFilter: PropTypes.instanceOf(RegExp), }).isRequired, }; diff --git a/lib/ui/src/modules/ui/components/left_panel/stories_tree/tree_decorators_utils.js b/lib/ui/src/modules/ui/components/left_panel/stories_tree/tree_decorators_utils.js deleted file mode 100644 index 82969e23f3ca..000000000000 --- a/lib/ui/src/modules/ui/components/left_panel/stories_tree/tree_decorators_utils.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; - -function getParts(name, highlight) { - const nameParts = []; - let last = 0; - - highlight.forEach(([start, end]) => { - if (last < start) { - nameParts.push({ - strong: false, - text: name.substring(last, start), - }); - } - - nameParts.push({ - strong: true, - text: name.substring(start, end + 1), - }); - - last = end + 1; - }); - - if (last < name.length) { - nameParts.push({ - strong: false, - text: name.substring(last, name.length), - }); - } - - return nameParts; -} - -export function highlightNode(node, style) { - const { name, highlight } = node; - - if (!highlight || !highlight.length) { - return name; - } - - const nameParts = getParts(name, highlight); - - return nameParts.filter(part => part.text).map((part, index) => { - const key = `${part.text}-${index}`; - - if (part.strong) { - return ( - - {part.text} - - ); - } - - return ( - - {part.text} - - ); - }); -} diff --git a/lib/ui/src/modules/ui/components/left_panel/stories_tree/tree_decorators_utils.test.js b/lib/ui/src/modules/ui/components/left_panel/stories_tree/tree_decorators_utils.test.js deleted file mode 100644 index 1de23bb03820..000000000000 --- a/lib/ui/src/modules/ui/components/left_panel/stories_tree/tree_decorators_utils.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import { shallow } from 'enzyme'; -import { highlightNode } from './tree_decorators_utils'; - -describe('manager.ui.components.left_panel.tree_decorators_utils.test', () => { - describe('highlightNode', () => { - test('should return name when there no highlighting matches', () => { - const node = { - name: 'some name', - highlight: null, - }; - - const result = highlightNode(node); - - expect(result).toEqual('some name'); - }); - - test('should return highlighted name when there matches', () => { - const node = { - name: 'some name', - highlight: [[1, 3], [5, 7]], - }; - - const result = highlightNode(node, { highLightText: { color: 'red' } }); - - expect(shallow(result[0]).html()).toEqual('s'); - expect(shallow(result[1]).html()).toEqual('ome'); - expect(shallow(result[2]).html()).toEqual(' '); - expect(shallow(result[3]).html()).toEqual('nam'); - expect(shallow(result[4]).html()).toEqual('e'); - }); - }); -}); diff --git a/lib/ui/src/modules/ui/containers/left_panel.js b/lib/ui/src/modules/ui/containers/left_panel.js index 43be47b26c16..43188d6378a7 100755 --- a/lib/ui/src/modules/ui/containers/left_panel.js +++ b/lib/ui/src/modules/ui/containers/left_panel.js @@ -2,33 +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 { - prepareStoriesForHierarchy, - resolveStoryHierarchy, - createHierarchy, -} from '../libs/hierarchy'; +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, hierarchySeparator, sidebarAnimations } = uiOptions; - - const preparedStories = prepareStoriesForHierarchy(stories, hierarchySeparator); - const filteredStories = filters.storyFilter( - preparedStories, + stories, storyFilter, selectedKind, - selectedStory, sortStoriesByKind ); - const storiesHierarchy = createHierarchy(filteredStories); + const storiesHierarchy = createHierarchy(filteredStories, hierarchySeparator); const selectedHierarchy = resolveStoryHierarchy(selectedKind, hierarchySeparator); - return { + const data = { storiesHierarchy, selectedKind, selectedStory, @@ -43,6 +34,8 @@ export const mapper = (state, props, { actions }) => { name, url, }; + + return data; }; export default compose(genPoddaLoader(mapper))(LeftPanel); 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 e7af7d49db92..8896e6ed983c 100755 --- a/lib/ui/src/modules/ui/containers/left_panel.test.js +++ b/lib/ui/src/modules/ui/containers/left_panel.test.js @@ -36,20 +36,7 @@ describe('manager.ui.containers.left_panel', () => { const result = mapper(state, props, env); expect(result.storiesHierarchy.map).toEqual( - new Map([ - [ - 'sk', - [ - { - kind: 'sk', - stories: [{ highlight: null, name: 'dd' }], - highlight: null, - name: 'sk', - namespaces: ['sk'], - }, - ], - ], - ]) + new Map([['sk', [{ ...stories[0], name: 'sk', namespaces: ['sk'] }]]]) ); expect(result.selectedKind).toBe(selectedKind); expect(result.selectedHierarchy).toEqual(selectedHierarchy); @@ -98,30 +85,8 @@ describe('manager.ui.containers.left_panel', () => { expect(result.storiesHierarchy.map).toEqual( new Map([ - [ - 'pk', // selected kind is always there. That's why this is here. - [ - { - kind: 'pk', - stories: [{ highlight: null, name: 'dd' }], - highlight: null, - name: 'pk', - namespaces: ['pk'], - }, - ], - ], - [ - 'ss', - [ - { - kind: 'ss', - stories: [{ highlight: null, name: 'dd' }], - highlight: [[0, 1]], - name: 'ss', - namespaces: ['ss'], - }, - ], - ], + ['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'] }]], ]) ); }); @@ -165,31 +130,8 @@ describe('manager.ui.containers.left_panel', () => { expect(result.storiesHierarchy.map).toEqual( new Map([ - // selected kind is always there. That's why this is here. - [ - 'pk', - [ - { - kind: 'pk', - stories: [{ highlight: null, name: 'dd' }], - highlight: null, - name: 'pk', - namespaces: ['pk'], - }, - ], - ], - [ - 'ss', - [ - { - kind: 'ss', - stories: [{ highlight: null, name: 'dd' }], - highlight: [[0, 1]], - name: 'ss', - namespaces: ['ss'], - }, - ], - ], + ['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/filters.js b/lib/ui/src/modules/ui/libs/filters.js index 6fb1860282fe..cc327758b818 100755 --- a/lib/ui/src/modules/ui/libs/filters.js +++ b/lib/ui/src/modules/ui/libs/filters.js @@ -1,117 +1,37 @@ -import Fuse from '@hypnosphi/fuse.js'; +import fuzzysearch from 'fuzzysearch'; import sortBy from 'lodash.sortby'; -const searchOptions = { - shouldSort: false, - tokenize: true, - matchAllTokens: false, - includeMatches: true, - findAllMatches: true, - includeScore: false, - threshold: 0.2, - location: 0, - distance: 200, - maxPatternLength: 32, - minMatchCharLength: 2, - keys: ['namespaces', 'storyName', 'searchHook'], -}; - function sort(stories, sortStoriesByKind) { if (!sortStoriesByKind) return stories; return sortBy(stories, ['kind']); } -function flattenStories(items) { - return items.reduce((arr, item) => { - const flatten = item.stories.map(story => ({ - kind: item.kind, - namespaces: item.namespaces, - storyName: story, - })); - - return arr.concat(flatten); - }, []); -} - -function applySearchHookForSelectedKind(stories, filter, selectedKind, selectedStory) { - return stories.map(story => { - if (story.kind === selectedKind && story.storyName === selectedStory) { - return { - ...story, - searchHook: filter, - }; - } - - return story; - }); -} - -function getGroupedStoryItem(map, item, matches) { - let storyItem = map.get(item.kind); - - if (!storyItem) { - storyItem = { - kind: item.kind, - namespaces: item.namespaces, - stories: [], - matches: matches.filter(match => match.key === 'namespaces'), - }; - - map.set(item.kind, storyItem); - } - - return storyItem; -} - -function appendStoryMatch(item, matches) { - const storyMatch = matches.find(match => match.key === 'storyName'); - - if (storyMatch) { - item.matches.push({ - indices: storyMatch.indices, - value: storyMatch.value, - key: 'stories', - }); - } -} - -function groupStories(matchedItems) { - const storiesMap = matchedItems.reduce((map, matchedItem) => { - const { item, matches } = matchedItem; - const groupedStoryItem = getGroupedStoryItem(map, item, matches); - - groupedStoryItem.stories.push(item.storyName); - appendStoryMatch(groupedStoryItem, matches); - - return map; - }, new Map()); - - return Array.from(storiesMap.values()); -} - -export function storyFilter(stories, filter, selectedKind, selectedStory, sortStoriesByKind) { - if (!stories) { - return null; - } - +export function storyFilter(stories, filter, selectedKind, sortStoriesByKind) { + if (!stories) return null; const sorted = sort(stories, sortStoriesByKind); + if (!filter) return sorted; + return sorted.reduce((acc, kindInfo) => { + // Don't filter out currently selected filter + if (kindInfo.kind === selectedKind) return acc.concat(kindInfo); + const needle = filter.toLocaleLowerCase(); + const hstack = kindInfo.kind.toLocaleLowerCase(); + + // If a match is found in the story hierachy structure return kindInfo + if (fuzzysearch(needle, hstack)) return acc.concat(kindInfo); + + // Now search at individual story level and filter results + const matchedStories = kindInfo.stories.filter(story => { + const storyHstack = story.toLocaleLowerCase(); + return fuzzysearch(needle, storyHstack); + }); - if (!filter) { - return sorted; - } - - const flattened = flattenStories(sorted); - - const storiesWithHook = applySearchHookForSelectedKind( - flattened, - filter, - selectedKind, - selectedStory - ); - - const fuse = new Fuse(storiesWithHook, searchOptions); - const foundStories = fuse.search(filter); + if (matchedStories.length) + return acc.concat({ + kind: kindInfo.kind, + stories: matchedStories, + }); - return groupStories(foundStories); + return acc; + }, []); } diff --git a/lib/ui/src/modules/ui/libs/filters.test.js b/lib/ui/src/modules/ui/libs/filters.test.js index 2e24b4eb77c7..abfe36c21348 100755 --- a/lib/ui/src/modules/ui/libs/filters.test.js +++ b/lib/ui/src/modules/ui/libs/filters.test.js @@ -8,55 +8,36 @@ describe('manager.ui.libs.filters', () => { }); test('should original stories if there is no filter', () => { - const stories = [{ kind: ['aa'], namespaces: ['aa'], stories: ['bb'] }]; + const stories = [{ kind: 'aa', stories: ['bb'] }]; const res = storyFilter(stories); expect(res).toBe(stories); }); test('should always return the selectedKind', () => { - const stories = [ - { kind: 'aa', namespaces: ['aa'], stories: ['bb'] }, - { kind: 'bb', namespaces: ['bb'], stories: ['bb'] }, - ]; - + const stories = [{ kind: 'aa', stories: ['bb'] }, { kind: 'bb', stories: ['bb'] }]; const selectedKind = 'bb'; - const selectedStory = 'bb'; - const res = storyFilter(stories, 'no-match', selectedKind, selectedStory); + const res = storyFilter(stories, 'no-match', selectedKind); - expect(res).toMatchObject([stories[1]]); - }); - - test('should always return the selectedKind with the single selectedStory', () => { - const stories = [ - { kind: 'aa', namespaces: ['aa'], stories: ['bb'] }, - { kind: 'bb', namespaces: ['bb'], stories: ['bb', 'cc', 'dd'] }, - ]; - - const selectedKind = 'bb'; - const selectedStory = 'cc'; - const res = storyFilter(stories, 'no-match', selectedKind, selectedStory); - - expect(res[0].stories).toEqual(['cc']); + expect(res).toEqual([stories[1]]); }); test('should filter kinds correctly', () => { const stories = [ - { kind: 'aa', namespaces: ['aa'], stories: ['bb'] }, - { kind: 'bb', namespaces: ['bb'], stories: ['bb'] }, - { kind: 'ss', namespaces: ['ss'], stories: ['bb'] }, + { kind: 'aa', stories: ['bb'] }, + { kind: 'bb', stories: ['bb'] }, + { kind: 'ss', stories: ['bb'] }, ]; const selectedKind = 'bb'; - const selectedStory = 'bb'; - const res = storyFilter(stories, 'aa', selectedKind, selectedStory); + const res = storyFilter(stories, 'aa', selectedKind); - expect(res).toMatchObject([stories[0], stories[1]]); + expect(res).toEqual([stories[0], stories[1]]); }); test('should not sort stories by kind', () => { const stories = [ - { kind: 'ss', namespaces: ['ss'], stories: ['bb'] }, - { kind: 'aa', namespaces: ['aa'], stories: ['bb'] }, - { kind: 'bb', namespaces: ['bb'], stories: ['bb'] }, + { kind: 'ss', stories: ['bb'] }, + { kind: 'aa', stories: ['bb'] }, + { kind: 'bb', stories: ['bb'] }, ]; const res = storyFilter(stories); @@ -65,62 +46,53 @@ describe('manager.ui.libs.filters', () => { test('should sort stories by kind', () => { const stories = [ - { kind: 'ss', namespaces: ['ss'], stories: ['bb'] }, - { kind: 'aa', namespaces: ['aa'], stories: ['bb'] }, - { kind: 'bb', namespaces: ['bb'], stories: ['bb'] }, + { kind: 'ss', stories: ['bb'] }, + { kind: 'aa', stories: ['bb'] }, + { kind: 'bb', stories: ['bb'] }, ]; - const res = storyFilter(stories, null, null, null, true); + const res = storyFilter(stories, null, null, true); expect(res).toEqual([stories[1], stories[2], stories[0]]); }); test('should filter on story level', () => { const stories = [ - { kind: 'aa', namespaces: ['aa'], stories: ['bb'] }, - { kind: 'cc', namespaces: ['cc'], stories: ['dd'] }, - { kind: 'ee', namespaces: ['ee'], stories: ['ff'] }, + { kind: 'aa', stories: ['bb'] }, + { kind: 'cc', stories: ['dd'] }, + { kind: 'ee', stories: ['ff'] }, ]; const selectedKind = 'aa'; - const selectedStory = 'bb'; - const res = storyFilter(stories, 'ff', selectedKind, selectedStory); + const res = storyFilter(stories, 'ff', selectedKind); - expect(res).toMatchObject([stories[0], stories[2]]); + expect(res).toEqual([stories[0], stories[2]]); }); test('should filter out unmatched stories at lowest level', () => { const stories = [ - { kind: 'aa', namespaces: ['aa'], stories: ['bb'] }, - { kind: 'cc', namespaces: ['cc'], stories: ['dd'] }, - { kind: 'ee', namespaces: ['ee'], stories: ['ff', 'gg'] }, + { kind: 'aa', stories: ['bb'] }, + { kind: 'cc', stories: ['dd'] }, + { kind: 'ee', stories: ['ff', 'gg'] }, ]; const selectedKind = 'aa'; - const selectedStory = 'bb'; - const res = storyFilter(stories, 'ff', selectedKind, selectedStory); + const res = storyFilter(stories, 'ff', selectedKind); - expect(res).toMatchObject([stories[0], { kind: 'ee', stories: ['ff'] }]); + expect(res).toEqual([stories[0], { kind: 'ee', stories: ['ff'] }]); }); test('should be case insensitive at tree level', () => { - const stories = [ - { kind: 'Aa', namespaces: ['aA'], stories: ['bb'] }, - { kind: 'cc', namespaces: ['cc'], stories: ['dd'] }, - ]; + const stories = [{ kind: 'aA', stories: ['bb'] }, { kind: 'cc', stories: ['dd'] }]; const selectedKind = 'aA'; const res = storyFilter(stories, 'aa', selectedKind); - expect(res).toMatchObject([stories[0]]); + expect(res).toEqual([stories[0]]); }); test('should be case insensitive at story level', () => { - const stories = [ - { kind: 'aa', namespaces: ['aa'], stories: ['bb'] }, - { kind: 'cc', namespaces: ['cc'], stories: ['dd', 'eE'] }, - ]; + const stories = [{ kind: 'aa', stories: ['bb'] }, { kind: 'cc', stories: ['dd', 'eE'] }]; const selectedKind = 'aa'; - const selectedStory = 'bb'; - const res = storyFilter(stories, 'ee', selectedKind, selectedStory); + const res = storyFilter(stories, 'ee', selectedKind); - expect(res).toMatchObject([stories[0], { kind: 'cc', stories: ['eE'] }]); + expect(res).toEqual([stories[0], { kind: 'cc', stories: ['eE'] }]); }); }); }); diff --git a/lib/ui/src/modules/ui/libs/hierarchy.js b/lib/ui/src/modules/ui/libs/hierarchy.js index c96f0cb1d651..8f23bee2b683 100644 --- a/lib/ui/src/modules/ui/libs/hierarchy.js +++ b/lib/ui/src/modules/ui/libs/hierarchy.js @@ -1,49 +1,9 @@ -function findMatches(matches, type, value) { - if (!matches) { - return null; - } - - const matchForType = matches - .filter(match => match.key === type) - .find(match => match.value === value); - - if (!matchForType) { - return null; - } - - return matchForType.indices; -} - -function createComponentNode(namespace, story) { - return { - name: story.name, - namespaces: story.namespaces, - highlight: findMatches(story.matches, 'namespaces', namespace), - kind: story.kind, - stories: story.stories.map(s => ({ - name: s, - highlight: findMatches(story.matches, 'stories', s), - })), - }; -} - -function createNamespaceNode(namespace, hierarchy, story) { - return { - isNamespace: true, - name: namespace, - namespaces: [...hierarchy.namespaces, namespace], - highlight: findMatches(story.matches, 'namespaces', namespace), - map: new Map(), - }; -} - function fillHierarchy(namespaces, hierarchy, story) { if (namespaces.length === 1) { const namespace = namespaces[0]; const childItems = hierarchy.map.get(namespace) || []; - const component = createComponentNode(namespace, story); - childItems.push(component); + childItems.push(story); hierarchy.map.set(namespace, childItems); return; } @@ -53,7 +13,14 @@ function fillHierarchy(namespaces, hierarchy, story) { let childHierarchy = childItems.find(item => item.isNamespace); if (!childHierarchy) { - childHierarchy = createNamespaceNode(namespace, hierarchy, story); + childHierarchy = { + isNamespace: true, + name: namespace, + namespaces: [...hierarchy.namespaces, namespace], + firstKind: story.kind, + map: new Map(), + }; + childItems.push(childHierarchy); hierarchy.map.set(namespace, childItems); } @@ -61,26 +28,6 @@ function fillHierarchy(namespaces, hierarchy, story) { fillHierarchy(namespaces.slice(1), childHierarchy, story); } -export function createHierarchy(stories) { - const hierarchyRoot = { - isNamespace: true, - namespaces: [], - name: '', - map: new Map(), - }; - - if (stories) { - stories.forEach(story => { - const { namespaces } = story; - const name = namespaces[namespaces.length - 1]; - - fillHierarchy(namespaces, hierarchyRoot, { ...story, name }); - }); - } - - return hierarchyRoot; -} - export function resolveStoryHierarchy(storyName = '', hierarchySeparator) { if (!hierarchySeparator) { return [storyName]; @@ -89,17 +36,29 @@ export function resolveStoryHierarchy(storyName = '', hierarchySeparator) { return storyName.split(new RegExp(hierarchySeparator)); } -export function prepareStoriesForHierarchy(stories, hierarchySeparator) { +export function createHierarchy(stories, hierarchySeparator) { + const hierarchyRoot = { + isNamespace: true, + namespaces: [], + name: '', + map: new Map(), + }; + if (!stories) { - return null; + return hierarchyRoot; } - return stories.map(story => { + const groupedStories = stories.map(story => { const namespaces = resolveStoryHierarchy(story.kind, hierarchySeparator); return { - ...story, namespaces, + name: namespaces[namespaces.length - 1], + ...story, }; }); + + groupedStories.forEach(story => fillHierarchy(story.namespaces, hierarchyRoot, story)); + + return hierarchyRoot; } diff --git a/lib/ui/src/modules/ui/libs/hierarchy.test.js b/lib/ui/src/modules/ui/libs/hierarchy.test.js index 5ee61d8308f5..f2a63d251741 100644 --- a/lib/ui/src/modules/ui/libs/hierarchy.test.js +++ b/lib/ui/src/modules/ui/libs/hierarchy.test.js @@ -1,4 +1,4 @@ -import { createHierarchy, resolveStoryHierarchy, prepareStoriesForHierarchy } from './hierarchy'; +import { createHierarchy, resolveStoryHierarchy } from './hierarchy'; describe('manager.ui.libs.hierarchy', () => { describe('createHierarchy', () => { @@ -24,18 +24,10 @@ describe('manager.ui.libs.hierarchy', () => { }); }); - test('should return flat hierarchy if kind is not separated', () => { + test('should return flat hierarchy if hierarchySeparator is undefined', () => { const stories = [ - { - kind: 'some.name.item1', - namespaces: ['some.name.item1'], - stories: ['a1', 'a2'], - }, - { - kind: 'another.space.20', - namespaces: ['another.space.20'], - stories: ['b1', 'b2'], - }, + { kind: 'some.name.item1', stories: ['a1', 'a2'] }, + { kind: 'another.space.20', stories: ['b1', 'b2'] }, ]; const result = createHierarchy(stories); @@ -47,9 +39,8 @@ describe('manager.ui.libs.hierarchy', () => { { kind: 'some.name.item1', name: 'some.name.item1', - highlight: null, namespaces: ['some.name.item1'], - stories: [{ name: 'a1', highlight: null }, { name: 'a2', highlight: null }], + stories: ['a1', 'a2'], }, ], ], @@ -59,9 +50,8 @@ describe('manager.ui.libs.hierarchy', () => { { kind: 'another.space.20', name: 'another.space.20', - highlight: null, namespaces: ['another.space.20'], - stories: [{ name: 'b1', highlight: null }, { name: 'b2', highlight: null }], + stories: ['b1', 'b2'], }, ], ], @@ -70,18 +60,10 @@ describe('manager.ui.libs.hierarchy', () => { expect(result.map).toEqual(new Map(expected)); }); - test('should return hierarchy if kind is separated', () => { + test('should return hierarchy if hierarchySeparator is defined', () => { const stories = [ - { - kind: 'some.name.item1', - namespaces: ['some', 'name', 'item1'], - stories: ['a1', 'a2'], - }, - { - kind: 'another.space.20', - namespaces: ['another', 'space', '20'], - stories: ['b1', 'b2'], - }, + { kind: 'some.name.item1', stories: ['a1', 'a2'] }, + { kind: 'another.space.20', stories: ['b1', 'b2'] }, ]; const result = createHierarchy(stories, '\\.'); @@ -92,8 +74,8 @@ describe('manager.ui.libs.hierarchy', () => { [ { name: 'some', + firstKind: 'some.name.item1', isNamespace: true, - highlight: null, namespaces: ['some'], map: new Map([ [ @@ -101,8 +83,8 @@ describe('manager.ui.libs.hierarchy', () => { [ { name: 'name', + firstKind: 'some.name.item1', isNamespace: true, - highlight: null, namespaces: ['some', 'name'], map: new Map([ [ @@ -111,12 +93,8 @@ describe('manager.ui.libs.hierarchy', () => { { kind: 'some.name.item1', name: 'item1', - highlight: null, namespaces: ['some', 'name', 'item1'], - stories: [ - { name: 'a1', highlight: null }, - { name: 'a2', highlight: null }, - ], + stories: ['a1', 'a2'], }, ], ], @@ -133,8 +111,8 @@ describe('manager.ui.libs.hierarchy', () => { [ { name: 'another', + firstKind: 'another.space.20', isNamespace: true, - highlight: null, namespaces: ['another'], map: new Map([ [ @@ -142,8 +120,8 @@ describe('manager.ui.libs.hierarchy', () => { [ { name: 'space', + firstKind: 'another.space.20', isNamespace: true, - highlight: null, namespaces: ['another', 'space'], map: new Map([ [ @@ -152,12 +130,8 @@ describe('manager.ui.libs.hierarchy', () => { { kind: 'another.space.20', name: '20', - highlight: null, namespaces: ['another', 'space', '20'], - stories: [ - { name: 'b1', highlight: null }, - { name: 'b2', highlight: null }, - ], + stories: ['b1', 'b2'], }, ], ], @@ -188,34 +162,4 @@ describe('manager.ui.libs.hierarchy', () => { expect(result).toEqual(['some', 'name', 'item1']); }); }); - - describe('prepareStoriesForHierarchy', () => { - test('should return null when nothing provided', () => { - const result = prepareStoriesForHierarchy(); - - expect(result).toBe(null); - }); - - test('should return kind in namespaces when separator is not provided', () => { - const stories = [{ kind: 'some.name.item1' }, { kind: 'another.space.20' }]; - - const result = prepareStoriesForHierarchy(stories); - - expect(result).toEqual([ - { kind: 'some.name.item1', namespaces: ['some.name.item1'] }, - { kind: 'another.space.20', namespaces: ['another.space.20'] }, - ]); - }); - - test('should return separated namespaces when separator is provided', () => { - const stories = [{ kind: 'some.name.item1' }, { kind: 'another.space.20' }]; - - const result = prepareStoriesForHierarchy(stories, '\\.'); - - expect(result).toEqual([ - { kind: 'some.name.item1', namespaces: ['some', 'name', 'item1'] }, - { kind: 'another.space.20', namespaces: ['another', 'space', '20'] }, - ]); - }); - }); });