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

[KED-1611] Refactor node-list #152

Merged
merged 13 commits into from
Apr 29, 2020
84 changes: 84 additions & 0 deletions src/components/node-list/filter-nodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { createSelector } from 'reselect';
import utils from '@quantumblack/kedro-ui/lib/utils';
const { escapeRegExp, getHighlightedText } = utils;

/**
* Get a list of IDs of the visible nodes
* @param {object} nodes Grouped nodes
* @return {array} List of node IDs
*/
export const getNodeIDs = nodes => {
const getNodeIDs = type => nodes[type].map(node => node.id);
const concatNodeIDs = (nodeIDs, type) => nodeIDs.concat(getNodeIDs(type));

return Object.keys(nodes).reduce(concatNodeIDs, []);
};

/**
* Add a new highlightedLabel field to each of the node objects
* @param {object} nodes Grouped lists of nodes
* @param {string} searchValue Search term
* @return {object} The grouped nodes with highlightedLabel fields added
*/
export const highlightMatch = (nodes, searchValue) => {
const addHighlightedLabel = node => ({
highlightedLabel: getHighlightedText(node.name, searchValue),
...node
});
const addLabelsToNodes = (newNodes, type) => ({
...newNodes,
[type]: nodes[type].map(addHighlightedLabel)
});

return Object.keys(nodes).reduce(addLabelsToNodes, {});
};

/**
* Check whether a name matches the search text
* @param {string} name
* @param {string} searchValue
* @return {boolean} True if match
*/
export const nodeMatchesSearch = (node, searchValue) => {
const valueRegex = searchValue
? new RegExp(escapeRegExp(searchValue), 'gi')
: '';
return Boolean(node.name.match(valueRegex));
};

/**
* Return only the results that match the search text
* @param {object} nodes Grouped lists of nodes
* @param {string} searchValue Search term
* @return {object} Grouped nodes
*/
export const filterNodes = (nodes, searchValue) => {
const filterNodesByType = type =>
nodes[type].filter(node => nodeMatchesSearch(node, searchValue));
const filterNodeLists = (newNodes, type) => ({
...newNodes,
[type]: filterNodesByType(type)
});

return Object.keys(nodes).reduce(filterNodeLists, {});
};

/**
* Return filtered/highlighted nodes, and filtered node IDs
* @param {object} nodes Grouped lists of nodes
* @param {string} searchValue Search term
* @return {object} Grouped nodes, and node IDs
*/
const getFilteredNodes = createSelector(
[state => state.nodes, state => state.searchValue],
(nodes, searchValue) => {
const filteredNodes = filterNodes(nodes, searchValue);

return {
filteredNodes: highlightMatch(filteredNodes, searchValue),
nodeIDs: getNodeIDs(filteredNodes)
};
}
);

export default getFilteredNodes;
154 changes: 154 additions & 0 deletions src/components/node-list/filter-nodes.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import getFilteredNodes, {
getNodeIDs,
highlightMatch,
nodeMatchesSearch,
filterNodes
} from './filter-nodes';
import { mockState } from '../../utils/state.mock';
import { getGroupedNodes } from '../../selectors/nodes';

const ungroupNodes = groupedNodes =>
Object.keys(groupedNodes).reduce(
(names, key) => names.concat(groupedNodes[key]),
[]
);

