Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hierarchy expansion on search #1598

Merged
merged 13 commits into from
Aug 6, 2017
1 change: 1 addition & 0 deletions lib/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"global": "^4.3.2",
"json-stringify-safe": "^5.0.1",
"keycode": "^2.1.8",
"lodash.debounce": "4.0.8",
"lodash.pick": "^4.4.0",
"lodash.sortby": "^4.7.0",
"mantra-core": "^1.7.0",
Expand Down
1 change: 1 addition & 0 deletions lib/ui/src/modules/ui/components/left_panel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const storyProps = [
'selectedHierarchy',
'selectedStory',
'onSelectStory',
'storyFilter',
'sidebarAnimations',
];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,26 +34,54 @@ function getSelectedNodes(selectedHierarchy) {
.reduce((nodesMap, node) => ({ ...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),
};
}

componentWillReceiveProps(nextProps) {
const { selectedHierarchy: nextSelectedHierarchy = [] } = nextProps;
const { selectedHierarchy: currentSelectedHierarchy = [] } = this.props;
const {
selectedHierarchy: nextSelectedHierarchy = [],
storyFilter: nextStoryFilter,
} = nextProps;

const {
selectedHierarchy: currentSelectedHierarchy = [],
storyFilter: currentStoryFilter,
} = this.props;

if (!deepEqual(nextSelectedHierarchy, currentSelectedHierarchy)) {
const shouldClearFilteredNodes = nextStoryFilter !== currentStoryFilter;
const selectedHierarchyChanged = !deepEqual(nextSelectedHierarchy, currentSelectedHierarchy);

if (selectedHierarchyChanged || shouldClearFilteredNodes) {
const selectedNodes = getSelectedNodes(nextSelectedHierarchy);

this.setState(prevState => ({
storyFilter: getStoryFilterRegex(nextStoryFilter),
overriddenFilteredNodes: shouldClearFilteredNodes ? {} : prevState.overriddenFilteredNodes,
nodes: {
...prevState.nodes,
...selectedNodes,
Expand All @@ -78,6 +106,10 @@ class Stories extends React.Component {
...prevState.nodes,
[node.key]: toggled,
},
overriddenFilteredNodes: {
...prevState.overriddenFilteredNodes,
[node.key]: !toggled,
},
}));
}

Expand All @@ -92,6 +124,8 @@ class Stories extends React.Component {
}

mapStoriesHierarchy(storiesHierarchy) {
const { storyFilter } = this.state;

const treeModel = {
namespaces: storiesHierarchy.namespaces,
name: storiesHierarchy.name,
Expand All @@ -116,26 +150,39 @@ class Stories extends React.Component {
treeModel.type = treeNodeTypes.COMPONENT;

treeModel.children = storiesHierarchy.stories.map(story => ({
kind: storiesHierarchy.kind,
story,
storyFilter,
kind: storiesHierarchy.kind,
name: story,
active: selectedStory === story && selectedKind === storiesHierarchy.kind,
type: treeNodeTypes.STORY,
}));
}

treeModel.key = createNodeKey(treeModel);
treeModel.toggled = this.state.nodes[treeModel.key];
treeModel.toggled = this.isToggled(treeModel);
treeModel.storyFilter = storyFilter;

return treeModel;
}

isToggled(treeModel) {
return this.state.nodes[treeModel.key] || this.isFilteredNode(treeModel.key);
}

isFilteredNode(key) {
if (!this.state.storyFilter) {
return false;
}

return !this.state.overriddenFilteredNodes[key];
}

render() {
const { storiesHierarchy, sidebarAnimations } = this.props;

const data = this.mapStoriesHierarchy(storiesHierarchy);
data.toggled = true;
data.name = 'stories';
data.root = true;

return (
Expand All @@ -153,9 +200,12 @@ class Stories extends React.Component {
Stories.defaultProps = {
onSelectStory: null,
storiesHierarchy: null,
storyFilter: null,
sidebarAnimations: true,
};

Stories.propTypes = {
storyFilter: PropTypes.string,
storiesHierarchy: PropTypes.shape({
namespaces: PropTypes.arrayOf(PropTypes.string),
name: PropTypes.string,
Expand All @@ -165,7 +215,7 @@ Stories.propTypes = {
selectedKind: PropTypes.string.isRequired,
selectedStory: PropTypes.string.isRequired,
onSelectStory: PropTypes.func,
sidebarAnimations: PropTypes.bool.isRequired,
sidebarAnimations: PropTypes.bool,
};

export default Stories;
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,22 @@ describe('manager.ui.components.left_panel.stories', () => {

expect(setState).not.toHaveBeenCalled();
});

test('should render stories with with highlighting when storiesFilter is provided', () => {
const wrap = mount(
<Stories
storiesHierarchy={dataWithSeparator}
selectedKind="another.space.20"
selectedStory="b2"
selectedHierarchy={['another', 'space', '20']}
storyFilter="th"
/>
);

const highlightedElements = wrap.find('strong');

expect(highlightedElements.text()).toBe('th');
});
});

describe('events', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,22 +84,59 @@ ContainerDecorator.propTypes = {
onClick: PropTypes.func.isRequired,
};

function HeaderDecorator(props) {
const { style, node } = props;

let newStyle = style;

if (node.type === treeNodeTypes.STORY) {
newStyle = {
...style,
title: {
...style.title,
...style.storyTitle,
},
};
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 (
<span key={key}>
{part}
</span>
);
}

return (
<strong key={key} style={style.highLightText}>
{part}
</strong>
);
});
}

return <decorators.Header {...props} style={newStyle} />;
render() {
const { style, node, ...restProps } = this.props;

let newStyle = style;

if (node.type === treeNodeTypes.STORY) {
newStyle = {
...style,
title: {
...style.title,
...style.storyTitle,
},
};
}

const name = this.decorateNameMatchedToSearchTerm(node, style);

const newNode = {
...node,
name,
};

return <decorators.Header style={newStyle} node={newNode} {...restProps} />;
}
}

HeaderDecorator.propTypes = {
Expand All @@ -109,6 +146,7 @@ HeaderDecorator.propTypes = {
}).isRequired,
node: PropTypes.shape({
type: PropTypes.oneOf([treeNodeTypes.NAMESPACE, treeNodeTypes.COMPONENT, treeNodeTypes.STORY]),
storyFilter: PropTypes.instanceOf(RegExp),
}).isRequired,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ export default {
storyTitle: {
fontSize: '13px',
},
highLightText: {
backgroundColor: '#FFFEAA',
fontWeight: 'inherit',
},
},
subtree: {
paddingLeft: '19px',
Expand Down
Loading