diff --git a/packages/react-instantsearch/src/components/CurrentRefinements.css b/packages/react-instantsearch/src/components/CurrentRefinements.css index 2966f2595c..77ceb98585 100644 --- a/packages/react-instantsearch/src/components/CurrentRefinements.css +++ b/packages/react-instantsearch/src/components/CurrentRefinements.css @@ -10,5 +10,8 @@ .itemLabel { } +.itemParent { +} + .itemClear { } \ No newline at end of file diff --git a/packages/react-instantsearch/src/components/CurrentRefinements.js b/packages/react-instantsearch/src/components/CurrentRefinements.js index 1a5f548e67..13d5bb9162 100644 --- a/packages/react-instantsearch/src/components/CurrentRefinements.js +++ b/packages/react-instantsearch/src/components/CurrentRefinements.js @@ -27,17 +27,32 @@ class CurrentRefinements extends Component {
{items.map(item =>
{item.label} - + {item.items ? + item.items.map(nestedItem => +
+ + {nestedItem.label} + + +
) : + }
)}
diff --git a/packages/react-instantsearch/src/connectors/connectCurrentRefinements.js b/packages/react-instantsearch/src/connectors/connectCurrentRefinements.js index 52b042673e..5a84840ace 100644 --- a/packages/react-instantsearch/src/connectors/connectCurrentRefinements.js +++ b/packages/react-instantsearch/src/connectors/connectCurrentRefinements.js @@ -8,7 +8,7 @@ import createConnector from '../core/createConnector'; * @kind connector * @category connector * @providedPropType {function} refine - a function to remove a single filter - * @providedPropType {array.<{key: string, label: string}>} items - all the filters, the key for calling the refine prop function, label is for the display. + * @providedPropType {array.<{label: string, attributeName: string, currentRefinement: string || object, items: array, value: function}>} items - all the filters, the `value` is to pass to the `refine` function for removing all currentrefinements, `label` is for the display. When existing several refinements for the same atribute name, then you get a nested `items` object that contains a `label` and a `value` function to use to remove a single filter. `attributeName` and `currentRefinement` are metadata containing row values. */ export default createConnector({ displayName: 'AlgoliaCurrentRefinements', @@ -16,12 +16,14 @@ export default createConnector({ getProps(props, state, search, metadata) { return { items: metadata.reduce((res, meta) => - typeof meta.filters !== 'undefined' ? res.concat(meta.filters) : res - , []), + typeof meta.items !== 'undefined' ? res.concat(meta.items) : res + , []), }; }, - refine(props, state, filters) { - return filters.reduce((res, filter) => filter.clear(res), state); + refine(props, state, items) { + // `value` corresponds to our internal clear function computed in each connector metadata. + const refinementsToClear = items instanceof Array ? items.map(item => item.value) : [items]; + return refinementsToClear.reduce((res, clear) => clear(res), state); }, }); diff --git a/packages/react-instantsearch/src/connectors/connectCurrentRefinements.test.js b/packages/react-instantsearch/src/connectors/connectCurrentRefinements.test.js index 00ac2bfb63..f80c97ca59 100644 --- a/packages/react-instantsearch/src/connectors/connectCurrentRefinements.test.js +++ b/packages/react-instantsearch/src/connectors/connectCurrentRefinements.test.js @@ -8,16 +8,16 @@ const {refine, getProps} = connect; describe('connectCurrentRefinements', () => { it('provides the correct props to the component', () => { const props = getProps(null, null, null, [ - {filters: ['one']}, - {filters: ['two']}, - {filters: ['three']}, + {items: ['one']}, + {items: ['two']}, + {items: ['three']}, ]); expect(props.items).toEqual(['one', 'two', 'three']); }); it('refine applies the selected filters clear method on state', () => { const state = refine(null, {wow: 'sweet'}, [{ - clear: nextState => ({...nextState, cool: 'neat'}), + value: nextState => ({...nextState, cool: 'neat'}), }]); expect(state).toEqual({wow: 'sweet', cool: 'neat'}); }); diff --git a/packages/react-instantsearch/src/connectors/connectHierarchicalMenu.js b/packages/react-instantsearch/src/connectors/connectHierarchicalMenu.js index 2949714042..c310f4b9f1 100644 --- a/packages/react-instantsearch/src/connectors/connectHierarchicalMenu.js +++ b/packages/react-instantsearch/src/connectors/connectHierarchicalMenu.js @@ -80,7 +80,7 @@ function transformValue(value, limit, props, state) { * @propType {string} [separator='>'] - Specifies the level separator used in the data. * @propType {string[]} [rootPath=null] - The already selected and hidden path. * @propType {boolean} [showParentLevel=true] - Flag to set if the parent level should be displayed. - * @providedPropType {function} refine - a function to remove a single filter + * @providedPropType {function} refine - a function to toggle a refinement * @providedPropType {function} createURL - a function to generate a URL for the corresponding state * @providedPropType {string} currentRefinement - the refinement currently applied * @providedPropType {array.<{children: object, count: number, isRefined: boolean, label: string, value: string}>} items - the list of items the HierarchicalMenu can display. Children has the same shape as parent items. @@ -204,12 +204,14 @@ export default createConnector({ const currentRefinement = getCurrentRefinement(props, state); return { id, - filters: !currentRefinement ? [] : [{ + items: !currentRefinement ? [] : [{ label: `${id}: ${currentRefinement}`, - clear: nextState => ({ + attributeName: id, + value: nextState => ({ ...nextState, [id]: '', }), + currentRefinement, }], }; }, diff --git a/packages/react-instantsearch/src/connectors/connectHierarchicalMenu.test.js b/packages/react-instantsearch/src/connectors/connectHierarchicalMenu.test.js index c7846e66e6..530511955d 100644 --- a/packages/react-instantsearch/src/connectors/connectHierarchicalMenu.test.js +++ b/packages/react-instantsearch/src/connectors/connectHierarchicalMenu.test.js @@ -196,21 +196,23 @@ describe('connectHierarchicalMenu', () => { it('registers its id in metadata', () => { const metadata = getMetadata({id: 'ok'}, {}); - expect(metadata).toEqual({id: 'ok', filters: []}); + expect(metadata).toEqual({items: [], id: 'ok'}); }); it('registers its filter in metadata', () => { const metadata = getMetadata({id: 'ok'}, {ok: 'wat'}); expect(metadata).toEqual({ id: 'ok', - filters: [{ + items: [{ label: 'ok: wat', + attributeName: 'ok', + currentRefinement: 'wat', // Ignore clear, we test it later - clear: metadata.filters[0].clear, + value: metadata.items[0].value, }], }); - const state = metadata.filters[0].clear({ok: 'wat'}); + const state = metadata.items[0].value({ok: 'wat'}); expect(state).toEqual({ok: ''}); }); }); diff --git a/packages/react-instantsearch/src/connectors/connectMenu.js b/packages/react-instantsearch/src/connectors/connectMenu.js index 7c5487f597..2199f2e47e 100644 --- a/packages/react-instantsearch/src/connectors/connectMenu.js +++ b/packages/react-instantsearch/src/connectors/connectMenu.js @@ -33,7 +33,7 @@ function getCurrentRefinement(props, state) { * @propType {number} [limitMax=20] - the maximun number of displayed items. Only used when showMore is set to `true` * @propType {string[]} [sortBy=['count:desc','name:asc']] - defines how the items are sorted. See [the helper documentation](https://community.algolia.com/algoliasearch-helper-js/reference.html#specifying-a-different-sort-order-for-values) for the full list of options * @propType {string} defaultRefinement - the value of the item selected by default - * @providedPropType {function} refine - a function to remove a single filter + * @providedPropType {function} refine - a function to toggle a refinement * @providedPropType {function} createURL - a function to generate a URL for the corresponding state * @providedPropType {string} currentRefinement - the refinement currently applied * @providedPropType {array.<{count: number, isRefined: boolean, label: string, value: string}>} items - the list of items the Menu can display. @@ -124,12 +124,14 @@ export default createConnector({ const currentRefinement = getCurrentRefinement(props, state); return { id, - filters: currentRefinement === null ? [] : [{ + items: currentRefinement === null ? [] : [{ label: `${props.attributeName}: ${currentRefinement}`, - clear: nextState => ({ + attributeName: props.attributeName, + value: nextState => ({ ...nextState, [id]: '', }), + currentRefinement, }], }; }, diff --git a/packages/react-instantsearch/src/connectors/connectMenu.test.js b/packages/react-instantsearch/src/connectors/connectMenu.test.js index 4b27775a59..f5d24f4413 100644 --- a/packages/react-instantsearch/src/connectors/connectMenu.test.js +++ b/packages/react-instantsearch/src/connectors/connectMenu.test.js @@ -150,21 +150,23 @@ describe('connectMenu', () => { it('registers its id in metadata', () => { const metadata = getMetadata({id: 'ok'}, {}); - expect(metadata).toEqual({id: 'ok', filters: []}); + expect(metadata).toEqual({id: 'ok', items: []}); }); it('registers its filter in metadata', () => { const metadata = getMetadata({id: 'ok', attributeName: 'wot'}, {ok: 'wat'}); expect(metadata).toEqual({ id: 'ok', - filters: [{ + items: [{ label: 'wot: wat', + attributeName: 'wot', + currentRefinement: 'wat', // Ignore clear, we test it later - clear: metadata.filters[0].clear, + value: metadata.items[0].value, }], }); - const state = metadata.filters[0].clear({ok: 'wat'}); + const state = metadata.items[0].value({ok: 'wat'}); expect(state).toEqual({ok: ''}); }); }); diff --git a/packages/react-instantsearch/src/connectors/connectMultiRange.js b/packages/react-instantsearch/src/connectors/connectMultiRange.js index bf6b64b9fd..19eaace7f5 100644 --- a/packages/react-instantsearch/src/connectors/connectMultiRange.js +++ b/packages/react-instantsearch/src/connectors/connectMultiRange.js @@ -47,7 +47,7 @@ function getCurrentRefinement(props, state) { * @propType {string} attributeName - the name of the attribute in the records * @propType {{label: string, start: number, end: number}[]} items - List of options. With a text label, and upper and lower bounds. * @propType {string} defaultRefinement - the value of the item selected by default, follow the shape of a `string` with a pattern of `'{start}:{end}'`. - * @providedPropType {function} refine - a function to remove a single filter + * @providedPropType {function} refine - a function to select a range. * @providedPropType {function} createURL - a function to generate a URL for the corresponding 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 state of the widget is `'10:20'`. When `start` isn't defined, the state of the widget is `':{end}'`, and the same way around when `end` isn't defined. However, when neither `start` nor `end` are defined, the state is an empty string. * @providedPropType {array.<{isRefined: boolean, label: string, value: string}>} items - the list of ranges the MultiRange can display. @@ -113,17 +113,19 @@ export default createConnector({ getMetadata(props, state) { const id = getId(props); const value = getCurrentRefinement(props, state); - const filters = []; + const items = []; if (value !== '') { const {label} = find(props.items, item => stringifyItem(item) === value); - filters.push({ + items.push({ label: `${props.attributeName}: ${label}`, - clear: nextState => ({ + attributeName: props.attributeName, + currentRefinement: label, + value: nextState => ({ ...nextState, [id]: '', }), }); } - return {id, filters}; + return {id, items}; }, }); diff --git a/packages/react-instantsearch/src/connectors/connectMultiRange.test.js b/packages/react-instantsearch/src/connectors/connectMultiRange.test.js index 1d68692a2b..a1b61636f1 100644 --- a/packages/react-instantsearch/src/connectors/connectMultiRange.test.js +++ b/packages/react-instantsearch/src/connectors/connectMultiRange.test.js @@ -118,7 +118,7 @@ describe('connectMultiRange', () => { it('registers its id in metadata', () => { const metadata = getMetadata({id: 'ok'}, {}); - expect(metadata).toEqual({id: 'ok', filters: []}); + expect(metadata).toEqual({id: 'ok', items: []}); }); it('registers its filter in metadata', () => { @@ -135,14 +135,16 @@ describe('connectMultiRange', () => { ); expect(metadata).toEqual({ id: 'wot', - filters: [{ + items: [{ label: 'wot: YAY', // Ignore clear, we test it later - clear: metadata.filters[0].clear, + value: metadata.items[0].value, + attributeName: 'wot', + currentRefinement: 'YAY', }], }); - const state = metadata.filters[0].clear({wot: '100:200'}); + const state = metadata.items[0].value({wot: '100:200'}); expect(state).toEqual({wot: ''}); }); }); diff --git a/packages/react-instantsearch/src/connectors/connectRange.js b/packages/react-instantsearch/src/connectors/connectRange.js index f17cb718ac..f6ea85c9be 100644 --- a/packages/react-instantsearch/src/connectors/connectRange.js +++ b/packages/react-instantsearch/src/connectors/connectRange.js @@ -14,7 +14,7 @@ import createConnector from '../core/createConnector'; * @propType {{min: number, max: number}} defaultRefinement - Default state of the widget containing the start and the end of the range. * @propType {number} min - Minimum value. When this isn't set, the minimum value will be automatically computed by Algolia using the data in the index. * @propType {number} max - Maximum value. When this isn't set, the maximum value will be automatically computed by Algolia using the data in the index. - * @providedPropType {function} refine - a function to remove a single filter + * @providedPropType {function} refine - a function to select a range. * @providedPropType {function} createURL - a function to generate a URL for the corresponding state * @providedPropType {string} currentRefinement - the refinement currently applied */ @@ -125,21 +125,23 @@ export default createConnector({ getMetadata(props, state) { const id = getId(props); const currentRefinement = getCurrentRefinement(props, state); - let filter; + let item; const hasMin = typeof currentRefinement.min !== 'undefined'; const hasMax = typeof currentRefinement.max !== 'undefined'; if (hasMin || hasMax) { - let filterLabel = ''; + let itemLabel = ''; if (hasMin) { - filterLabel += `${currentRefinement.min} <= `; + itemLabel += `${currentRefinement.min} <= `; } - filterLabel += props.attributeName; + itemLabel += props.attributeName; if (hasMax) { - filterLabel += ` <= ${currentRefinement.max}`; + itemLabel += ` <= ${currentRefinement.max}`; } - filter = { - label: filterLabel, - clear: nextState => ({ + item = { + label: itemLabel, + currentRefinement, + attributeName: props.attributeName, + value: nextState => ({ ...nextState, [id]: {}, }), @@ -148,7 +150,7 @@ export default createConnector({ return { id, - filters: filter ? [filter] : [], + items: item ? [item] : [], }; }, }); diff --git a/packages/react-instantsearch/src/connectors/connectRange.test.js b/packages/react-instantsearch/src/connectors/connectRange.test.js index b3d7e0738a..86b6d6fe22 100644 --- a/packages/react-instantsearch/src/connectors/connectRange.test.js +++ b/packages/react-instantsearch/src/connectors/connectRange.test.js @@ -109,14 +109,16 @@ describe('connectRange', () => { ); expect(metadata).toEqual({ id: 'wot', - filters: [{ + items: [{ label: '5 <= wot', + attributeName: 'wot', + currentRefinement: {min: 5, max: undefined}, // Ignore clear, we test it later - clear: metadata.filters[0].clear, + value: metadata.items[0].value, }], }); - const state = metadata.filters[0].clear({wot: {min: 5}}); + const state = metadata.items[0].value({wot: {min: 5}}); expect(state).toEqual({wot: {}}); metadata = getMetadata( @@ -125,9 +127,11 @@ describe('connectRange', () => { ); expect(metadata).toEqual({ id: 'wot', - filters: [{ + items: [{ label: 'wot <= 10', - clear: metadata.filters[0].clear, + attributeName: 'wot', + currentRefinement: {min: undefined, max: 10}, + value: metadata.items[0].value, }], }); @@ -137,9 +141,11 @@ describe('connectRange', () => { ); expect(metadata).toEqual({ id: 'wot', - filters: [{ + items: [{ label: '5 <= wot <= 10', - clear: metadata.filters[0].clear, + attributeName: 'wot', + currentRefinement: {min: 5, max: 10}, + value: metadata.items[0].value, }], }); }); diff --git a/packages/react-instantsearch/src/connectors/connectRefinementList.js b/packages/react-instantsearch/src/connectors/connectRefinementList.js index 6017447c80..89de2a83ef 100644 --- a/packages/react-instantsearch/src/connectors/connectRefinementList.js +++ b/packages/react-instantsearch/src/connectors/connectRefinementList.js @@ -49,7 +49,7 @@ function getValue(name, props, state) { * @propType {number} [limitMax=20] - the maximun number of displayed items. Only used when showMore is set to `true` * @propType {string[]} [sortBy=['count:desc','name:asc']] - defines how the items are sorted. See [the helper documentation](https://community.algolia.com/algoliasearch-helper-js/reference.html#specifying-a-different-sort-order-for-values) for the full list of options * @propType {string[]} defaultRefinement - the values of the items selected by default. The state of this widget takes the form of a list of `string`s, which correspond to the values of all selected refinements. However, when there are no refinements selected, the value of the state is an empty string. - * @providedPropType {function} refine - a function to remove a single filter + * @providedPropType {function} refine - a function to toggle a refinement * @providedPropType {function} createURL - a function to generate a URL for the corresponding state * @providedPropType {string[]} currentRefinement - the refinement currently applied * @providedPropType {array.<{count: number, isRefined: boolean, label: string, value: string}>} items - the list of items the RefinementList can display. @@ -139,26 +139,36 @@ export default createConnector({ searchParameters = searchParameters[addKey](attributeName); return getCurrentRefinement(props, state).reduce((res, val) => - res[addRefinementKey](attributeName, val) - , searchParameters); + res[addRefinementKey](attributeName, val) + , searchParameters); }, getMetadata(props, state) { const id = getId(props); return { id, - filters: getCurrentRefinement(props, state).map(item => ({ - label: `${props.attributeName}: ${item}`, - clear: nextState => { - const nextSelectedItems = getCurrentRefinement(props, nextState).filter( - other => other !== item - ); - return { - ...nextState, - [id]: nextSelectedItems.length > 0 ? nextSelectedItems : '', - }; - }, - })), + items: getCurrentRefinement(props, state).length > 0 ? [{ + attributeName: props.attributeName, + label: `${props.attributeName}: `, + currentRefinement: getCurrentRefinement(props, state), + value: nextState => ({ + ...nextState, + [id]: '', + }), + items: getCurrentRefinement(props, state).map(item => ({ + label: `${item}`, + value: nextState => { + const nextSelectedItems = getCurrentRefinement(props, nextState).filter( + other => other !== item + ); + + return { + ...nextState, + [id]: nextSelectedItems.length > 0 ? nextSelectedItems : '', + }; + }, + })), + }] : [], }; }, }); diff --git a/packages/react-instantsearch/src/connectors/connectRefinementList.test.js b/packages/react-instantsearch/src/connectors/connectRefinementList.test.js index fa2d7a85e7..ab82caf41a 100644 --- a/packages/react-instantsearch/src/connectors/connectRefinementList.test.js +++ b/packages/react-instantsearch/src/connectors/connectRefinementList.test.js @@ -163,7 +163,7 @@ describe('connectRefinementList', () => { it('registers its id in metadata', () => { const metadata = getMetadata({id: 'ok'}, {}); - expect(metadata).toEqual({id: 'ok', filters: []}); + expect(metadata).toEqual({id: 'ok', items: []}); }); it('registers its filter in metadata', () => { @@ -173,22 +173,30 @@ describe('connectRefinementList', () => { ); expect(metadata).toEqual({ id: 'ok', - filters: [ + items: [ { - label: 'wot: wat', - // Ignore clear, we test it later - clear: metadata.filters[0].clear, - }, - { - label: 'wot: wut', - clear: metadata.filters[1].clear, + label: 'wot: ', + attributeName: 'wot', + currentRefinement: ['wat', 'wut'], + value: metadata.items[0].value, + items: [ + { + label: 'wat', + value: metadata.items[0].items[0].value, + }, + { + label: 'wut', + value: metadata.items[0].items[1].value, + }, + ], + // Ignore value, we test it later }, ], }); - let state = metadata.filters[0].clear({ok: ['wat', 'wut']}); + let state = metadata.items[0].items[0].value({ok: ['wat', 'wut']}); expect(state).toEqual({ok: ['wut']}); - state = metadata.filters[1].clear(state); + state = metadata.items[0].items[1].value(state); expect(state).toEqual({ok: ''}); }); }); diff --git a/packages/react-instantsearch/src/connectors/connectToggle.js b/packages/react-instantsearch/src/connectors/connectToggle.js index 58db4880e6..a2a9ffbbaa 100644 --- a/packages/react-instantsearch/src/connectors/connectToggle.js +++ b/packages/react-instantsearch/src/connectors/connectToggle.js @@ -29,7 +29,7 @@ function getCurrentRefinement(props, state) { * @propType {string} function - Custom filter. Takes in a `SearchParameters` and returns a new `SearchParameters` with the filter applied. * @propType {string} value - Value of the refinement to apply on `attributeName`. Required when `attributeName` is present. * @propType {boolean} [defaultChecked=false] - Default state of the widget. Should the toggle be checked by default? - * @providedPropType {function} refine - a function to remove a single filter + * @providedPropType {function} refine - a function to toggle a refinement * @providedPropType {function} createURL - a function to generate a URL for the corresponding state */ export default createConnector({ @@ -80,16 +80,18 @@ export default createConnector({ getMetadata(props, state) { const id = getId(props); const checked = getCurrentRefinement(props, state); - const filters = []; + const items = []; if (checked) { - filters.push({ + items.push({ label: props.label, - clear: nextState => ({ + currentRefinement: props.label, + attributeName: props.attributeName, + value: nextState => ({ ...nextState, [id]: 'off', }), }); } - return {id, filters}; + return {id, items}; }, }); diff --git a/packages/react-instantsearch/src/connectors/connectToggle.test.js b/packages/react-instantsearch/src/connectors/connectToggle.test.js index 138ac32ee0..761c1ba8f9 100644 --- a/packages/react-instantsearch/src/connectors/connectToggle.test.js +++ b/packages/react-instantsearch/src/connectors/connectToggle.test.js @@ -61,23 +61,25 @@ describe('connectToggle', () => { it('registers its filter in metadata', () => { let metadata = getMetadata({id: 't'}, {}); expect(metadata).toEqual({ + items: [], id: 't', - filters: [], }); - metadata = getMetadata({id: 't', label: 'yep'}, {t: 'on'}); + metadata = getMetadata({attributeName: 't', id: 't', label: 'yep'}, {t: 'on'}); expect(metadata).toEqual({ - id: 't', - filters: [ + items: [ { label: 'yep', // Ignore clear, we test it later - clear: metadata.filters[0].clear, + value: metadata.items[0].value, + attributeName: 't', + currentRefinement: 'yep', }, ], + id: 't', }); - const state = metadata.filters[0].clear({t: 'on'}); + const state = metadata.items[0].value({t: 'on'}); expect(state).toEqual({t: 'off'}); }); });