describe('filter-nodes', () => {
describe('getFilteredNodes', () => {
const nodes = getGroupedNodes(mockState.lorem);
const searchValue = 'e';
const { filteredNodes, nodeIDs } = getFilteredNodes({ nodes, searchValue });
const nodeList = ungroupNodes(filteredNodes);

describe('filteredNodes', () => {
test.each(nodeList.map(node => node.name))(
`node name "%s" contains search term "${searchValue}"`,
name => {
expect(name).toEqual(expect.stringMatching(searchValue));
}
);

test.each(nodeList.map(node => node.highlightedLabel))(
`node label "%s" contains highlighted search term "<b>${searchValue}</b>"`,
name => {
expect(name).toEqual(expect.stringMatching(`<b>${searchValue}</b>`));
}
);
});

describe('nodeIDs', () => {
test.each(nodeIDs)(
`node ID "%s" contains search term "${searchValue}"`,
nodeID => {
expect(nodeID).toEqual(expect.stringMatching(searchValue));
}
);
});
});

describe('getNodeIDs', () => {
const generateNodes = (type, count) =>
Array.from(new Array(count)).map((d, i) => ({
id: type + i
}));

const nodes = {
data: generateNodes('data', 10),
task: generateNodes('task', 10),
parameters: generateNodes('parameters', 10)
};

it('returns a list of node IDs', () => {
const nodeIDs = getNodeIDs(nodes);
expect(nodeIDs).toHaveLength(30);
expect(nodeIDs).toEqual(expect.arrayContaining([expect.any(String)]));
expect(nodeIDs).toContain('data0');
expect(nodeIDs).toContain('task1');
expect(nodeIDs).toContain('parameters1');
});
});

describe('highlightMatch', () => {
const nodes = getGroupedNodes(mockState.animals);
const searchValue = 'e';
const formattedNodes = highlightMatch(nodes, searchValue);
const nodeList = ungroupNodes(formattedNodes);

describe(`nodes which match the search term "${searchValue}"`, () => {
const matchingNodeList = nodeList.filter(node =>
node.name.includes(searchValue)
);
test.each(matchingNodeList.map(node => node.highlightedLabel))(
`node label "%s" contains highlighted search term "<b>${searchValue}</b>"`,
label => {
expect(label).toEqual(expect.stringMatching(`<b>${searchValue}</b>`));
}
);
});

describe(`nodes which do not match the search term "${searchValue}"`, () => {
const notMatchingNodeList = nodeList.filter(
node => !node.name.includes(searchValue)
);
test.each(notMatchingNodeList.map(node => node.highlightedLabel))(
`node label "%s" does not contain "<b>"`,
label => {
expect(label).not.toEqual(expect.stringMatching(`<b>`));
}
);
});
});

describe('nodeMatchesSearch', () => {
const node = { name: 'qwertyuiop' };

it('returns true if the node name matches the search', () => {
expect(nodeMatchesSearch(node, 'qwertyuiop')).toBe(true);
expect(nodeMatchesSearch(node, 'qwe')).toBe(true);
expect(nodeMatchesSearch(node, 'p')).toBe(true);
});

it('returns true if the search is falsey', () => {
expect(nodeMatchesSearch(node, '')).toBe(true);
expect(nodeMatchesSearch(node, null)).toBe(true);
expect(nodeMatchesSearch(node, undefined)).toBe(true);
});

it('returns false if the node name does not match the search', () => {
expect(nodeMatchesSearch(node, 'a')).toBe(false);
expect(nodeMatchesSearch(node, 'qwe ')).toBe(false);
expect(nodeMatchesSearch(node, ' ')).toBe(false);
expect(nodeMatchesSearch(node, '_')).toBe(false);
});
});

describe('filterNodes', () => {
const nodes = getGroupedNodes(mockState.animals);
const searchValue = 'a';
const filteredNodes = filterNodes(nodes, searchValue);
const nodeList = ungroupNodes(filteredNodes);
const notMatchingNodeList = ungroupNodes(nodes).filter(
node => !node.name.includes(searchValue)
);

describe('nodes which match the search term', () => {
test.each(nodeList.map(node => node.name))(
`node name "%s" should contain search term "${searchValue}"`,
name => {
expect(name).toEqual(expect.stringMatching(searchValue));
}
);
});

describe('nodes which do not match the search term', () => {
test.each(notMatchingNodeList.map(node => node.id))(
`filtered node list should not contain a node with id "%s"`,
nodeID => {
expect(nodeList.map(node => node.id)).not.toContain(
expect.stringMatching(searchValue)
);
}
);
});
});
});
Loading