Skip to content

Commit

Permalink
feat(MultiRange): indicate if a range has no refinements (#1926)
Browse files Browse the repository at this point in the history
  • Loading branch information
mthuret authored and vvo committed Jan 31, 2017
1 parent 8933dc1 commit 80b6450
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 50 deletions.
1 change: 1 addition & 0 deletions packages/react-instantsearch/src/components/List.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class List extends Component {
{...this.props.cx(
'item',
item.isRefined && 'itemSelected',
item.noRefinement && 'itemNoRefinement',
items && 'itemParent',
items && item.isRefined && 'itemSelectedParent'
)}
Expand Down
2 changes: 2 additions & 0 deletions packages/react-instantsearch/src/components/MultiRange.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class MultiRange extends Component {
items: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.node.isRequired,
value: PropTypes.string.isRequired,
isRefined: PropTypes.bool.isRequired,
noRefinement: PropTypes.bool.isRequired,
})).isRequired,
refine: PropTypes.func.isRequired,
transformItems: PropTypes.func,
Expand Down
48 changes: 36 additions & 12 deletions packages/react-instantsearch/src/components/MultiRange.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ describe('MultiRange', () => {
createURL={() => '#'}
refine={() => null}
items={[
{label: 'label1', value: '10:', isRefined: false},
{label: 'label2', value: '10:20', isRefined: false},
{label: 'label3', value: '20:30', isRefined: false},
{label: 'label4', value: '30:', isRefined: false},
{label: 'label1', value: '10:', isRefined: false, noRefinement: false},
{label: 'label2', value: '10:20', isRefined: false, noRefinement: false},
{label: 'label3', value: '20:30', isRefined: false, noRefinement: false},
{label: 'label4', value: '30:', isRefined: false, noRefinement: false},
]}
canRefine={true}
/>
Expand All @@ -30,10 +30,10 @@ describe('MultiRange', () => {
createURL={() => '#'}
refine={() => null}
items={[
{label: 'label1', value: '10:', isRefined: false},
{label: 'label2', value: '10:20', isRefined: true},
{label: 'label3', value: '20:30', isRefined: false},
{label: 'label4', value: '30:', isRefined: false},
{label: 'label1', value: '10:', isRefined: false, noRefinement: false},
{label: 'label2', value: '10:20', isRefined: true, noRefinement: false},
{label: 'label3', value: '20:30', isRefined: false, noRefinement: false},
{label: 'label4', value: '30:', isRefined: false, noRefinement: false},
]}
canRefine={true}
/>
Expand All @@ -47,10 +47,10 @@ describe('MultiRange', () => {
<MultiRange
refine={refine}
items={[
{label: 'label', value: '10:'},
{label: 'label', value: '10:20'},
{label: 'label', value: '20:30'},
{label: 'label', value: '30:'},
{label: 'label', value: '10:', isRefined: false, noRefinement: false},
{label: 'label', value: '10:20', isRefined: false, noRefinement: false},
{label: 'label', value: '20:30', isRefined: false, noRefinement: false},
{label: 'label', value: '30:', isRefined: false, noRefinement: false},
]}
canRefine={true}
/>
Expand All @@ -69,4 +69,28 @@ describe('MultiRange', () => {

wrapper.unmount();
});

it('indicate when there is no refinement', () => {
const refine = jest.fn();
const wrapper = mount(
<MultiRange
refine={refine}
items={[
{label: 'label', value: '10:', isRefined: false, noRefinement: true},
{label: 'label', value: '10:20', isRefined: false, noRefinement: true},
{label: 'label', value: '20:30', isRefined: false, noRefinement: true},
{label: 'label', value: '30:', isRefined: false, noRefinement: true},
]}
canRefine={false}
/>
);

const itemWrapper = wrapper.find('.ais-MultiRange__noRefinement');
expect(itemWrapper.length).toBe(1);

const items = wrapper.find('.ais-MultiRange__itemNoRefinement');
expect(items.length).toBe(4);

wrapper.unmount();
});
});
29 changes: 26 additions & 3 deletions packages/react-instantsearch/src/connectors/connectMultiRange.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,25 @@ function getCurrentRefinement(props, searchState) {
return '';
}

function isRefinementsRangeIncludesInsideItemRange(stats, start, end) {
return stats.min > start && stats.min < end || stats.max > start && stats.max < end;
}

function isItemRangeIncludedInsideRefinementsRange(stats, start, end) {
return start > stats.min && start < stats.max || end > stats.min && end < stats.max;
}

function itemHasRefinement(attributeName, results, value) {
const stats = results.getFacetByName(attributeName) ?
results.getFacetStats(attributeName) : null;
const range = value.split(':');
const start = Number(range[0]) === 0 || value === '' ? Number.NEGATIVE_INFINITY : Number(range[0]);
const end = Number(range[1]) === 0 || value === '' ? Number.POSITIVE_INFINITY : Number(range[1]);
return !(Boolean(stats) &&
(isRefinementsRangeIncludesInsideItemRange(stats, start, end)
|| isItemRangeIncludedInsideRefinementsRange(stats, start, end)));
}

/**
* connectMultiRange connector provides the logic to build a widget that will
* give the user the ability to select a range value for a numeric attribute.
Expand All @@ -51,7 +70,7 @@ function getCurrentRefinement(props, searchState) {
* @providedPropType {function} refine - a function to select a range.
* @providedPropType {function} createURL - a function to generate a URL for the corresponding search state
* @providedPropType {string} currentRefinement - the refinement currently applied. follow the shape of a `string` with a pattern of `'{start}:{end}'` which corresponds to the current selected item. For instance, when the selected item is `{start: 10, end: 20}`, the searchState of the widget is `'10:20'`. When `start` isn't defined, the searchState of the widget is `':{end}'`, and the same way around when `end` isn't defined. However, when neither `start` nor `end` are defined, the searchState is an empty string.
* @providedPropType {array.<{isRefined: boolean, label: string, value: string}>} items - the list of ranges the MultiRange can display.
* @providedPropType {array.<{isRefined: boolean, label: string, value: string, isRefined: boolean, noRefinement: boolean}>} items - the list of ranges the MultiRange can display.
*/
export default createConnector({
displayName: 'AlgoliaMultiRange',
Expand All @@ -67,20 +86,23 @@ export default createConnector({
transformItems: PropTypes.func,
},

getProvidedProps(props, searchState) {
getProvidedProps(props, searchState, searchResults) {
const currentRefinement = getCurrentRefinement(props, searchState);
const items = props.items.map(item => {
const value = stringifyItem(item);
return {
label: item.label,
value,
isRefined: value === currentRefinement,
noRefinement: searchResults && searchResults.results ?
itemHasRefinement(getId(props), searchResults.results, value) : false,
};
});

return {
items: props.transformItems ? props.transformItems(items) : items,
currentRefinement,
canRefine: items.length > 0,
canRefine: !items.reduce((noRefinement, item) => noRefinement && item.noRefinement, true),
};
},

Expand All @@ -102,6 +124,7 @@ export default createConnector({
getSearchParameters(searchParameters, props, searchState) {
const {attributeName} = props;
const {start, end} = parseItem(getCurrentRefinement(props, searchState));
searchParameters = searchParameters.addDisjunctiveFacet(attributeName);

if (start) {
searchParameters = searchParameters.addNumericRefinement(
Expand Down
101 changes: 67 additions & 34 deletions packages/react-instantsearch/src/connectors/connectMultiRange.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,19 @@ let params;

describe('connectMultiRange', () => {
it('provides the correct props to the component', () => {
let results = {
getFacetStats: () => ({min: 0, max: 300}),
getFacetByName: () => true,
};

props = getProvidedProps({
items: [
{label: 'All'},
],
}, {});
}, {}, {results});
expect(props).toEqual({
items: [
{label: 'All', value: '', isRefined: true},
{label: 'All', value: '', isRefined: true, noRefinement: false},
],
currentRefinement: '',
canRefine: true,
Expand All @@ -36,11 +41,11 @@ describe('connectMultiRange', () => {
{label: 'All'},
{label: 'Ok', start: 100},
],
}, {});
}, {}, {results});
expect(props).toEqual({
items: [
{label: 'All', value: '', isRefined: true},
{label: 'Ok', value: '100:', isRefined: false},
{label: 'All', value: '', isRefined: true, noRefinement: false},
{label: 'Ok', value: '100:', isRefined: false, noRefinement: false},
],
currentRefinement: '',
canRefine: true,
Expand All @@ -51,12 +56,11 @@ describe('connectMultiRange', () => {
{label: 'All'},
{label: 'Not ok', end: 200},
],
canRefine: true,
}, {});
}, {}, {results});
expect(props).toEqual({
items: [
{label: 'All', value: '', isRefined: true},
{label: 'Not ok', value: ':200', isRefined: false},
{label: 'All', value: '', isRefined: true, noRefinement: false},
{label: 'Not ok', value: ':200', isRefined: false, noRefinement: false},
],
currentRefinement: '',
canRefine: true,
Expand All @@ -69,45 +73,74 @@ describe('connectMultiRange', () => {
{label: 'Not ok', end: 200},
{label: 'Maybe ok?', start: 100, end: 200},
],
canRefine: true,
}, {});
}, {}, {results});
expect(props).toEqual({
items: [
{label: 'All', value: '', isRefined: true},
{label: 'Ok', value: '100:', isRefined: false},
{label: 'Not ok', value: ':200', isRefined: false},
{label: 'Maybe ok?', value: '100:200', isRefined: false},
{label: 'All', value: '', isRefined: true, noRefinement: false},
{label: 'Ok', value: '100:', isRefined: false, noRefinement: false},
{label: 'Not ok', value: ':200', isRefined: false, noRefinement: false},
{label: 'Maybe ok?', value: '100:200', isRefined: false, noRefinement: false},
],
currentRefinement: '',
canRefine: true,
});

props = getProvidedProps({attributeName: 'ok', items: []}, {multiRange: {ok: 'wat'}});
expect(props).toEqual({items: [], currentRefinement: 'wat', canRefine: false});
it('no items define', () => {
props = getProvidedProps({attributeName: 'ok', items: []}, {multiRange: {ok: 'wat'}}, {});
expect(props).toEqual({items: [], currentRefinement: 'wat', canRefine: false});

props = getProvidedProps({attributeName: 'ok', items: []}, {multiRange: {ok: 'wat'}});
expect(props).toEqual({items: [], currentRefinement: 'wat', canRefine: false});
props = getProvidedProps({attributeName: 'ok', items: []}, {multiRange: {ok: 'wat'}}, {});
expect(props).toEqual({items: [], currentRefinement: 'wat', canRefine: false});

props = getProvidedProps({attributeName: 'ok', items: [], defaultRefinement: 'wat'}, {});
expect(props).toEqual({items: [], currentRefinement: 'wat', canRefine: false});
props = getProvidedProps({attributeName: 'ok', items: [], defaultRefinement: 'wat'}, {}, {});
expect(props).toEqual({items: [], currentRefinement: 'wat', canRefine: false});
});

const transformItems = jest.fn(() => ['items']);
props = getProvidedProps({
items: [
it('use the transform items props if passed', () => {
const transformItems = jest.fn(() => ['items']);
props = getProvidedProps({
items: [
{label: 'All'},
{label: 'Ok', start: 100},
{label: 'Not ok', end: 200},
{label: 'Maybe ok?', start: 100, end: 200},
],
transformItems,
}, {});
expect(transformItems.mock.calls[0][0]).toEqual([
{label: 'All', value: '', isRefined: true},
{label: 'Ok', value: '100:', isRefined: false},
{label: 'Not ok', value: ':200', isRefined: false},
{label: 'Maybe ok?', value: '100:200', isRefined: false},
]);
expect(props.items).toEqual(['items']);
],
transformItems,
}, {}, {results});
expect(transformItems.mock.calls[0][0]).toEqual([
{label: 'All', value: '', isRefined: true, noRefinement: false},
{label: 'Ok', value: '100:', isRefined: false, noRefinement: false},
{label: 'Not ok', value: ':200', isRefined: false, noRefinement: false},
{label: 'Maybe ok?', value: '100:200', isRefined: false, noRefinement: false},
]);
expect(props.items).toEqual(['items']);
});

it('compute the no refinement value for each item range when stats exists', () => {
results = {
getFacetStats: () => ({min: 250, max: 300}),
getFacetByName: () => true,
};

props = getProvidedProps({
items: [
{label: '1', start: 100},
{label: '2', start: 400},
{label: '3', end: 200},
{label: '4', start: 100, end: 200},
],
}, {}, {results});
expect(props).toEqual({
items: [
{label: '1', value: '100:', isRefined: false, noRefinement: false},
{label: '2', value: '400:', isRefined: false, noRefinement: true},
{label: '3', value: ':200', isRefined: false, noRefinement: true},
{label: '4', value: '100:200', isRefined: false, noRefinement: true},
],
currentRefinement: '',
canRefine: true,
});
});
});

it('calling refine updates the widget\'s search state', () => {
Expand Down
3 changes: 2 additions & 1 deletion packages/react-instantsearch/src/widgets/MultiRange.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ import MultiRangeComponent from '../components/MultiRange.js';
* @themeKey ais-MultiRange__itemLabelSelected - The selected label item
* @themeKey ais-MultiRange__itemRadio - The radio of an item
* @themeKey ais-MultiRange__itemRadioSelected - The selected radio item
* @themeKey ais-MultiRange__noRefinement - present when there is no refinement
* @themeKey ais-MultiRange__noRefinement - present when there is no refinement for all ranges
* @themeKey ais-MultiRange__itemNoRefinement - present when there is no refinement for one range
* @example
* import React from 'react';
*
Expand Down

0 comments on commit 80b6450

Please sign in to comment.